# 四.topojson

前言

TopoJSON 和 GeoJSON 一样,本身只是一种文件格式、一种地理数据标准。而为了对 TopoJSON 数据做操作,需要用到一个叫 topojson 的 js 库。

相对于 GeoJSON 有很大优势:消除了大量冗余(比如边界地理数据重复等),使得描述同一地理的 TopoJSON 文件比 GeoJSON 文件体积小了很多;就绘制而言,使用 TopoJSON 可以做到仅绘制外围轮廓、共享边境不重复绘制、查找相邻地区等

要注意的是地图绘制都是基于 GeoJSON 的,TopoJSON 并不能直接用于绘图,它总是要转换成 GeoJSON 格式才能被用于绘图。

# 1.topojson.topology

将 GeoJSON 格式的数据对象转换成 TopoJSON 格式的对象。

第一个参数是包含 GeoJSON 数据的对象,第二个可选参数是指转换过程中处理坐标时的精度。

const topology = topojson.topology({ geo: geoJSON }, 1e6)
1

要注意的是第一个参数并不能直接传 GeoJSON 数据作为参数,而是需要这种 key-value 的形式,这个 key 会被用于在返回的 topology 对象中进行索引,具体使用看后面。

一个返回的 topology 对象(TopoJSON 对象)通常包含以下几个属性:

  • arcs
  • bbox
  • objects - 几何对象集合
  • transform
  • type: “Topology”

# 2.topojson.feature

将 TopoJSON 对象中的几何对象转换成 GeoJSON,返回的是 Feature 或 FeatureCollection 几何对象。

第一个参数是要转换的 TopoJSON 对象,第二个参数是该对象内具体的某个几何对象。

const geo_data = topojson.feature(topology, topology.objects.geo)
1

返回的这个 geo_data 就能当成普通的 GeoJSON 数据来使用。

查看代码详情
<template>
  <div ref="map" class="map"></div>
</template>

<script>
export default {
  mounted() {
    let {
      Map,
      View,
      layer: { Tile: TileLayer, Layer },
      source: { State: SourceState, Stamen },
      extent: { getCenter, getWidth },
      proj: { fromLonLat, toLonLat },
    } = ol
    class CanvasLayer extends Layer {
      constructor(options) {
        super(options)
        this.features = options.features
        this.svg = d3
          .select(document.createElement("div"))
          .append("svg")
          .style("position", "absolute")
        this.svg.append("path").datum(this.features).attr("class", "boundary")
      }
      getSourceState() {
        return SourceState.READY
      }
      render(frameState) {
        const width = frameState.size[0]
        const height = frameState.size[1]
        const projection = frameState.viewState.projection
        const d3Projection = d3.geoMercator().scale(1).translate([0, 0])
        let d3Path = d3.geoPath().projection(d3Projection)

        const pixelBounds = d3Path.bounds(this.features)
        const pixelBoundsWidth = pixelBounds[1][0] - pixelBounds[0][0]
        const pixelBoundsHeight = pixelBounds[1][1] - pixelBounds[0][1]

        const geoBounds = d3.geoBounds(this.features)
        const geoBoundsLeftBottom = fromLonLat(geoBounds[0], projection)
        const geoBoundsRightTop = fromLonLat(geoBounds[1], projection)
        let geoBoundsWidth = geoBoundsRightTop[0] - geoBoundsLeftBottom[0]
        if (geoBoundsWidth < 0) {
          geoBoundsWidth += getWidth(projection.getExtent())
        }
        const geoBoundsHeight = geoBoundsRightTop[1] - geoBoundsLeftBottom[1]

        const widthResolution = geoBoundsWidth / pixelBoundsWidth
        const heightResolution = geoBoundsHeight / pixelBoundsHeight
        const r = Math.max(widthResolution, heightResolution)
        const scale = r / frameState.viewState.resolution

        const center = toLonLat(getCenter(frameState.extent), projection)
        const angle = (-frameState.viewState.rotation * 180) / Math.PI

        d3Projection
          .scale(scale)
          .center(center)
          .translate([width / 2, height / 2])
          .angle(angle)

        d3Path = d3Path.projection(d3Projection)
        d3Path(this.features)

        this.svg.attr("width", width)
        this.svg.attr("height", height)

        this.svg.select("path").attr("d", d3Path)

        return this.svg.node()
      }
    }
    const map = new Map({
      layers: [
        new TileLayer({
          source: new Stamen({
            layer: "watercolor",
          }),
        }),
      ],
      target: this.$refs.map,
      view: new View({
        center: fromLonLat([-97, 38]),
        zoom: 4,
      }),
    })
    d3.json(this.$withBase("/data/topojson/us.json")).then(function (us) {
      const layer = new CanvasLayer({
        features: topojson.feature(us, us.objects.counties),
      })
      map.addLayer(layer)
    })
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

# 3.topojson.merge

简单点说就是获得 objects 包含的几何体组成的整体的外轮廓。返回的是一个 GeoJSON 的 MultiPolygon 几何对象。

const geo_outline = topojson.merge(topology, topology.objects.geo.geometries)
1

# 4.topojson.mesh

获得 objects 构成的整体的完整轮廓线,包括外轮廓和内部共享边界线,但是是一个整体的线,所以没有线条重复的情况。返回的是一个 GeoJSON 的 MultiLineString 对象。

如果不传 objects 参数,那就是处理整个 topology。

可选的 filter 参数是一个函数,它又接收两个参数。这个 filter 参数通常用来过滤不共享的外轮廓或者被共享的内部边界线。

const geo_interior = topojson.mesh(
  topology,
  topology.objects.geo,
  (a, b) => a !== b
)
1
2
3
4
5

a !== b 会过滤掉外轮廓;a === b 则会过滤掉内部被共享的线。

# 5.topojson.neighbors

会返回一个数组,数组中的子数组对应着 objects 中相应索引的几何对象的相邻几何对象,但是只是包含相邻对象的索引。

const neighbors = topojson.neighbors(topology.objects.geo.geometries)
1