求经纬度两点距离

最近一直在做和地图相关的开发,所以一直在研究坐标系,投影相关的知识。整理下,供自己查阅。接下来就讲述下描述地球的几种坐标系。

# 世界大地测量系统

世界大地测量系统(World Geodetic System)就是我们常说的经纬度,维度指得是当前地点和赤道的夹角。所以赤道的纬度(Latitude)是 0,北极是正 90,南极是负 90. 经度是本初子午线(Prime meridian)为分界,东边是东经(正值),西边是西经(负值)。当经度为 180 度的时候,我们称之为对向子午线(antimeridian)。当跨过对向子午线的时候,很多相关计算都需要特殊处理,需要注意。这里计算两个坐标的距离的话,由于是球面,其实计算的是两个坐标沿着球面的弧线。本质思想是求出两个坐标的夹角,然后乘以地球半径得出结果。详细细节可以参考Calculate distance, bearing and more between Latitude/Longitude points (opens new window)

# 麦卡托投影

世界大地测量系统是三维描述地球的位置信息,对于导航或者显示来说很不方便。所以在平面上使用麦卡托投影(Mercator projection),可以把三维平铺到二维。但是注意,麦卡托投影会使面积发生变形。所以投影出来的坐标是不能直接用来计算距离的。维度越高,变形就越大。所以需要计算比例因子 (opens new window)来纠正。但对于两个坐标纬度相差比较大的情况下,计算就更复杂了 (opens new window)。所以不推荐使用麦卡托投影来计算距离。

# 笛卡尔坐标系

笛卡尔坐标系(Cartesian coordinate system)也称直角坐标系,是我们高中时学过的坐标系。我们可以通过经纬度和地球半径来计算出直接坐标系的 X,Y,Z。然后通过直角坐标系中求两点距离公式,计算出两点距离,不过这个距离是直线距离。如果两点距离很远的情况下,可以看作是我们穿过地球的直线距离。可以借助 Cesium 的Cartesian3 (opens new window)库方便的完成笛卡尔坐标系的转换和距离计算。

# MapKit 投影

苹果的 MapKit,基于麦卡托投影把经纬度转换成二维坐标。而且做了相关的数学运算,保证投影之后的数值可以像在平面坐标系一样计算距离。不过苹果的实现未知,我也无法在 Web 端使用。

# 总结

坐标系计算中有时候需要考虑坐标的高度信息计算距离,有时候需要计算已知坐标,角度和距离,求另一个坐标, 求两个坐标的插值。 目前还没有找到合适的计算库来解决这些问题。如果大家有相关的建议或者意见,欢迎评论。

参考:

geodesy (opens new window)

Displaying Maps (opens new window)

WGS84 To Mercator (opens new window)

Vue中如何设计面包屑

最近做的项目层级结构比较深,需要使用面包屑来定位用户所在的位置,方便用户跳转层级。所以就遇到 web app 如何设计面包屑问题。

# 根据路由匹配设置面包屑

当路由是下面这种情况的时候:

let routes = [
  {
    path: '/users',
    name: 'users-index',
    meta: {
      text: 'Users'
    },
    children: [
      {
        path: 'settings',
        name: 'user-setting'
      },
      // ...
    ]
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

我们可以使用$route.matched属性来生成面包屑。示例代码如下:

<ul>
  <li v-for="route in $route.matched">
    <router-link :to="{name: route.name}">
      {{ route.meta.text }}
    </router-link>
  </li>
</ul>
1
2
3
4
5
6
7

这种方法只适用于解决静态面包屑,当我们需要处理例如路径是users/1/settings这种情况,显然路由匹配就不是最佳选择了。并且路由层级的加深也并不意味着页面嵌套的加深。 当资源层级很深的时候,我们需要做很多空的父节点来处理。

# 每个页面设置面包屑

let routes = [
  {
    path: '/users/userId(\\d+)',
    component: userLayout
    children: [
      {
        path: 'settings',
        name: 'user-setting',
        component: userSettings
      },
      {
        path: '',
        name: 'user-dashboard'
      }
      // ...
    ]
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

当 URL 路径是users/1/settings的时候,我们期望的面包屑应该是Users > Lee > Settings。但是我们要怎么达到这个效果呢,特别是姓名信息我们还需要异步从服务器获取。

解决办法是使用Vuex来存储面包屑的信息。有使用数组来存储面包屑的方案,但是需要注意面包屑加入的顺序问题。为了更灵活的控制面包屑,我选择了使用链表的方式来存储面包屑。

# 定义数据结构

下面来看看面包屑的结构。

interface BreadCrumb {
  key: symbol;
  text?: string;
  to?: Route;
}

interface BreadCrumbItem {
  prev: BreadCrumbList;
  breadCrumb: BreadCrumb;
  next: BreadCrumbList;
}
type BreadCrumbList = BreadCrumbItem | null;
1
2
3
4
5
6
7
8
9
10
11
12

使用双向链表可以更方便的查询节点,每个节点主要存储三个信息,key 主要用来标识这个节点的唯一性,所以是Symbol类型,text 是当前节点展示的文本,to 是配合 vue-router 跳转到其他页面。 prev 和 next 当然就是指向上一个节点或者下一个节点啦。

# 定义 state 和 mutation

const state = {
  breadCrumbList: null,
  breadCrumbTail: null
};
1
2
3
4

state 里存储的分别是面包屑的头指针和尾指针。这样方便使用。

const mutations = {
  set(state, breadCrumbItem) {
    state.breadCrumbList = breadCrumbItem;
    state.breadCrumbTail = state.breadCrumbList;
  },
  add(state, breadCrumbItem) {
    if (state.breadCrumbTail) {
      state.breadCrumbTail.next = breadCrumbItem;
      breadCrumbItem.prev = state.breadCrumbTail;
      state.breadCrumbTail = breadCrumbItem;
    }
  },
  replace(state, breadCrumbItem) {
    let point = state.breadCrumbList;
    while (point) {
      if (point.breadCrumb.key === breadCrumbItem.breadCrumb.key) {
        point.breadCrumb.text = breadCrumbItem.breadCrumb.text;
        point.breadCrumb.to = breadCrumbItem.breadCrumb.to;
        return;
      } else {
        point = point.next;
      }
    }
  },
  remove(state, breadCrumbItem) {
    let point = state.breadCrumbTail;
    while (point) {
      if (point.breadCrumb === breadCrumbItem.breadCrumb) {
        let before = point.prev;
        let after = point.next;
        if (before) {
          before.next = after;
        } else {
          state.breadCrumbList = after;
        }
        if (after) {
          after.prev = before;
        } else {
          point.next = null;
        }
        return;
      } else {
        point = point.prev;
      }
    }
  },
  empty(state, breadCrumbItem) {
    state.breadCrumbList = null;
    state.breadCrumbTail = null;
  }
};
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

面包屑提供的方法有,set, add, replace, remove, empty。 功能都顾名思义。

# 每个组件设置面包屑

根据上面的路由设计,我们需要在userLayout组件里面完成Users > Lee前两级面包屑的填充。然后在userSettings组件里完成最后一级面包屑的填充。 由于第二级面包屑需要异步请求数据去填充,所以我们需要先事先添加一个空面包屑,之后再 replace。

// userLayout

mounted() {
  const breadCrumbIndex = {
    prev: null,
    breadCrumb: {
      key: Symbol('users'),
      text: 'Users',
      to: {
        name: 'user-index'
      }
    },
    next: null
  }
  const breadCrumbItem = {
    prev: null,
    breadCrumb: {
      key: Symbol('users')
    },
    next: null
  };
  store.commit('set', breadCrumbIndex)
  store.commit('add', breadCrumbItem);
  axios.get(`/api/user/${userId}`).then(res => {
    store.commit('replace', {
      ...breadCrumbItem,
      breadCrumb: {
        ...breadCrumbItem.breadCrumb,
        text: res.data.name,
        to: {
          name: 'user-dashboard'
        }
      }
    })
  })
  this.$once('hook:beforeDestroy', () => {
    store.commit('remove', breadCrumbIndex);
    store.commit('remove', breadCrumbItem);
  })
}

// userSettings

mounted() {
  const breadCrumbItem = {
    prev: null,
    breadCrumb: {
      key: Symbol('users'),
      name: 'Settings',
      to: this.$router.currentRoute
    },
    next: null
  };
  store.commit('add', breadCrumbItem);
  this.$once('hook:beforeDestroy', () => {
    store.commit('remove', breadCrumbItem);
  })
}
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

# 组件切换生命周期的执行顺序

当我们加载 userDashboard 并切换到 userSettings 页面的时候,组件执行生命周期执行顺序是。

beforeCreate userDashboard
created userDashboard
beforMount userDashboard
mounted userDashboard  // userDashboard show
beforeCreate userSettings // route to userSettings
created userSettings
beforMount userSettings
beforeDestory userDashboard
destroyed userDashboard
mounted userSettings
1
2
3
4
5
6
7
8
9
10

我们发现组件在beforeDestory后面之前的是mounted。所以如果把添加面包屑的代码放在beforeMount里的时候,其实是先添加面包屑再删除之前的面包屑的。不过好在我们用的是链表,所以顺序不影响面包屑的生成。

# 生成面包屑

breadCrumb() {
  const breadcrumb = [];
  let point = store.state.breadCrumbList;
  while (point && point.breadCrumb.name) {
    breadcrumb.push(point.breadCrumb);
    point = point.next;
  }

  return breadcrumb;
}
1
2
3
4
5
6
7
8
9
10

把面包屑的链表转换成数组,然后使用 vue 的v-for来渲染就可以了。

# 总结

面包屑的这两种实现方式我都有尝试过,个人感觉针对动态面包屑第二种方式比较优雅。如果你有更好的实现方式,欢迎讨论。

参考:

Handling breadcrumbs with VueX in a VueJS Single Page Application (opens new window)

Github如何省略每次输入验证信息

项目开发中经常会遇到 CI 自动拉取 Github 库的情况,我们个人使用的时候会手动输入账户密码信息,但是针对 CI 来说,我们一般会把敏感的认证信息放在环境变量里。所以针对认证信息的自动输入,需要做一些处理。

# 环境变量自动替换

针对 Travis CI,我们一般把认证信息放在环境变量里,所以在执行 clone 其他 repo 的时候,可以先设置认证信息。

git config --global url."https://${CI_USER_NAME}:${CI_USER_TOKEN}@github.com/".insteadOf "https://github.com/"'
1

这样设置的话,会自动替换 git clone 的网址。

# URL 里面写入认证信息

有时候需要以另外一个身份进行 clone repo 的时候,会直接在 repo 的 URL 前面加上认证信息。

git clone https://${USER_NAME}:${USER_TOKEN}@github.com/{username}/{repo}
1

这样可以很方便的 clone 信息,但是认证信息会存在当前库的 git config 文件里,有泄漏的风险。

# netrc 设置

netr 文件用于存储网站的认证信息,一般位置在~/.netrc。格式是:

machine github.com
login username
password xxxxxxx
1
2
3

# 总结

总共有这么三种省略认证信息的方式,根据实际情况选择自己方便的一种认证方式即可。

参考:

GitHub でユーザ名・パスワード省略 (opens new window)