我要说的第一件事是很难。 比我想的要难得多。 在将产品发布到工作中之前,我曾经历过非常艰难的经历,但是我从未接触过个人项目。 他们都以不同程度的厌恶结束了原型,但是这一原型似乎可以生存。 目前,它已在两个移动平台上面向80多个国家(整个欧洲,亚洲和北美)启动,并且在文章结尾处将提供下载链接-因此,我邀请有兴趣的每个人尝试,尝试和批评。

这是一个简短的想法,它是从头开始的:
我认为,在现有的移动地图上进行搜索仅适用于行人,而根本不适用于驾驶员。 您需要停下来,深入研究遍布过多信息和广告的地图,戳一下小图标。 这很不方便,不会帮助您到一个陌生的地方,最后很危险。 需要一个直观,干净的解决方案,该解决方案不会分散您的注意力,也不会使您减速。
在
第一部分中,我描述了从这种简单的想法到可行的解决方案的路径,然后我将描述如何将该解决方案拖到发行版中。
为了节省您的时间,我将简要介绍上一部分:在那儿我写了一篇文章,而不是进行搜索,而是决定使用动态扫描,并且尽可能简化了应用程序界面。 他没有为驾驶员设置荒谬的入口线,而是添加了几个大按钮,方便您在路上使用:加油站,收费,ATM,停车,药房。 我没有列出地图,而是列出了列表,选择结果后,将打开通过Apple / Google Maps的导航。 对于该应用程序,我决定使用Flutter(同时我知道它是哪种动物),我从OpenStreetMap中获取了数据。 我的故事是关于一个或多或少理智的原型已经准备就绪的事实而结束的。
然后花了大约4到5个月,然后生活发生了变化,该项目被搁置了-我开始对此感到厌倦。 一个月后,我将其除尘,在集线器上写了一篇文章,刷新了头脑,然后决定:让我们完成它。 任何知道原型和产品之间差异的人都会在这个地方伤心地微笑。
接下来发生的事情又花了四个月的时间。 我得到了一个任务跟踪器,从总体上讲,它形成了问题列表,并收集了用于测试的设备。 在晚上和周末,我坐下来工作,将项目向前推进。 似乎在什么时候列表在增加,而我却被无休止的细微差别和画龙点睛淹没了。 他紧紧握住自己,扔掉了一些东西,恰恰相反,这推动了完美主义的发展。 接下来,我将尝试讨论在这个看似简单的项目的开发中最有趣的一点。
技术领域
通用架构
在工作的某个地方,开始出现一种感觉,即该体系结构正在不受控制地扩散。 太多的组件和连接已结束;已经发现了几个瓶颈。 幸运的是,该项目很小,而且我感觉很及时,因此整理事情并不困难。 在单个组件的级别上,这归结为重构并丢弃了不必要的库;在全局级别上,我将该功能扩展到了我在
DigitalOcean中启动的3台小型服务器。
- API服务器 (Python)-主服务器层,我们转向它。 没有很多逻辑,主要是输出结果的形成。 在资源方面最经济。
- 弹性服务器 (Java)-Elasticsearch和Photon(开源地理编码器)正在其上旋转。 他们使用从OpenStreetMap导入整个星球的相同索引。 服务器功能:通过垃圾填埋场和地理编码器搜索地点。 从本质上讲,弹性非常快且轻巧,因此服务器也不是很油腻。
- 地理服务器 (节点)是最难的。 我基于开放源代码路由机器,编写了一个小型api,其任务包括所有地理计算:铺设路线,计算等时线和生成图块。 每个单独的操作都不是很灵活,但是任何搜索都需要数十个操作,这成为了瓶颈。 目前,在此服务器上有16 GB的RAM,通常,所有操作都在瞬间完成-除了生成图块。 当队列中有很多照片时,您可以在几秒钟内等待带有卡片的照片。 幸运的是,它们以异步方式出现在客户端上,并且不会严重破坏整体情况(我希望如此)。
此外,我决定按国家/地区分别提供来自OpenStreetMap的摘录,以进行地理计算。 它的工作方式是这样的:我们向地理编码器发出第一个带有坐标的请求,它确定了国家/地区,然后仅提取该国家/地区的下载文件来进行所需的操作。 这是必要的,因为即使我的功能相当强大的服务器也无法转换大小超过2 GB的提取文件-该过程很快会耗尽所有内存和阻塞。 幸运的是,除了美国以外,几乎所有国家都符合这一限制:必须将这种怪物分为各个州。 最后,为了维护一百五十个摘录,我决定编写一堆脚本来检查其运行状况,修复和更新。

