票牛如何使用 MapboxGL 实现在线选座

1 年前

为了让用户可以更方便的买到自己理想中的演出票,我们最近推出了在线选座功能。从首页列表进入,有选座标签的演出即可进行在线选座。这是一项颇具挑战的工作,过程中需要解决不少以前没有遇到过问题,但也因此而变得有趣。

背景

传统的选座流程一般是这样的:应用给到一张静态图片,在图片上以svg或者area标签的方式标注可选区域,用户点击对应区域之后切换到选票界面,再加载区域中的所有票。

这样的方案足够简单直接,但在信息的充分性上是缺失的,很多时候除了区域,用户还想要关注到更确切的“排”或“座”的信息,甚至希望能直接的了解到所在位置的视野是什么样的。而仅靠静态图很难承载这些信息,或是前端工程上会变得过于复杂。

于是我们考虑,是否可以把场馆图当作地图来考虑,由地图引擎来驱动数据呈现、用户交互,从而让我们的选座体验可以接近 Google Map、Mapbox 等这些一流应用,过程无缝顺滑方便。

GeoJSON

在进一步之前,先来认识一下GeoJSON,官方 Spec 对其有如下介绍 via

GeoJSON is a format for encoding a variety of geographic data structures. A GeoJSON object may represent a geometry, a feature, or a collection of features. GeoJSON supports the following geometry types: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, and GeometryCollection. Features in GeoJSON contain a geometry object and additional properties, and a feature collection represents a list of features.

GeoJSON 将地图上的对象以一个 geometry 及一组 properties 来描述,geometry 可以是点、线、面或他们的集合,properties 则可用于描述任何相关信息。在我们的场景下使用这种数据格式再合适不过。

以座位图为例,每个区或排有自己的几何形状,由一个或多个多边形组成(Polygon/MultiPolygon)。同时每个区有自己的id、名字、票档等信息;而对于地图上的标签,则可以将其表示为 Point,以 text 属性描述其文字。

GeoJSON 的数据并没有设计师熟悉的工具进行编辑,所以我们结合自身需要开发了一个后台工具,设计师可以使用最熟悉的工具(Illustrator或者Sketch)制作 SVG,由于目前GeoJSON并不支持圆、椭圆、贝塞尔曲线等矢量数据结构,故上传后我们的工具会以一定的采样算法将其转化成多边形从而用GeoJSON描述。之后运营同学可以选择特定区域将其按排数等分,生成最终的数据并存储下来。

Mapbox GL

Mapbox 是一家专注提供地图服务及开发套件的科技公司,Github、FourSquare、Airbnb 等知名公司都在使用他们的产品,同时他们将其大部分项目开源出来,以社区的力量来推进行业的发展。Mapbox 提供了两套JSSDK,其中 Mapbox.js 基于开源地图引擎 Leaflet 构建。

一开始我们也使用 Leaflet 来实现了地图的渲染和展示。不过和其他基于 Dom 来构建的地图引擎一样,Leaflet 只支持整数级别的缩放,这令使用过程变得有些跳跃,不是很理想。另外其对 GeoJSON 展现的处理方式是将其绘制成 SVG,而不支持文字的展现,于是只能曲线救国,自己创建 Marker 改变其元素中的文本来达到目的。

好消息是,2014 年的时候,Mapbox 推出了 Mapbox GL(ref: https://www.mapbox.com/blog/mapbox-gl/ ),底层基于 WebGL,在加载了矢量瓦片(Vector Tile)数据之后,渲染工作都由客户端完成,这使得层级缩放得以变得无缝且顺畅,同时文字的渲染和展现也变得很自然,可以点去官方博客感受一下。

于是最近我们将座位图切换到了 Mapbox GL 上。由于我们的座位图数据的坐标是 0,0 -> 1000,1000 的坐标系,原先使用 Leaftlet 的时候可以通过自定义坐标系,使用L.CRS.Simple来承载。而现在并没有同样方便的方式来做类似的事情,只能使用地理位置坐标。这里用了一个比较 tricky 的办法,从实际地图上截取了一片正方形区域,记录四个角上的坐标点,在使用前对原来的坐标做一次映射就可以了。

数据驱动样式 (Data-Driven Styling)

在 Mapbox GL 上定义样式十分轻松简单。类似 CSS 的语法非常容易上手,对于比较复杂的场景,还可以使用 data-driven styling 根据数据来决定样式。比如文字图层的描述如下。

map.addLayer({  
  "id": "texts",
  "type": "symbol",
  "source": {
    "type": "geojson",
    "data": this.textsData
  },
  "paint": {
    "text-halo-color": "#ffffff",
    "text-halo-width": 0.5
  },
  "layout": {
    "text-size": {
        "stops": [ // 根据缩放层级来设置文字尺寸
            [16, 8],
            [20, 22]
        ]
    },
    "text-field": "{text}", // 根据 properties 中的 text 字段来渲染
    "text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"],
  }
})

有票区域的圆点则可以这样描述:

map.addLayer({  
  "id": "sectionMarkers",
  "type": "circle",
  "maxzoom": ZOOM_LEVEL_TO_SHOW_ROWS, // 大于该层级才显示
  "source": {
    "type": "geojson",
    "data": this.sectionMarkers
  },
  "paint": {
    "circle-stroke-width": 1,
    "circle-color": { // 根据 properties 中的 selected 属性来渲染不用的颜色
      property: "selected",
      type: "categorical",
      stops: [
        [0, "#F8E81C"],
        [1, '#F6A623']
      ]
    }
  }
});

这样一来数据和样式被很好的区分开来,项目也更好维护了。更多细节可以参考 Mapbox Style Specification

下一步

得益于开源社区的繁荣,我们可以在保证高质量的前提下,把更多精力专注在打磨自身的产品功能上。

在目前的基础之上,将来让用户可以更方便的切换视角查看座位、发现更具性价比的位置也不是什么难事。而针对不同的场馆类型,也将会有更多的定制来让每一次选择的过程更加舒心。

最后,我们正在招聘前端工程师,如果你对我们正在做的事情或解决的问题感兴趣,欢迎投递简历到 mobile@ipiaoniu.com,期待相见 :)

0
推荐阅读