Skip to content

Commit

Permalink
Merge pull request #742 from ybert/reverse-with-osm-tag
Browse files Browse the repository at this point in the history
Reverse Geocoding with osm tag
  • Loading branch information
lonvia authored Jul 27, 2023
2 parents 9855305 + 99bca60 commit 8f44f0f
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 6 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ Or, just by they key
http://localhost:2322/api?q=berlin&osm_tag=tourism
```

You can also use this feature for reverse geocoding. Want to see the 5 pharmacies closest to a location ?

```
http://localhost:2322/reverse?lon=10&lat=52&osm_tag=amenity:pharmacy&limit=5
```

#### Filter results by layer

List of available layers:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ private SearchResponse search(QueryBuilder queryBuilder, Integer limit, Point lo
}

private ReverseQueryBuilder buildQuery(ReverseRequest photonRequest) {
return ReverseQueryBuilder.builder(photonRequest.getLocation(), photonRequest.getRadius(),
photonRequest.getQueryStringFilter(), photonRequest.getLayerFilters());
return ReverseQueryBuilder.
builder(photonRequest.getLocation(), photonRequest.getRadius(), photonRequest.getQueryStringFilter(), photonRequest.getLayerFilters()).
withOsmTagFilters(photonRequest.getOsmTagFilters());
}

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package de.komoot.photon.elasticsearch;

import com.vividsolutions.jts.geom.Point;
import de.komoot.photon.searcher.TagFilter;
import de.komoot.photon.searcher.TagFilterKind;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermsQueryBuilder;

import java.util.List;
import java.util.Set;

/**
Expand All @@ -18,6 +21,9 @@ public class ReverseQueryBuilder {
private String queryStringFilter;
private Set<String> layerFilter;

private BoolQueryBuilder orQueryBuilderForIncludeTagFiltering = null;
private BoolQueryBuilder andQueryBuilderForExcludeTagFiltering = null;

private ReverseQueryBuilder(Point location, Double radius, String queryStringFilter, Set<String> layerFilter) {
this.location = location;
this.radius = radius;
Expand All @@ -42,10 +48,68 @@ public QueryBuilder buildQuery() {
finalQuery.must(new TermsQueryBuilder("type", layerFilter));
}

if (orQueryBuilderForIncludeTagFiltering != null || andQueryBuilderForExcludeTagFiltering != null) {
BoolQueryBuilder tagFilters = QueryBuilders.boolQuery();
if (orQueryBuilderForIncludeTagFiltering != null)
tagFilters.must(orQueryBuilderForIncludeTagFiltering);
if (andQueryBuilderForExcludeTagFiltering != null)
tagFilters.mustNot(andQueryBuilderForExcludeTagFiltering);
finalQuery.filter(tagFilters);
}

if (finalQuery.must().size() == 0) {
finalQuery.must(QueryBuilders.matchAllQuery());
}

return finalQuery.filter(fb);
}

public ReverseQueryBuilder withOsmTagFilters(List<TagFilter> filters) {
for (TagFilter filter : filters) {
addOsmTagFilter(filter);
}
return this;
}

public ReverseQueryBuilder addOsmTagFilter(TagFilter filter) {
if (filter.getKind() == TagFilterKind.EXCLUDE_VALUE) {
appendIncludeTermQuery(QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("osm_key", filter.getKey()))
.mustNot(QueryBuilders.termQuery("osm_value", filter.getValue())));
} else {
QueryBuilder builder;
if (filter.isKeyOnly()) {
builder = QueryBuilders.termQuery("osm_key", filter.getKey());
} else if (filter.isValueOnly()) {
builder = QueryBuilders.termQuery("osm_value", filter.getValue());
} else {
builder = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("osm_key", filter.getKey()))
.must(QueryBuilders.termQuery("osm_value", filter.getValue()));
}
if (filter.getKind() == TagFilterKind.INCLUDE) {
appendIncludeTermQuery(builder);
} else {
appendExcludeTermQuery(builder);
}
}
return this;
}

private void appendIncludeTermQuery(QueryBuilder termQuery) {

if (orQueryBuilderForIncludeTagFiltering == null)
orQueryBuilderForIncludeTagFiltering = QueryBuilders.boolQuery();

orQueryBuilderForIncludeTagFiltering.should(termQuery);
}


private void appendExcludeTermQuery(QueryBuilder termQuery) {

if (andQueryBuilderForExcludeTagFiltering == null)
andQueryBuilderForExcludeTagFiltering = QueryBuilders.boolQuery();

andQueryBuilderForExcludeTagFiltering.should(termQuery);
}
}
14 changes: 13 additions & 1 deletion src/main/java/de/komoot/photon/query/ReverseRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import com.vividsolutions.jts.geom.Point;

import de.komoot.photon.searcher.TagFilter;

import java.io.Serializable;
import java.util.Set;
import java.util.*;

/**
* @author svantulden
Expand All @@ -16,6 +18,7 @@ public class ReverseRequest implements Serializable {
private String queryStringFilter;
private Boolean locationDistanceSort = true;
private Set<String> layerFilters;
private final List<TagFilter> osmTagFilters = new ArrayList<>(1);
private boolean debug;

public ReverseRequest(Point location, String language, Double radius, String queryStringFilter, Integer limit,
Expand Down Expand Up @@ -58,7 +61,16 @@ public Set<String> getLayerFilters() {
return layerFilters;
}

public List<TagFilter> getOsmTagFilters() {
return osmTagFilters;
}

public boolean getDebug() {
return debug;
}

ReverseRequest addOsmTagFilter(TagFilter filter) {
osmTagFilters.add(filter);
return this;
}
}
19 changes: 17 additions & 2 deletions src/main/java/de/komoot/photon/query/ReverseRequestFactory.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package de.komoot.photon.query;

import com.vividsolutions.jts.geom.Point;

import de.komoot.photon.searcher.TagFilter;
import spark.QueryParamsMap;
import spark.Request;

Expand All @@ -18,7 +20,7 @@ public class ReverseRequestFactory {
private final LayerParamValidator layerParamValidator;

private static final HashSet<String> REQUEST_QUERY_PARAMS = new HashSet<>(Arrays.asList("lang", "lon", "lat", "radius",
"query_string_filter", "distance_sort", "limit", "layer", "debug"));
"query_string_filter", "distance_sort", "limit", "layer", "osm_tag", "debug"));

public ReverseRequestFactory(List<String> supportedLanguages, String defaultLanguage) {
this.languageResolver = new RequestLanguageResolver(supportedLanguages, defaultLanguage);
Expand Down Expand Up @@ -83,6 +85,19 @@ public ReverseRequest create(Request webRequest) throws BadRequestException {
}

String queryStringFilter = webRequest.queryParams("query_string_filter");
return new ReverseRequest(location, language, radius, queryStringFilter, limit, locationDistanceSort, layerFilter, enableDebug);
ReverseRequest request = new ReverseRequest(location, language, radius, queryStringFilter, limit, locationDistanceSort, layerFilter, enableDebug);

QueryParamsMap tagFiltersQueryMap = webRequest.queryMap("osm_tag");
if (tagFiltersQueryMap.hasValue()) {
for (String filter : tagFiltersQueryMap.values()) {
TagFilter tagFilter = TagFilter.buildOsmTagFilter(filter);
if (tagFilter == null) {
throw new BadRequestException(400, String.format("Invalid parameter 'osm_tag=%s': bad syntax for tag filter.", filter));
}
request.addOsmTagFilter(TagFilter.buildOsmTagFilter(filter));
}
}

return request;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package de.komoot.photon.query;

import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.params.provider.Arguments.arguments;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Point;

import de.komoot.photon.ESBaseTester;
import de.komoot.photon.Importer;
import de.komoot.photon.PhotonDoc;
import de.komoot.photon.searcher.PhotonResult;
import de.komoot.photon.searcher.TagFilter;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class QueryReverseFilterTagValueTest extends ESBaseTester {
@TempDir
private static Path instanceTestDirectory;

private static final String[] TAGS = new String[]{"tourism", "attraction",
"tourism", "hotel",
"tourism", "museum",
"tourism", "information",
"amenity", "parking",
"amenity", "restaurant",
"amenity", "information",
"food", "information",
"railway", "station"};

@BeforeAll
public void setup() throws IOException {
setUpES(instanceTestDirectory, "en", "de", "fr");
Importer instance = makeImporter();
double lon = 13.38886;
double lat = 52.51704;
for (int i = 0; i < TAGS.length; i++) {
String key = TAGS[i];
String value = TAGS[++i];
PhotonDoc doc = this.createDoc(lon, lat, i, i, key, value);
instance.add(doc);
lon += 0.00004;
lat += 0.00006;
doc = this.createDoc(lon, lat, i + 1, i + 1, key, value);
instance.add(doc);
lon += 0.00004;
lat += 0.00006;
}
instance.finish();
refresh();
}

@AfterAll
@Override
public void tearDown() {
super.tearDown();
}

private List<PhotonResult> reverseWithTags(String[] params) {
Point pt = FACTORY.createPoint(new Coordinate(13.38886, 52.51704));
ReverseRequest request = new ReverseRequest(pt, "en", 1.0, "", 50, true, new HashSet<>(), false);
for (String param : params) {
request.addOsmTagFilter(TagFilter.buildOsmTagFilter(param));
}
return getServer().createReverseHandler().reverse(request);
}

@ParameterizedTest
@MethodSource("simpleTagFilterProvider")
public void testSingleTagFilter(String filter, int expectedResults) {
assertEquals(expectedResults, reverseWithTags(new String[]{filter}).size());
}

static Stream<Arguments> simpleTagFilterProvider() {
return Stream.of(
arguments("tourism:attraction", 2),
arguments(":attraction", 2),
arguments(":information", 6),
arguments("tourism", 8),
arguments("!tourism:attraction", 16),
arguments(":!information", 12),
arguments("!tourism", 10),
arguments("tourism:!information", 6)
);
}

@ParameterizedTest
@MethodSource("combinedTagFilterProvider")
public void testCombinedTagFilter(String[] filters, int expectedResults) {
assertEquals(expectedResults, reverseWithTags(filters).size());
}

static Stream<Arguments> combinedTagFilterProvider() {
return Stream.of(
arguments(new String[]{"food", "amenity"}, 8),
arguments(new String[]{":parking", ":museum"}, 4),
arguments(new String[]{"food", ":information"}, 6),
arguments(new String[]{"!tourism", "!amenity"}, 4),
arguments(new String[]{"tourism", "!amenity"}, 8),
arguments(new String[]{":information", "!amenity"}, 4),
arguments(new String[]{"tourism:!information", "food"}, 8),
arguments(new String[]{"tourism:!information", "tourism:!hotel"}, 8),
arguments(new String[]{"tourism", "!:information", "food"}, 6)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import de.komoot.photon.searcher.TagFilter;
import spark.QueryParamsMap;
import spark.Request;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

Expand All @@ -31,11 +34,21 @@ public Request createRequestWithLongitudeLatitude(Double longitude, Double latit
Mockito.when(mockRequest.queryParamOrDefault("distance_sort", "true")).thenReturn("true");

QueryParamsMap mockEmptyQueryParamsMap = Mockito.mock(QueryParamsMap.class);
Mockito.when(mockRequest.queryMap("osm_tag")).thenReturn(mockEmptyQueryParamsMap);
Mockito.when(mockRequest.queryMap("layer")).thenReturn(mockEmptyQueryParamsMap);

return mockRequest;
}

public void requestWithOsmFilters(Request mockRequest, String... filterParams) {
Mockito.when(mockRequest.queryParams("q")).thenReturn("new york");

QueryParamsMap mockQueryParamsMap = Mockito.mock(QueryParamsMap.class);
Mockito.when(mockQueryParamsMap.hasValue()).thenReturn(true);
Mockito.when(mockQueryParamsMap.values()).thenReturn(filterParams);
Mockito.when(mockRequest.queryMap("osm_tag")).thenReturn(mockQueryParamsMap);
}

public void requestWithLayers(Request mockRequest, String... layers) {
QueryParamsMap mockQueryParamsMap = Mockito.mock(QueryParamsMap.class);
Mockito.when(mockQueryParamsMap.hasValue()).thenReturn(true);
Expand Down Expand Up @@ -211,6 +224,32 @@ public void testWithBadLayerFilters() {
assertBadRequest(mockRequest, "Invalid layer 'bad'. Allowed layers are: house,street,locality,district,city,county,state,country");
}

@Test
public void testTagFilters() throws Exception {
Request mockRequest = createRequestWithLongitudeLatitude(-87d, 41d);
requestWithOsmFilters(mockRequest, "foo", ":!bar");
ReverseRequestFactory reverseRequestFactory = new ReverseRequestFactory(Collections.singletonList("en"), "en");
reverseRequest = reverseRequestFactory.create(mockRequest);

List<TagFilter> result = reverseRequest.getOsmTagFilters();

assertEquals(2, result.size());

assertAll("filterlist",
() -> assertNotNull(result.get(0)),
() -> assertNotNull(result.get(1))
);
}

@Test
public void testBadTagFilters() throws BadRequestException {
Request mockRequest = createRequestWithLongitudeLatitude(-87d, 41d);
requestWithOsmFilters(mockRequest, "good", "bad:bad:bad");
ReverseRequestFactory reverseRequestFactory = new ReverseRequestFactory(Collections.singletonList("en"), "en");

assertThrows(BadRequestException.class, () -> reverseRequestFactory.create(mockRequest));
}

@Test
public void testWithDebug() throws Exception {
Request mockRequest = createRequestWithLongitudeLatitude(-87d, 41d);
Expand Down

0 comments on commit 8f44f0f

Please sign in to comment.