总的来说,所有这些听起来比实际中要复杂。 原则上,我对最终的解决方案感到满意-它具有很好的保留空间,可以按支持的区域数量和负载水平进行缩放。
动态等时
长期以来,对我而言,关键问题之一是结果的异质性。 原因是完全可以理解的-这是道路网络本身及其上建筑物的异质密度。 在半径5分钟内的中型城市中,可以有2-3个ATM,现在我们将移至大都市的中心-同样,在5分钟内可以有20或30个结果。 最后,我们跳入乡村,观察到几乎可以肯定的0个结果,直到我们靠近城市并且搜索半径捕获了一些东西。
这个问题给服务器带来了非线性且不可预测的负载,最重要的是-给用户带来了糟糕的体验。 在选项(5分钟,10分钟,30分钟)中添加过滤器基本上无法解决任何问题。 在村子里,即使半径30分钟也不会返回任何东西,但是在特大城市中,即使5分钟也会使您不知所措。 另外,我们在驱动程序应随身携带的按钮中添加了额外的功能。 通常,胡说八道,您需要一个根本不同的解决方案。
找到解决方案后,结果非常简单。 不必从相反的方向提示用户选择搜索半径,我们可以使该半径自动生成。 逻辑实际上是基本的:
- 您对结果设置了限制(例如,至少1个且不超过20个),并从10分钟开始
- 按地点搜索。 到目前为止,我们不需要获取指向它们的方向,因此我们只需要计算等时线并按弹性中的多边形进行过滤-两种操作都非常便宜
- 如果结果数从极限(在本例中为0或20+)向一个方向爬出,则将时间除以2或乘以2,然后再次进行搜索。 如果它已包含在限制中,那么我们已经建立了路由,按时间排序等。
实际上,这有点复杂,并且有一些细微差别,例如,超密集的城市网格,当我们已经将时间减少到最小时,仍然有太多结果。 在这里已经有必要进行分类,因此要铺设路线,这有点贵。 但是,这些都是极端情况,并不十分引人注目。
实际上,一个人不太可能将列表滚动到5-6个位置以下,因此在95%的场景中,动态等时线解决了该问题。 我们消除了瓶颈(无法预测的结果数量),并使任何请求的地理服务器负载几乎保持不变。 签出非常容易:
旧方法:以10分钟为半径,获得30个结果
结果:1个等时同步请求+ 30个路由请求= 31
新方法:检查,有30个结果很多,将半径除以一半,现在我们得到10个结果
结果:2请求等时线+ 10请求路由= 12
新地图逻辑
在最后一部分中,我描述了生成带有铺设路线的卡片的机制。 事实证明,它在计算方面非常复杂且昂贵,但是我非常喜欢它们,因此决定离开它们。 同时,我了解到,按照目前的形式,它们几乎没有实际的好处-从他们那里还不清楚您要走的路,而他们全都向北了。 有必要改进。
我决定要做的第一件事是使用指南针实时部署地图。 在颤振中,这是用微观逻辑描述的,并且运行非常迅速,但是,随着10多个结果不断旋转,性能开始下降。 此外,它看上去绝对令人作呕:实际上,静态图片在旋转,并且在行驶过程中比以某种方式提供帮助更令人困惑。
下一个想法是在地图上指示箭头的移动方向。 这非常简单-我已经计算出向量,而我要做的就是生成箭头的几何形状。 同时,在静止位置,卡片继续用圆形标记显示驾驶员的位置。 需要注意的是-必须针对不同的缩放级别标准化标记和箭头的大小。 这似乎是一个简单的任务,但是我坚持了很长时间。 事情是这样的:我以米为单位生成了地图上的所有符号,并以米为单位绘制了整个地图的高度的一部分。 事实证明,在创建地图(确定方形边界框,在其上粘贴和修剪图块等)的过程中,累积了错误,这些小错误最终导致标记的大小完全不同。 小规模卡的情况尤其令人难堪。 我不会详细介绍该解决方案,但是由于这些错误,必须完全重绘卡生成的逻辑。
Turf在此方面提供了很多帮助-一套用于处理地理数据的出色工具。
使用箭头卡已经更加有用,但是仍然缺少一些东西。 经过现场测试后,很明显所有卡都朝北。 在静电学中,这并不引人注目,但当您驶入方向盘时,它立即变得显而易见。 驾驶员下意识地期望在驾驶时箭头始终朝上。 发现这一点后,我再次坐下来工作。 这又是看起来很简单的任务之一,但是您将花几天的时间。 看起来-计算方位角,然后在栅格化之前打开最终的GeoJSON。 但还是有一个细微差别-最终的GeoJSON是由直接边界框生成的,并且在其上旋转并裁剪后,它检测到空的位置。

在上图中,我大致给出了解决方案。 结果,就资源而言并覆盖99%的场景而言,成本并不是很高(我认为错误会爬到两极附近)。 通常,地理计算服务器仍然是项目中资源最密集的部分,但是现在,除了美观之外,其路线卡也非常实用。 我什至试图专门使用这些卡到达该地点,不包括导航。 甚至到了。

