自定义数据格式的矢量地图实现

本文介绍了一个自定义数据格式的矢量地图实现,通过解析XML数据存储为VectorDrawable格式,支持路径和兴趣点显示。项目采用Factory设计模式,实现地图的移动、缩放、旋转以及兴趣点交互。目前遇到的技术难点包括边界检测、路径导航的实现和视觉优化。

Update

项目已重构至2.0,启用了新的数据结构和代码结构。基本弥补了初版的大部分功能缺失。还有部分功能未实现,比如指南针、区域的绘制方式等。
其实代码早就写的差不多了,只是最近在准备期末复习,加上智商下线,所以一直忘了更新博客╮(╯_╰)╭。具体的项目地址在此

2017/06/28

背景

面试失利后,最近在专攻工程设计项目,指望着能通过它咸鱼翻身去名企。项目中需要一种可以自由控制并显示自定义的矢量路径和兴趣点的地图模块,仔细查了查发现并没有现成的轮子可以用,便想着自己定义一种数据格式进行解析和渲染。但真正实践起来,才发现难度颇大,先在这里记录下自己的思路和方法。

预期功能

  • 实现矢量路径的显示,不同道路的绘制样式不同。
  • 可以显示建筑点,可以区分不同建筑或类别,建筑点可以被点击,并返回对应的消息
  • 可以在特定的路径和建筑点上显示标签代表其名称。
  • 可以使用手势进行操作,整体的显示效果类似于主流的几个地图App。
  • 在单机上实现地图导航的功能。

技术原理实现

看了不少源码和资料,比如android-pathviewsvg-android,里面的实现方式和项目构架思路都令我受益匪浅。在想了很久以后,根据本人项目的实际情况总结了一下我自己不成熟的想法。


数据结构

数据存储格式

  1. 最好是和原有的SVG的XML文档格式兼容,方便以后的解析和渲染,于是我选择了从SVG转换而来的VectorDrawable XML格式作为数据格式基础。

    这是一个VectorDrawable格式的例子。

    <vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="960dp"
        android:height="480dp"
        android:viewportWidth="960.0"
        android:viewportHeight="480.0">
    <path
        android:pathData="M570.1,440.2l-29.2,-29c-7.1,-8.5 -6.2,-36.7 -6,-40.7H425c0.1,3.9 1.1,32.2 -6,40.7l-29.2,29c-1.2,1.4 -1.7,2.5 -1.6,3.4h-0.1c0.8,7.7 6.7,6.3 13.6,6.3H558c6.7,0 12.4,1.3 13.5,-5.6 0.6,-1 0.3,-2.3 -1.4,-4.1z"
        android:fillAlpha="1"
        android:strokeColor="#B4BEC8"
        android:fillColor="#B4BEC8"
        android:strokeWidth="2"/>
    <path
        android:pathData="M727.5,355.1c0,8.5 -6.6,15.4 -14.7,15.4h-465.5c-8.1,0 -14.7,-6.9 -14.7,-15.4V45.4c0,-8.5 6.6,-15.4 14.7,-15.4h465.5c8.1,0 14.7,6.9 14.7,15.4v309.7z"
        android:fillAlpha="1"
        android:strokeColor="#C8D2DC"
        android:fillColor="#C8D2DC"
        android:strokeWidth="2"/>
    <path
        android:pathData="M489,343.7c-0,-4.2 3.4,-6.3 3.6,-6.4 -2,-2.9 -5,-3.3 -6.1,-3.3 -2.6,-0.3 -5.1,1.5 -6.4,1.5s-3.4,-1.5 -5.5,-1.4c-2.8,0 -5.4,1.6 -6.9,4.2 -2.9,5.1 -0.8,12.7 2.1,16.8 1.4,2 3.1,4.3 5.3,4.2 2.1,-0.1 2.9,-1.4 5.5,-1.4 2.6,0 3.3,1.4 5.5,1.3 2.3,-0 3.7,-2.1 5.1,-4.1 1.6,-2.3 2.3,-4.6 2.3,-4.7 -0.1,-0 -4.4,-1.7 -4.5,-6.8M484.8,331.3c1.2,-1.4 2,-3.4 1.7,-5.3 -1.7,0.1 -3.7,1.1 -4.9,2.5 -1.1,1.2 -2,3.2 -1.8,5.2 1.9,0.1 3.8,-0.9 4.9,-2.4"
        android:fillAlpha="1"
        android:strokeColor="#C8D2DC"
        android:fillColor="#fff"
        android:strokeWidth="2"/>
    <path
        android:pathData="M727.5,315.5V46c0,-8.8 -6.6,-16 -14.7,-16h-465.5c-8.1,0 -14.7,7.2 -14.7,16v269.5H727.5z"
        android:fillAlpha="1"
        android:strokeColor="#3C4650"
        android:fillColor="#3C4650"
        android:strokeWidth="2"/>
    <path
        android:pathData="M251.2,48.9h457.2v245.5H251.2z"
        android:fillAlpha="1"
        android:strokeColor="#141E28"
        android:fillColor="#141E28"
        android:strokeWidth="2"/>
    </vector>
    
  2. 因为能力不够(笑),舍弃了一些不必要的数据属性。

    • 比如Group标签就暂时没有进行处理,也没有解析一些地图上用的不多的绘制命令如C1、Q2、A3等(可能等以后会再添加上)。
    • 因为地图的路径和兴趣点应有特定的绘制样式,为了增强灵活性所以忽略了Path标签内的原始属性如android:fillAlphaandroid:strokeWidth等。
  3. 将原本的<path>标签修改为了<road>,并添加了一些自定义的Attribute。

    • id代表该路径或兴趣点的ID
    • roadType代表道路类型,比如人行道、单行道、禁止通行等。
  4. 添加全新的自定义标签“兴趣点”<interest>,其中定义了一些自定义属性

    • id 兴趣点的id
    • absoluteX X轴绝对位置
    • absoluteY Y轴绝对位置
    • hasNextLevel是否存在二级地图,即该点是否是一个二级地图的进入点
    • pointType指兴趣点的类型(卫生间、建筑、景点之类的)

    未来还可以添加封闭的地图路径,如湖泊,围栏等等。

    这是一个矢量地图数据格式的测试样例。(瞎写的,很多地方不够完善)

    <mapVector xmlns:map="http://schemas.android.com/apk/res/android"
           map:width="900px"
           map:height="1300px"
           map:viewportWidth="960.0"
           map:viewportHeight="480.0">
    <road
            map:id="jdj1231"
            map:pathData="M0,0h800v700l-300,50"
            map:roadType="default"
            />
    <road
            map:id="21gskkdh"
            map:pathData="M200,0v800l500,200"
            map:roadType="peopleOnly"
            />
    <road
            map:id="gm34340"
            map:pathData="M0,0l100,900h400v300"
            map:roadType="peopleOnly"
            />
    <road
            map:id="vv56657"
            map:pathData="M0,700l400,-200h-100v-300"
            map:roadType="prohibitAll"
            />
    <road
            map:id="mmbi39"
            map:pathData="M0,1200h900v-700h-300"
            map:roadType="oneWay"
            />
    <interest
            map:id="gmior56546"
            map:absoluteX="500"
            map:absoluteY="650"
            map:pointType="building"
            />
    <interest
            map:id="qqwrhhy666"
            map:absoluteX="300"
            map:absoluteY="200"
            map:pointType="building"
            />
    <interest
            map:id="joidjf13233"
            map:absoluteX="700"
            map:absoluteY="600"
            map:pointType="toilet"
            />
    <interest
            map:id="zxcml23342"
            map:absoluteX="500"
            map:absoluteY="100"
            map:pointType="entrance"
            />
    <interest
            map:id="oij5fvvs44"
            map:absoluteX="300"
            map:absoluteY="1200"
            map:pointType="exit"
            />
    </mapVector>
    

