📜 ⬆️ ⬇️

Search MapKit: Tips & Tricks


MapKit is a software library that allows you to use Yandex cartographic data and technologies in mobile applications. It has official documentation , which already contains a detailed description of API methods, so today we will talk about something else.


In this post I will tell Habr readers about the features of the search in MapKit and share recommendations and tricks that may be useful to you.


TL; DR If you do not want to read the entire article, then here are two of the most useful points as compensation for reading the preface:



Links to documentation in the text will be for Android, classes and methods for iOS are called similarly.


What can search


First of all, let's talk about what can search in MapKit. The search can do what you roughly expect from an application with maps when you want to find something there.



When you type "cafe", "Lev Tolstoy street, 16" or "tram 3" in the search bar, the search by text works. This is the most sophisticated type of search. Heaped up in the sense that it supports the maximum set of parameters for customization. You can try to immediately search along the route or an interesting street, specify the desired number of results, set the user's position and so on. If after the first search you want to move the map or apply filters to the query (“pharmacies with a swimming pool”) - these are requests .


Reverse search is familiar to most users on the question "What is it?". It allows you to click on the map to determine which street or house is “under the cursor” or which organizations are near this point. Search by URI is needed when you want to find a specific object. It can be used, for example, to create bookmarks in an application. They found their favorite coffee house, marked it with an asterisk - next time it will be possible to find this organization by URI, wherever the map window is located. Neither reverse lookup nor URI lookups support re-queries, because there is nothing for them to specify.


Another possibility that lives in the search is search hints that allow you to automatically complete the query while you type it. But a detailed story about them postponed for another time.


How the request is arranged


Search, like many parts of MapKit, works asynchronously. The main object for working with this asynchrony is a search session . Let's look at a small example.


Little about examples

The examples in the article will be on Kotlin, so that it is easier to work with optional values ​​and the boilerplate code is smaller. MapKit has a demo app . It can be used to test examples, but for this SearchActivity worth converting from Java to Kotlin. showMessage , which from time to time appears in the code, is any convenient way for you to display a line of text on the screen or in the log.


 // `searchManager`  `searchSession` –  .    //    ,     . searchManager = SearchFactory.getInstance().createSearchManager( SearchManagerType.ONLINE ) val point = Geometry.fromPoint(Point(59.95, 30.32)) searchSession = searchManager!!.submit("", point, SearchOptions(), object: Session.SearchListener { override fun onSearchError(p0: Error) { showMessage("Error") } override fun onSearchResponse(p0: Response) { showMessage("Success") } } ) 

Immediately after the submit call, the control will return to your code, and when MapKit receives a response from the server, SearchListener will be called.


The search session allows you to:



Session at destruction is automatically canceled. This means that if it is not saved on the side of the client code, the search will not work.
Do not forget to save the session, the session is your friend!


Search options


A common way to customize search queries is the SearchOptions class, which allows you to change query parameters.



In addition to the listed parameters, there are still some that may be useful to you; you can find them in the class documentation. Please note that not all queries support all combinations of parameters. The documentation for each SearchManager or Session method indicates which parameters from SearchOptions it understands.


How does the answer work?


