MapKit是一个软件库,可让您在移动应用程序中使用地图数据和Yandex技术。 她的官方文档已经包含API方法的详细说明,因此今天我们将讨论其他内容。
在本文中,我将向Habr读者介绍MapKit中的搜索功能,并分享可能对您有用的建议和技巧。
TL; DR如果您不想阅读整篇文章,这里有两个最有用的要点,可以作为阅读前言的补偿:
- 不要忘记保存会话,否则搜索将无法进行。
- 所有最有趣的信息都存储在对象的元数据中。 如果要查找完整的地址,营业时间或在特定咖啡馆中一杯卡布奇诺咖啡的价格,则需要元数据。
文本中文档的链接适用于Android,适用于iOS的类和方法以相同的方式调用。
什么可以搜寻
首先,让我们谈谈MapKit中的搜索功能。 当您想在地图应用程序中查找内容时,搜索可以满足您的期望。
当您在搜索栏中输入“ cafe”,“ Lev Tolstoy street,16”或“ tram 3”时, 文本搜索将起作用。 这是最复杂的搜索类型。 从某种意义上说,它支持最大的自定义参数集。 您可以立即尝试沿着您感兴趣的路线或街道进行搜索,明确所需的结果数量,设置用户的位置,等等。 如果在第一次搜索后您想要移动地图或将过滤器应用于请求(“有游泳池的药房”),则这些是重新请求 。
大多数用户对“这里是什么?”这个问题很熟悉。 它允许您单击地图以确定哪个街道或房屋在“光标之下”或哪个组织在该点附近。 当您要查找特定对象时,需要按URI搜索 。 例如,它可用于在应用程序中创建书签。 我们找到了我们最喜欢的咖啡店,并用星号标记-下次无论地图窗口位于何处,都可以通过URI准确找到该组织。 反向查询和URI查询都不支持重新请求,因为没有为它们指定的内容。
搜索中存在的另一种可能性是搜索提示 ,它使您可以在键入查询时自动补充查询。 但是,我们将再次推迟有关它们的详细故事。
请求的安排方式
搜索与MapKit的许多部分一样,都是异步进行的。 搜索会话是处理此异步问题的主要对象。 让我们看一个小例子。
关于例子本文中的示例将在Kotlin上进行,以使使用可选值和减少样板代码的工作变得更加容易。 MapKit有一个演示应用程序 。 它可以用来测试示例,但是为此, SearchActivity
应该从Java转换为Kotlin。 showMessage
有时会出现在代码中,它是您在屏幕或日志中显示一行文本的任何便捷方法。
调用Submit之后submit
控件将立即返回您的代码,并且MapKit收到来自服务器的响应时,将调用SearchListener。
搜索会话使您可以:
- 取消请求 。 例如,如果用户关闭搜索屏幕。
- 出现错误时重复请求 。 例如,如果网络有任何问题。
- 收到响应后,继续与搜索进行交互。 通过会话进行重新请求 。
取消会话将自动取消。 这意味着,如果未将其保存在客户端代码侧,则搜索将无法进行。
不要忘记保存会话,该会话是您的朋友!
搜索选项
配置搜索查询的一般方法是通过SearchOptions
类,该类允许您修改查询参数。
除了列出的参数外,还有更多可能对您有用的参数,可以在该类的文档中找到它们。 请注意,并非所有查询都支持所有参数组合。 每个SearchManager
或Session
方法的文档都指出它可以理解SearchOptions
哪些参数。
答案如何安排
从支持方面的问题来看,大多数用户对搜索响应的格式感到困惑。 如果您查看响应类,它看起来非常简单(至少,对我们来说有趣的部分):
public class Response { public synchronized SearchMetadata getMetadata(); public synchronized GeoObjectCollection getCollection();
在这里, getCollection()
返回响应中的对象,而getMetadata()
是一些附加数据 ,例如,包含有关响应窗口 , 排名类型和找到的结果数的信息 。 如果查看GeoObjectCollection
内部GeoObjectCollection
可以看到它包含一些Item
,这些Item
可以是其他GeoObjectCollection
或GeoObject
。
搜索中的集合内部没有集合(至少现在还没有),因此让我们看一下GeoObject
。 在对象内部有一个名称( getName()
),一个描述( getDescriptionText()
),一个框架( getBoundingBox()
),一组几何形状( getGeometry()
)以及其他一些不太清晰的方法。 该组织的电话号码在哪里? 如何理解地名指的是哪个城市?
通过对象的方法,这不是很清楚。
地物
现在该讨论有关GeoObject
更多信息。
GeoObject
是这样的基本“卡片”对象。 它的内部可能是道路事件,与搜索结果不同的对象,路线中的动作或地图(POI)上的对象,例如纪念碑或某些著名的组织。
关于对象的所有最有趣的内容都存储在元数据中。 可以使用getMetadataContainer()
方法访问它们。 此容器中的键是元数据类型。 如果您在文档中看到以Metadata
一词结尾的内容,则很可能在这里。 在搜索不同的“元数据”片段15。
元数据可以分为几种类型。 第一种类型是元数据,它确定对象所属的类型:地名( ToponymObjectMetadata
),组织( BusinessObjectMetadata
)或传输( TransitObjectMetadata
)。 在地名的元数据中,您可以找到结构化的地址和详细的几何图形。 组织的元数据是营业时间或公司所在地。 此元数据由请求中的搜索类型确定-如果仅搜索地名,则响应中的每个对象都应具有相应的元数据。 如果要查找地名或组织,则每个对象将至少具有两个“元数据”之一。
这是查找公司电话号码的方法:
val phones = response.collection.children.firstOrNull()?.obj ?.metadataContainer ?.getItem(BusinessObjectMetadata::class.java) ?.phones
以下是在结构化地址中查找城市的方法:
val city = response.collection.children.firstOrNull()?.obj ?.metadataContainer ?.getItem(ToponymObjectMetadata::class.java) ?.address ?.components ?.firstOrNull { it.kinds.contains(Address.Component.Kind.LOCALITY) } ?.name
第二种类型是对象附带的元数据,尽管您没有询问它。 您需要了解的主要类型是URIObjectMetadata
。 在URIObjectMetadata
内部URIObjectMetadata
存储了对象的唯一标识符,必须在URI中将其传递给搜索 。
第三种是元数据,仅当您明确要求对其进行搜索时,它才会出现在答案中。 以另一种方式,此元数据称为片段 。 片段是一些小的附加数据,它们的变化比基本的“参考”数据更频繁,或者不是每个人都需要。 这可以是等级,与照片或全景照片的链接,汇率或加油站的燃料价格。 摘要列表必须使用搜索选项进行设置。 如果服务器有一个有序的代码段,那么它将把它添加到相应的对象中。
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) {
上面列出的所有元数据都会添加到响应中的各个对象中。 也有元数据添加到整个响应中。 但是它们是用SearchMetadata
方法带出来的,不需要从任何特殊集合中提取它们。
使用范例
现在,让我们浏览一下搜索类的主要方法,查看使用示例以及与之相关的一些非显而易见的地方。
文字搜寻
文本搜索(可能是整个搜索)的主要方法是submit
:
Session submit( String text, Geometry geometry, SearchOptions searchOptions, SearchListener searchListener );
text
参数应包含您要搜索的文本。geometry
参数有点棘手。 根据传输的几何类型,搜索的行为将有所不同:
- 如果您通过了一个点,则搜索将在该点旁边的一个小窗口中进行。
- 如果传递矩形窗口(
BoundingBox
)或四个点的多边形 ,则它将用作搜索框。 这种窗口的一个简单示例是地图的可见区域。 - 最后,如果传递折线 ,则描述该折线的窗口将用作搜索窗口,并将考虑该折线进行排名。
- 我们已经在上面谈到了
SearchOptions
和SearchListener
。
服务器可能认为正确的答案不在执行初始搜索的窗口中(当搜索窗口位于莫斯科时,“符拉迪沃斯托克咖啡馆”)。 在这种情况下,您将需要进入响应窗口并将卡移动到那里,以便在屏幕上看到结果(重新请求不允许自己执行此操作,也不会要求移动卡)。
submit
方法具有一个submit
双胞胎:
Session submit( String text, Polyline polyline, Geometry geometry, SearchOptions searchOptions, SearchListener searchListener );
带有一个附加参数。 此参数可用于传输较大的折线(例如,到另一座城市的路线)和较小的搜索窗口。 然后,搜索本身将切出已转移折线的必要部分,并将其仅用于查询。
重新要求
与其他类型的请求不同,重新请求是在搜索会话的帮助下完成的,该搜索会话返回相同的submit
及其孪生兄弟。 会话的部分方法简单明了:
- 您可以更改请求窗口 (例如,当用户移动卡时)
- 搜索选项可以更新(例如,更新用户的位置)
- 您可以更改排名的类型-通过距离或等级 。
要执行精确搜索,您需要使用resubmit
方法。 它接受与常规搜索相同的SearchListener
。 在调用它之前,您可以更改几个会话参数。 例如,同时更改排名类型并应用过滤器。
筛选器
由于我们在谈论过滤器。 筛选条件适用于Wi-Fi和意大利美食。 它们可能具有MapKit中所有搜索界面中最令人困惑的语法。 这是由于以下事实:使用相同的数据结构从搜索响应中获取过滤器并在重新请求中指定过滤器。
过滤器有两种类型。 布尔过滤器仅假设两个互斥的值-是或否。 这可能是咖啡馆,加油站的洗手间或组织附近的停车场中存在Wi-Fi。 枚举过滤器假定可以一起请求许多值。 例如,这是咖啡馆的厨房类型或加油站的燃料类型。
首先,让我们看看如何使过滤器可用于当前重启:
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)}" } }
在结果行中,对于布尔过滤器,将仅显示标识符,对于枚举过滤器,将显示过滤器本身的标识符和可用值的标识符。 现在,凭借对可用标识符的了解,我们将寻找带有Wi-Fi的义大利风味咖啡馆。 首先添加一个布尔过滤器:
val boolFilter = BusinessFilter( "wi_fi", "", false, BusinessFilter.Values.fromBooleans( listOf(BusinessFilter.BooleanValue(true, true)) ) )
现在,枚举过滤器:
val enumFilter = BusinessFilter( "type_cuisine", "", false, BusinessFilter.Values.fromEnums( listOf(BusinessFilter.EnumValue( Feature.FeatureEnumValue( "italian_cuisine", "", "" ), true, true )) ) )
最后,您可以向会话添加过滤器并调用resubmit()
:
searchSession!!.setFilters(listOf(boolFilter, enumFilter)) searchSession!!.resubmit(this)
请注意,您无法为第一个查询设置过滤器。 首先,您需要获取列出可用过滤器的搜索响应。 然后才重新启动。
附加结果
另一个会话使您可以检查是否有其他搜索结果可用于您的请求。 并且,如果是,请获取它们。 例如,当您在城市中寻找咖啡馆时,很可能所有咖啡馆都不适合搜索答案的一页。 要查看列表中的以下页面,需要几个hasNextPage
和fetchNextPage
。 在这里,您需要知道,首先,如果hasNextPage
方法返回false
,则调用fetchNextPage
将引发异常。 其次,使用这些方法意味着其余参数不会更改。 也就是说,该会话用于完善请求( resubmit()
),或检索以下页面( fetchNextPage()
)。 不需要组合这些模式。
反向搜寻
为了方便而进行的反向搜索也称为submit
:
Session submit( Point point, Integer zoom, SearchOptions searchOptions, SearchListener searchListener )
它与其他类型的查询的不同之处在于,只需要输入一种搜索即可。 您可以传递GEO
类型并查找地名,或者传递BIZ
类型并查找组织。 没有第三名。
使用GEO
类型进行搜索时,有一些点需要澄清。 请注意,答案将在层次结构中包含多个对象(也就是说,答案将包括房屋,街道,城市等)。 在简单的情况下,您可以仅拍摄第一个对象。 在更复杂的数据库中,搜索所需的层次结构。
需要缩放级别才能产生足够的结果,具体取决于用户在地图上看到的内容。 假设有一个用户在全国范围内查看地图。 如果用户不小心进入了街道或房屋,那么他单击另一条街道或房屋将很奇怪。 足够多的城市。 这就是zoom
参数的用途。
val point = Point(55.734, 37.588)
依URI搜寻
这里的一切都非常清楚-我们从URIObjectMetadata
获取URI,并记住它,经过一段时间的搜索之后,通过此URI,我们可以准确地得到我们记住的对象。
searchSession = searchManager!!.resolveURI(uri, SearchOptions(), this)
不知何故甚至无聊。
搜索层与光明的未来
在SearchManager
旁边,还有一个叫做搜索层的东西。 设想该层将搜索与地图结合起来。 他本人知道如何向其中添加结果,移动地图,以便在用户移动地图时显示这些结果并进行重新查询。 在许多方面,它类似于SearchManager
和Session
的组合,但是内置的地图功能增加了新功能。 而谈论它们超出了本文的范围。 在发布MapKit 3.1时,我们已经在实际应用程序中的搜索层上运行了,因此您可以在家中尝试使用它。 也许它将使您的搜索工作更加轻松。
结论
我希望阅读本文后,您将全面了解如何在MapKit中使用搜索。 当然,仍然存在一些微妙和不平凡的时刻(例如,我们几乎没有谈论提示和搜索层)。 可以在文档中找到某些内容,可以在GitHub上的项目中找到一些内容,或者寻求我们的支持。
尝试使用MapKit,使用其中的搜索功能来使用MapKit,使它们变得更好!
PS And还将在11月29日来访我们,以听取汽车路由后端的安排 。 顺便说一下,它也可以在MapKit中使用 ,但这是一个完全不同的故事。