资料品质
我从OpenStreetMap以不同的方式获取了所有数据。 如您所知,此资源是100%非盈利的,并且得到了集体的支持。 这是一个加号(它是免费的并且结构清晰),它也是一个减号-数据非常异构。
从高层次上讲,这意味着整个地球的覆盖范围参差不齐:在拥有大量发烧友的国家和城市中,描述了每片草坪和小路,而在其他地方,几乎是带有基本粗略物体的空白区域。 数据以完全相同的不均匀性进行更新。 在测试我的应用程序时,我遇到了几次新的加油站,咖啡馆,有时还没有设法在地图上绘制整条道路。 对此抱怨是愚蠢的:同一个Google花费了天文预算,并拥有负责数据相关性的整车队。 因此,在这里,我们能做的最好的事情就是更频繁地与OpenStreetMap提取同步。 好吧,祝他们社区好运。
但是在较低的层次上,由于对地图的混乱编辑,还有许多其他问题可以完全解决。 这主要涉及垃圾数据和重复项。 各种各样的混乱令人震惊:同一位置可以用不同的方式描述3次,机构没有错误放置名称,类型和标签,等等。 所有这些都没有统一的解决方案;相反,需要采取复杂的措施来使内容系统化。 例如,我具有以下条件:
同一标签有多个同义词和变体->我们描述别名词典(例如,parking,parking_space,parking_entrance等)。
有几个具有相同类型和相同坐标的地方:
- 如果每个人都没有名字->地点类型即为名字
- 只有一个名字->接受
- 每个人都有一个名字,他们各不相同->按时间顺序姓氏
有几个具有相同类型和几乎相同坐标的地方:
- 如果每个人都没有名字->最有可能重复,我们不会复杂化。 合并到具有平均坐标的一个点,在该点中,地点的类型即为名称。 一个男人会来了解
- 只有一个人有一个名字->同一个东西,只有现在我们已经有一个名字
- 每个人都有一个名字,他们是不同的->但这是一个集群
在我们的案例中,集群是一张标头,其中描述了几个位置。 最常见的是附近的商店或加油站群。 例如,一个自动取款机位于银行大楼内,另一个位于银行大楼外。 对于我们来说,它们并不代表任何困难:我们计算平均坐标并绘制到达它们的路线。 在界面中,我们清晰简洁地显示了这一点:

接口与设计
因此我想到,在开发开始之前,我通常已经想象了最终的情况。 同时,我不喜欢绘图方案和概念,而是喜欢与功能并行地形成设计(当然,如果这是我的项目)。 一方面,这种迭代方法非常酷,因为它允许您在视觉效果和代码之间进行切换-有时,您必须过于频繁地重做所有事情。 这里发生了同样的事情:我似乎已经铲掉了最简单的界面一百次。 从图标和缩进之类的琐事开始,以卡片,菜单等的组成结尾。 我不会描述所有内容,我将快速解决关键问题。 如果您对设计不感兴趣,请随时跳过它。
色彩调色板
很长时间以来,我不知道该如何处理调色板。 我真的想用不同的颜色指定地点的类别,绿色除外-我决定将其保存为重音。 我选择了易于区分和丰富的色彩,一切似乎都很好。 一段时间后,我发现加油站的蓝色与蓝色相呼应,在地图上指示驾驶员的位置。 他对此没有做任何事情,而是保留了它-但内部的完美主义者不满意。

“在路上”和“您就在附近”
在确定驾驶员行进方向的逻辑出现之后,就可以将路线分为“沿途”和其余。 正如我所说,这取决于到该地点的路线的第一部分:它与驾驶员路线的最后部分是否重合。 如果是这样,那么我们已经到了这个地方。 然后出现了如何在界面中显示此问题。 除了我上面描述的地图更改之外,还提出了“途中”标牌(或英语中的“途中” –似乎含义相同)的想法。 我将同一个模具用于另一种情况:当到找到的位置的距离小于25米时。 然后,获取方向毫无意义,我隐藏了地图,并写道您已经在附近(“您在附近” /“环顾四周”)。

社区卡
在调试开发的最开始,我使用了Google提供的静态地图来查看等时线和结果。 然后他和她一起跑了很长时间,不知道要贴在哪里:地图似乎很有趣,但是似乎不应该占据一个地方。 另外,痛苦的是,甚至不想在这样的琐事上依赖谷歌。 因此,最后,我删除了地图,但是一段时间后,我开始生成路线卡,并意识到自己在技术上已经“成长”到了大地图上。 事实证明,做到这一点并不困难,尽管到目前为止,通用地图仍然是整个项目中资源最密集的部分。 为了不占用接口空间,我将卡放在单独的页面上(它们会减少拉动的频率)。

本地化
为了在生产中正常输出,必须进行本地化。 一方面,它总是非常直接和简单的工作;另一方面,当您开始这样做时,成群的蟑螂从各处爬出来。 就我而言,OSM的主要内容已经本地化,因此仅保留了地点类型和界面元素。 除了几个插头(很长一段时间我无法沿着“沿途”制定模具)外,一切都很容易。 值得注意的是,地名可以同时占据2行和3行,并且可能不
适合小宽度的屏幕-因此
auto_size_text小部件在这里
有所帮助,我建议在合理的范围内使用它。

但是从技术角度来看,它并不是那么顺利。 —
Intl_translation , … . , . , (!) - , … , , .
, , — - . , , .
, , . , -, — . , . , . .
- support androidx: - , , . , — . : . , , .
, , :


. - — . , №2:
Android Auto Apple CarPlay. , .
, .