数据类

  • PointDef 点。抽象类,定义了矢量地图点的基本格式,如id,XY轴位置等。
  • InterestPointDef 兴趣点。 继承于PointDef,定义了兴趣点的类型和其他属性。
  • PathDef 矢量路径。抽象类,定义了矢量地图路径的描述方式,包括id、画笔Paint和被绘制的Path对象。
  • RoadDef 道路。继承于PathDef,增加了道路类型等。
  • MapSrcDef地图数据资源。包括兴趣点和道路的ArrayList、兴趣点对应兴趣点类型的图标table、兴趣点和路径的标签map、地图尺寸,ViewPort尺寸以及绘制单位等。

项目结构

为了矢量地图项目的可拓展性和灵活性,我采用了Factory的设计模式,通过向RecourseResolver实例中添加必要的资源数据由VectorMapFactory生产出对应的VectorMap实例,具体的数据解析细节则对App编程人员透明。地图显示模块VectorMapView则专注于响应用户的操作并对地图进行变换,而具体的渲染任务则交由VectorMap对象中对应的MapRender实例进行处理。通过这样的结构,可以在一个App运行时,展示多个矢量地图,并可以轻松的进行实时地图数据修改。包括进入二级地图时异步加载并显示、修改兴趣点或路径的tag、兴趣点标更换等。为了管理多个VectorMap实例,我还设计了一个VectorMapManager进行根据LRU规则进行管理(暂时还没用上,也没有写好)。

这是项目的具体结构。没有使用第三方库
项目结构

