Polygons for reverse geocoding

Combine Additional Data Provider queries with Reverse Geocoding queries to obtain extra data about specific entityType like:

  • Country
  • CountrySubdivision
  • CountrySecondarySubdivision
  • CountryTertiarySubdivision
  • Municipality
  • MunicipalitySubdivision
  • Neighbourhood
  • PostalCodeArea

Sample use case: You want to display Country or Municipality boundaries.

Use the following code to try this in your app:

JAVA
KOTLIN
1ReverseGeocoderSearchQuery reverseGeocoderQuery =
2 createReverseGeocoderQuery(latLng.getLatitude(), latLng.getLongitude());
3
4Observable<RevGeoWithAdpResponse> revGeoWithAdpResponseObservable =
5 revGeoWithAdpRequester.rawReverseGeocoding(reverseGeocoderQuery)
6 .subscribeOn(getWorkerScheduler())
7 .filter(revGeoWithAdpResponse -> !revGeoWithAdpResponse.getRevGeoResponse().getAddresses().isEmpty())
8 .doOnNext(this::updateUiWithResponse);
1val queryBuilder = ReverseGeocoderSearchQueryBuilder(latLng.latitude, latLng.longitude)
2entityType.value?.let {
3 queryBuilder.withEntityType(it)
4}
5
6val adpSearchRequester = AdpSearchRequester(getApplication(), getGeometriesZoomForEntityType())
7adpSearchRequester.revGeoWithAdp(queryBuilder.build(), revGeoWithAdpResult)
JAVA
KOTLIN
1revGeoWithAdpRequester.toPolygonObservable(revGeoWithAdpResponseObservable)
2 .observeOn(getObserverScheduler())
3 .subscribe(polygon -> {
4 tomtomMap.getOverlaySettings().addOverlay(polygon);
5 getProgressDisplayable().hideInProgressDialog();
6 }, error -> getProgressDisplayable().hideInProgressDialog())
1FuncUtils.apply<Geometry>(feature.geometry) { input ->
2 input.accept(object : GeoJsonObjectVisitorAdapter() {
3 override fun visit(polygon: Polygon?) {
4 polygon?.let { item ->
5 displayPolygon(item)
6 tomtomMap.setCurrentBounds(parsePolygonLatLngs(item))
7 }
8 }
9
10 override fun visit(multiPolygon: MultiPolygon?) {
11 val coordinates = ArrayList<LatLng>()
12 multiPolygon?.polygons?.forEach { polygon ->
13 coordinates.addAll(parsePolygonLatLngs(polygon))
14 displayPolygon(polygon)
15 }
16 tomtomMap.setCurrentBounds(coordinates)
17 }
18 })
19}

The class RevGeoWithAdpRequester serves as a helper to create complex queries:

JAVA
KOTLIN
1class RevGeoWithAdpRequester {
2
3 private final static int DEFAULT_GEOMETRIES_ZOOM = 4;
4 private final PolygonAdapter polygonAdapter = new PolygonAdapter();
5 private final SearchApi searchApi;
6
7 private int geometriesZoom = DEFAULT_GEOMETRIES_ZOOM;
8
9 RevGeoWithAdpRequester(SearchApi searchApi) {
10 this.searchApi = searchApi;
11 }
12
13 Observable<Polygon> toPolygonObservable(Observable<RevGeoWithAdpResponse> revGeoWithAdpResponseObservable) {
14 return revGeoWithAdpResponseObservable
15 .flatMap((Function<RevGeoWithAdpResponse, ObservableSource<Polygon>>) revGeoWithAdpResponse -> {
16 AdditionalDataSearchResult adpResult =
17 FuncUtils.first(revGeoWithAdpResponse.getAdpResponse().getResults()).get();
18
19 return createSpecificPolygonObservable(parseGeometryResult(adpResult));
20 });
21 }
22
23 @NonNull
24 @VisibleForTesting
25 protected GeometryResult parseGeometryResult(AdditionalDataSearchResult adpResult) {
26 final AtomicReference<GeometryResult> gr = new AtomicReference<>();
27 adpResult.accept(result -> gr.set(result));
28 return gr.get();
29 }
30
31 Observable<RevGeoWithAdpResponse> rawReverseGeocoding(ReverseGeocoderSearchQuery reverseGeocoderQuery) {
32 return searchApi.reverseGeocoding(reverseGeocoderQuery)
33 .toObservable()
34 .filter(response -> !response.getAddresses().isEmpty()
35 && FuncUtils.first(response.getAddresses()).get()
36 .getAdditionalDataSources().getGeometryDataSource().isPresent())
37 .flatMap(response -> {
38 final GeometryDataSource geometryDataSource =
39 FuncUtils.first(response.getAddresses()).get()
40 .getAdditionalDataSources().getGeometryDataSource().get();
41 return searchApi.additionalDataSearch(createAdditionalDataSearchQuery(geometryDataSource))
42 .toObservable().flatMap(adpResponse ->
43 Observable.just(new RevGeoWithAdpResponse(response, adpResponse)));
44 })
45 .filter(response -> response.isValid())
46 .filter(response -> response.getRevGeoResponse().hasResults())
47 .filter(response -> !response.getAdpResponse().getResults().isEmpty());
48 }
49
50 void setGeometriesZoom(int geometriesZoom) {
51 this.geometriesZoom = geometriesZoom;
52 }
53
54 private AdditionalDataSearchQuery createAdditionalDataSearchQuery(GeometryDataSource geometryDataSource) {
55 return AdditionalDataSearchQueryBuilder.create()
56 .withGeometryDataSource(geometryDataSource)
57 .withGeometriesZoom(geometriesZoom)
58 .build();
59 }
60
61 private Observable<Geometry> createGeometryObservable(GeometryResult result) {
62 return Observable.just(result)
63 .filter(geometryResult -> geometryResult.getGeometryData().isPresent())
64 .map(geometryResult -> geometryResult.getGeometryData().get())
65 .filter(geoJsonObject -> geoJsonObject instanceof FeatureCollection)
66 .map(geoJsonObject -> ((FeatureCollection) geoJsonObject).getFeatures())
67 .filter(features -> !features.isEmpty())
68 .map(features -> FuncUtils.first(features).get())
69 .filter(feature -> feature.getGeometry().isPresent())
70 .map(feature -> feature.getGeometry().get());
71 }
72
73 private Observable<Polygon> createPolygonsObservable(Geometry geometry) {
74 return Observable.just(geometry)
75 .filter(geo -> geo instanceof MultiPolygon)
76 .map(geo -> (MultiPolygon) geo)
77 .flatMap((Function<MultiPolygon, ObservableSource<com.tomtom.online.sdk.common.geojson.geometry.Polygon>>)
78 multiPolygon -> Observable.fromIterable(multiPolygon.getPolygons()))
79 .map(polygon -> getPolygonAdapter().convertToMapPolygon(polygon));
80 }
81
82 private Observable<Polygon> createSinglePolygonObservable(Geometry geometry) {
83 return Observable.just(geometry)
84 .filter(geo -> geo instanceof com.tomtom.online.sdk.common.geojson.geometry.Polygon)
85 .map(geo -> (com.tomtom.online.sdk.common.geojson.geometry.Polygon) geo)
86 .map(polygon -> getPolygonAdapter().convertToMapPolygon(polygon));
87 }
88
89 @VisibleForTesting
90 protected PolygonAdapter getPolygonAdapter() {
91 return polygonAdapter;
92 }
93
94 private Observable<Polygon> createSpecificPolygonObservable(GeometryResult result) {
95 Geometry geometry = createGeometryObservable(result).blockingFirst();
96 return (geometry instanceof MultiPolygon) ?
97 createPolygonsObservable(geometry) :
98 createSinglePolygonObservable(geometry);
99 }
100}
1class AdpSearchRequester(context: Context, private val geometriesZoom: Int) : RxContext {
2
3 private val disposable = SerialDisposable()
4 private val searchApi = OnlineSearchApi.create(context, BuildConfig.SEARCH_API_KEY)
5
6 fun fuzzyWithAdp(searchQuery: FuzzySearchQuery, resource: ResourceLiveData<AdditionalDataSearchResult>) {
7 resource.value = Resource.loading(null)
8 disposable.set(searchApi.search(searchQuery)
9 .subscribeOn(workingScheduler)
10 .filter { nonEmptySearchResults(it) }
11 .map { firstSearchResultWithGeometryDataSource(it) }
12 .filter { presentSearchResult(it) }
13 .map { firstDataSourceForSearchResult(it) }
14 .flatMap { performAdp(it) }
15 .observeOn(resultScheduler)
16 .subscribe(
17 { response -> resource.value = Resource.success(response.results.first()) },
18 { error -> resource.value = Resource.error(null, Error(error.message)) }
19 ))
20 }
21
22 private fun firstDataSourceForSearchResult(searchResult: Optional<FuzzySearchResult>) =
23 searchResult.get().additionalDataSources.geometryDataSource.get()
24
25 private fun presentSearchResult(searchResult: Optional<FuzzySearchResult>) =
26 searchResult.isPresent
27
28 private fun nonEmptySearchResults(searchResponse: FuzzySearchResponse) =
29 searchResponse.results.isEmpty().not()
30
31 private fun firstSearchResultWithGeometryDataSource(searchResponse: FuzzySearchResponse): Optional<FuzzySearchResult> {
32 return Optional.fromNullable(searchResponse.results.find { item ->
33 item.additionalDataSources?.geometryDataSource!!.isPresent
34 })
35 }
36
37 fun revGeoWithAdp(revGeoQuery: ReverseGeocoderSearchQuery, resource: ResourceLiveData<RevGeoWithAdpResponse>) {
38 val result = RevGeoWithAdpResponse()
39 resource.value = Resource.loading(null)
40
41 disposable.set(searchApi.reverseGeocoding(revGeoQuery)
42 .subscribeOn(workingScheduler)
43 .filter { nonEmptyRevGeoResults(it) }
44 .map { firstRevGeoResultWithGeometryDataSource(it) }
45 .map {
46 result.revGeoResult = it.orNull()
47 firstDataSourceForRevGeoResult(it)
48 }
49 .filter { nonEmptyGeometryDataSource(it) }
50 .map { it.get() }
51 .flatMap { performAdp(it) }
52 .observeOn(resultScheduler)
53 .subscribe(
54 { response ->
55 result.adpResult = response.results
56 resource.value = Resource.success(result)
57 },
58 { error -> resource.value = Resource.error(null, Error(error.message)) }
59 )
60 )
61 }
62
63 private fun nonEmptyGeometryDataSource(dataSource: Optional<GeometryDataSource>) =
64 dataSource.isPresent
65
66 private fun nonEmptyRevGeoResults(response: ReverseGeocoderSearchResponse) =
67 response.hasResults()
68
69 private fun firstRevGeoResultWithGeometryDataSource(revGeoResponse: ReverseGeocoderSearchResponse): Optional<ReverseGeocoderFullAddress> =
70 Optional.fromNullable(revGeoResponse.addresses.find { item ->
71 item.additionalDataSources?.geometryDataSource!!.isPresent
72 })
73
74 private fun firstDataSourceForRevGeoResult(it: Optional<ReverseGeocoderFullAddress>): Optional<GeometryDataSource> {
75 it.orNull()?.let {
76 return it.additionalDataSources!!.geometryDataSource!!
77 } ?: run {
78 return Optional.absent<GeometryDataSource>()
79 }
80 }
81
82 private fun performAdp(dataSource: GeometryDataSource): Maybe<AdditionalDataSearchResponse> {
83 val adpQuery = AdditionalDataSearchQueryBuilder.create()
84 .withGeometryDataSource(dataSource)
85 .withGeometriesZoom(geometriesZoom)
86 val request = searchApi.additionalDataSearch(adpQuery.build())
87 return request.toMaybe()
88 }
89
90 override fun getWorkingScheduler() = Schedulers.newThread()
91
92 override fun getResultScheduler() = AndroidSchedulers.mainThread()!!
93
94}

Sample views utilizing entities retrieved by combining both services:

Boundaries for a selected country

Boundaries for a selected municipality