Judging by the questions in support, the users are most confused by the format of the search response. If you look at the class of the answer, then it looks quite simple (at least, the interesting part of us):


 public class Response { public synchronized SearchMetadata getMetadata(); public synchronized GeoObjectCollection getCollection(); // ... } 

Here, getCollection() returns objects in the response, and getMetadata() some additional data that includes, for example, information about the response window , the type of ranking, and the number of results found . If you look inside the GeoObjectCollection you can see that there are some Item s in it, which can be either other GeoObjectCollection or GeoObject .


Collections within collections in the search does not happen (at least for now), so let's look at GeoObject . Inside the object there is a name ( getName() ), a description ( getDescriptionText() ), a frame ( getBoundingBox() ), a set of geometries ( getGeometry() ), and a few other not very clear methods. Where are the phone numbers of the organization? How to understand which city the toponym belongs to?


According to the methods of the object, this is not very clear.


Geoobject


It is time to talk more about GeoObject .


GeoObject is such a basic "card" object. Inside it can be a traffic event, a separate object from the search result, a maneuver in the route or an object on the map (POI), such as a monument or some conspicuous organization.



All the most interesting thing about an object is stored in metadata. They can be accessed using the getMetadataContainer() method. The key in this container is the type of metadata. If you see in the documentation something that ends with the word Metadata , then you will most likely come here. In search of different "metadata" pieces 15.



Metadata can be divided into several types. The first type is metadata that determines what type of object the object is: toponyms ( ToponymObjectMetadata ), organizations ( BusinessObjectMetadata ) or transport ( TransitObjectMetadata ). In the metadata for the toponym, you can find a structured address and detailed geometry. The organization’s metadata is the opening hours or company website. This metadata is determined by the type of search in the query - if you searched for only toponyms, then each object in the response must have corresponding metadata. If you searched for place names or organizations, then each object will have at least one of the two "metadata".


Here's how to find phone numbers for a company:


 val phones = response.collection.children.firstOrNull()?.obj ?.metadataContainer ?.getItem(BusinessObjectMetadata::class.java) ?.phones 

But to find a city in a structured address:


 val city = response.collection.children.firstOrNull()?.obj ?.metadataContainer ?.getItem(ToponymObjectMetadata::class.java) ?.address ?.components ?.firstOrNull { it.kinds.contains(Address.Component.Kind.LOCALITY) } ?.name 

The second type is the metadata that comes with the object, although you did not ask for it. The main type you need to know about is URIObjectMetadata . Inside the URIObjectMetadata is stored the unique identifier of the object that needs to be passed to the search by URI .


 //       «»,     //     val uri = response.collection.children.firstOrNull()?.obj ?.metadataContainer ?.getItem(UriObjectMetadata::class.java) ?.uris ?.firstOrNull() ?.value 

And the third type is metadata that will come in response only if you specifically ask for a search about it. Differently, this metadata is called snippet . Snippets are small additional pieces of data that either change more often than the main “reference” data or that are not needed by everyone. This may be a rating, a link to photos or panoramas, the exchange rate or the price of fuel at a gas station. The list of snippets must be specified using search options. If the server has an ordered snippet, then it will add it to the corresponding object.


 val point = Geometry.fromPoint(Point(59.95, 30.32)) val options = SearchOptions() options.snippets = Snippet.FUEL.value searchSession = searchManager!!.submit("", point, options, this) ... override fun onSearchResponse(response: Response) { //         showMessage(response.collection.children.firstOrNull()?.obj ?.metadataContainer ?.getItem(FuelMetadata::class.java) ?.fuels ?.joinToString("\n") { "Fuel(name=${it.name}, price=${it.price?.text})" } ?: "No fuel" ) } 

All the metadata listed above are added to individual objects in the response. There is also metadata that is added to the answer entirely. But they are rendered into SearchMetadata methods and they do not need to be extracted from any special collection.


 //            response.metadata.businessResultMetadata?.categories //     (  )     response.metadata.toponymResultMetadata?.responseInfo?.mode 

Examples of using


Now let's go through the main methods of search classes, look at usage examples and for some unobvious moments associated with them.


Search by text


The main method for searching by text (and for the whole search, probably) is submit :


 Session submit( String text, Geometry geometry, SearchOptions searchOptions, SearchListener searchListener ); 


The server may consider that the correct answer is not in the window in which the initial search was performed (“cafe in Vladivostok”, when the search box in Moscow). In this case, you will need to take the answer window and move the card there so that the results can be seen on the screen (re-querying yourself this is not allowed and the card is not asked to move).


The submit method has a submit brother:


 Session submit( String text, Polyline polyline, Geometry geometry, SearchOptions searchOptions, SearchListener searchListener ); 

with one additional parameter. This parameter can be used to transfer a large y polyline (for example, a route to another city) and a small search box. Then the search itself will cut out the necessary part of the transferred polyline and will use only it for the query.


Requests


Requests, unlike other types of requests, are made using a search session, which returns the same submit and his twin brother. Part of the session methods is simple and clear:



To perform a refined search, use the resubmit method. It accepts the same SearchListener as a normal search. Before calling it, you can change several parameters of the session. For example, at the same time change the type of ranking and apply filters.


Filters


Since we are talking about filters. Filters are when “Wi-Fi” and “Italian cuisine”. They probably have the most confusing syntax of all the search interfaces in MapKit. This is due to the fact that the same data structures are used to obtain filters from the search response and to specify filters in the re-query.



Filters are of two types. Boolean filters assume only two mutually exclusive values ​​- “is” or “no”. This may be the presence of Wi-Fi in a cafe, a toilet at a gas station or parking next to the organization. Enum filters assume a variety of values ​​that can be queried together. This, for example, the type of kitchen for a cafe or types of fuel at the gas station.


Let's first see how to get the filters available for the current rerouting:


 private fun filters(response: Response): String? { fun enumValues(filter: BusinessFilter) = filter .values .enums ?.joinToString(prefix = " -> ") { e -> e.value.id } ?: "" return response .metadata .businessResultMetadata ?.businessFilters ?.joinToString(separator = "\n") { f -> "${f.id}${enumValues(f)}" } } 

In the resulting line for the boolean filters, only the identifier will be shown, and for the enum filters, the identifier of the filter itself and the identifiers of the available values. Now, armed with knowledge of the available identifiers, we will look for the very cafes of Italian cuisine that have Wi-Fi. First, add a boolean filter:


 val boolFilter = BusinessFilter( /* id= */ "wi_fi", /* name= */ "", /* disabled= */ false, /* values= */ BusinessFilter.Values.fromBooleans( listOf(BusinessFilter.BooleanValue(true, true)) ) ) 

Now the enum filter:


 val enumFilter = BusinessFilter( /* id= */ "type_cuisine", /* name= */ "", /* disabled= */ false, /* values= */ BusinessFilter.Values.fromEnums( listOf(BusinessFilter.EnumValue( Feature.FeatureEnumValue( /* id= */ "italian_cuisine", /* name= */ "", /* imageUrlTemplate= */ "" ), true, true )) ) ) 

And finally, you can add filters to the session and call resubmit() :


 searchSession!!.setFilters(listOf(boolFilter, enumFilter)) searchSession!!.resubmit(this) 

Please note that you cannot set filters for the first query. First you need to get a search response that lists the available filters. And only then create a re-request.


Additional results

Another session allows you to check if there are additional search results for your search. And, if they are, get them. For example, when you search for a cafe in your city, most likely, all of them will not fit on one page of the search answer. A couple of methods hasNextPage and fetchNextPage needed to view the next pages of the list. Here you need to know that first, calling fetchNextPage will throw an exception if the hasNextPage method returns false . And secondly, the use of these methods implies that the remaining parameters do not change. That is, the session is used either to refine the request ( resubmit() ) or to get the next pages ( fetchNextPage() ). Combine these modes is not necessary.


Reverse lookup


Reverse search for convenience is also called submit :


 Session submit( Point point, Integer zoom, SearchOptions searchOptions, SearchListener searchListener ) 

It differs from other types of queries in that it requires only one type of search to enter. Either you pass the GEO type and look for place names, or the BIZ type and look for organizations. There is no third.


In the reverse search with the GEO type, there are moments that require explanation. Note that the answer will contain several objects in the hierarchy (that is, the answer will be a house, a street, a city, and so on). In simple cases, you can just take the first object. In more complex, search for the desired hierarchy.


The zoom level ( zoom ) is needed to produce adequate results, depending on what the user sees on the map. Imagine a user looking at a map across a country. Then it will be strange for him to click on to show a separate street or house if the user accidentally managed to get into them. Quite enough cities. This is what the zoom option is for.


 val point = Point(55.734, 37.588) //         «  , 16» searchSession = searchManager!!.submit(point, 16, SearchOptions(), this) //    –  " " searchSession = searchManager!!.submit(point, 14, SearchOptions(), this) 

Search by URI


Everything is clear enough here - we take the URI from the URIObjectMetadata , remember, after a while we come to the search and using this URI we get exactly the object that we have memorized.


 searchSession = searchManager!!.resolveURI(uri, SearchOptions(), this) 

Somehow even boring.


Search layer and bright future


Next to SearchManager there is still a thing called the search layer . The layer was conceived for combining a search with a map. He himself is able to add results to it, move the map so that these results show and make re-queries when the user moves the map. In many ways, it is similar to the combined SearchManager and Session , but the built-in work with the map adds new features. And talking about them is beyond the scope of this article. At the time of the release of MapKit 3.1, we already had time to roll in the search layer in real-world applications, so you can try using it at home. Perhaps it will make your search work easier.



Conclusion


I hope that after reading the article, you will have an understanding of how to work with MapKit search in full force. Surely there are still some subtle and non-trivial moments (for example, we almost did not talk about the tips and the search layer). Something can be found in the documentation, something to clarify in projects on GitHub or ask our support.


Try MapKit, use search in it and come to Maps to make them even better!


PS And also come to visit us on November 29 to listen about how the backend of automotive routing works . Which, by the way, can also be used in MapKit , but that's another story.


')

Source: https://habr.com/ru/post/428564/


All Articles