Java类

  • GestureParser 手势解析器。配合GestureDetector实现单指移动地图,双指旋转和缩放。
  • InterestPointDef 兴趣点类。包含兴趣点的定义。
  • MapRender 地图渲染器类。负责渲染地图数据。
  • MapSrcDef 地图资源类。包含道路列表、兴趣点列表、地图大小和地图度量单位等。
  • MathUtils 数学工具类。包含一些静态的数学函数,主要是几何学的,由于Java自带的Math类没有,只好自己实现了。
  • PaintType 画刷类型类。由于在绘制地图时大量数据都需要不同样式的Paint,所以使用该类统一管理笔刷的类型和与其他数据类型的映射关系。现在该类的主要功能是进行笔刷的映射,将道路类型的int变量映射成画刷的int类型。
  • ParseUtils 解析工具类。包括一些小的解析静态函数和解析静态类,负责执行一些数据解析的操作。
  • PathDef 路径抽象类。定义了路径的基本属性。
  • PointDef 点抽象类。定义了点的基本属性
  • RecourseResolver 资源解析类。包含各种XML的ContentHandler和数据读取函数。负责解析数据文件。
  • RoadPathDef 道路类。 定义了一些道路属性。
  • VectorMap 矢量地图类。包含矢量地图数据、地图渲染器、标签哈希Map、图标表、画刷表等。
  • VectorMapFactory 矢量地图工厂类。负责协调其他类完成数据解析以及最终的构建工作。
  • VectorMapManager 矢量地图管理类。负责管理多个矢量地图,包括清理和寻找地图。未来可能添加其他功能。
  • VectorMapView 矢量地图视图类。负责在App上显示地图,并接受用户的手势操作,控制地图的显示方式以及位置。

View属性

直接看代码定义吧。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="VectorMapView">
        <!--是否显示标签-->
        <attr name="showTags" format="boolean"/>
        <!--允许旋转-->
        <attr name="allowRotate" format="boolean"/>
        <!--允许缩放-->
        <attr name="allowScale" format="boolean"/>
        <!--允许移动-->
        <attr name="allowTranslate" format="boolean"/>
        <!--允许点击(兴趣点找到对应id)-->
        <attr name="allowClick" format="boolean"/>
        <!--允许视角突破地图边界-->
        <attr name="allowBreakBoundary" format="boolean"/>
        <!--地图填充类型-->
        <attr name="mapFillType" format="enum">
            <enum name="Default" value="0"/>
            <enum name="Minimum" value="1"/>
            <enum name="Maximum" value="2"/>
        </attr>
        <!--最大缩放绝对比例-->
        <attr name="maxScale" format="float"/>
        <!--最小缩放绝对比例-->
        <attr name="minScale" format="float"/>
        <!--一次放大的相对比例-->
        <attr name="scaleOneTap" format="float"/>
        <!--地图起始视点X轴位置-->
        <attr name="originalPositionX" format="float"/>
        <!--地图起始视点Y轴位置-->
        <attr name="originalPositionY" format="float"/>
        <!--地图起始视点旋转角度-->
        <attr name="originalRotate" format="float"/>
        <!--点击敏感度(点击位置的搜索范围大小)-->
        <attr name="clickSensitivity" format="float"/>
    </declare-styleable>
</resources>

项目成果

花了快半个月时间,基本实现了预期的功能,地图可以自由移动缩放和旋转(图标和标签方向大小不变),但会受到边界检测和最大最小缩放比例的显示,点击兴趣点会返回对应的id,通过id可以找到对应兴趣点的信息。总体来说效果差强人意,还是有很大改进的空间的。
效果展示

关于项目源码,由于工程设计项目暂时还没有完成,在实际磨合中应该还会有不少修改,所以我准备在项目完成后再开源到GayHub上。

技术难点

其实对我来说,一个问题,没解决就是难点。解决了,回头一看,其实也就那么回事儿(马后炮的感觉……),所以我就说说对这个项目我还未解决的问题。

  • 项目架构不成熟。这一直是我的心病,或者说是强迫症(笑)。总是会假设面对各种实际业务情况时项目的迭代速度和出现问题时修改的复杂度,想的多了总会有些不尽如人意的地方。这个可能需要我不断的动手来增加经验才能解决。

  • 暂时没法完美解决边界检测以及修正的功能。我自己已经实现了在view和地图角度一致的情况下的边界检测以及控制函数。但当地图发生旋转时,现在的检测机制并不能很好的检测出来,更不知道如何进行修正。看了看主流的几个地图App好像都不需要边界检测,所以也没啥好办法。╮(╯_╰)╭可能还得补补数学才行。

  • 路径导航暂时无法实现(还没想好)。可能还是数据结构的问题,现在路径与路径间没有联系,路径的交点也没有记录。最短路径的算法倒是简单粗暴4,所以还得考虑在移动端单机实现的效率问题。

  • 丑。(没有衔接动画、缺少一些画面修饰)

总结和感想

刚开始准备做的时候总感觉难得不得了,什么自定义view啦,XML解析啦,矩阵换算啦,手势控制啦,感觉根本做不出来。但是真正沉下心查资料一点点写,发现其实也就那样,写着写着也就实现出来了。
我觉得,难的不是技术,而是想法。毕竟在代码的世界里,我们就是上帝,就是马良,就是贝多芬,如果只是填充代码,那和咸鱼(码农)又有什么区别呢?


  1. 三次贝塞尔曲线
  2. 二次贝塞尔曲线
  3. 弧线
  4. BFS
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值