GPS参数提取及轨迹重现

本项目分为两个版本:

​ 1、基于百度地图API的轨迹快速重现,项目体验地址:GPS轨迹重现在线版

​ 2、基于Leaflet的状态及轨迹重现,项目体验地址:GPS数据提取及轨迹重现

两者均采用在线地图,都需要Internet的支持

1、基于百度地图API的轨迹快速重现

1.1、功能简介

  • 数据提取:提取出GPS文件中的坐标信息
  • 坐标转换:原始坐标转化成百度坐标
  • 轨迹描绘:根据提取出来的坐标信息将轨迹一次性描绘出来
  • 支持地图的放大与缩小

1.2、效果展示

GPS_fast 00_00_00-00_00_30~1

1.3、项目实现

1.3.1、准备

  • 这里需要注意的是,api是用JavaScript写的,所以需要懂一点前端(HTML+CSS+Javascript)的知识,不难。

首先,因为需要用到百度地图API,所以先要到官网申请一个ak,百度地图开放平台,具体操作可以参考百度地图API及使用,之后就可以在本地新建一个html文件,在官网上复制一个例程保存到改文件中并更改ak,然后就可以使用百度API了,写好代码后双击就可以在浏览器中看到运行效果。

1.3.2、实现

  • 完成了上述准备之后就可以开始实现项目了,js代码可以写在html文件中的script标签中,也可以写在js文件中然后通过script标签引入,具体可参考源码。

1、加载地图,这里我用的是API3.0,百度地图JSAPI 3.0类参考,示例可以在这里找到 百度地图API SDK

1
2
3
4
5
// 百度地图API功能
var map = new BMap.Map("allmap"); // 创建Map实例
map.centerAndZoom(new BMap.Point(104.0820, 30.63231), 17); // 初始化地图,设置中心点坐标和地图级别
map.enableScrollWheelZoom(true); //开启鼠标滚轮缩放
map.setMapType(BMAP_SATELLITE_MAP); // 设置地图类型为地球模式

2、数据提取及轨迹描绘,数据提取是先读取文件并将内容保存到一个变量中,然后通过“$”符号进行分行处理,再根据‘,’进行分割处理并提取有效信息,详情见以下代码,关于GPS数据格式可参考GPS数据读取与处理

至于轨迹描绘,本来我是直接用百度地图API进行轨迹状态重现的,也就是逐点描绘,但后来因为一些问题(卡、慢)没有解决就改用了Leaflet。这里我采用的是百度地图API提供的加载海量点的方法来实现轨迹的快速描绘,

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
96
97
var pointArr = [];
var infoArr = [];
//文件读取函数
function dealSelectFiles() {
var file = document.getElementById("selectFiles").files[0];
var reader = new FileReader();//这是核心,读取操作就是由它完成.
reader.readAsText(file);//读取文件的内容,也可以读取文件的URL

reader.onload = function () {
//当读取完成后回调这个函数,然后此时文件的内容存储到了result中,直接操作即可
var arr = this.result.split("$"); //数据分行处理

var i = 1;
flag_timer = 1;
var high;

while(arr[i]!=null && arr[i] != undefined && arr[i] != ''){
//找到返回位置,未找到返回-1
if(arr[i].indexOf('GPGGA') != -1){
var info = arr[i].split(",");
high = info[9] +' m';
}
else if(arr[i].indexOf('GPRMC') != -1 && arr[i].indexOf('V') == -1){

var info_temp = {
date:undefined,
time:undefined,
high:undefined,
speed:undefined,
direction:undefined
};

var info = arr[i].split(",");

if(info[2] == 'A'){
final_lat = info[3];
final_lon = info[5];
my_translate(info[3],info[5]);
sped = parseFloat(info[7]) * 1.852; //km/h = 海速*1.852

info_temp.high = high;
info_temp.speed = sped;
info_temp.time = (parseInt(info[1].slice(0,2))+8)%24+':'+info[1].slice(2,4)+':'+info[1].slice(4,6);
info_temp.date = info[9].slice(4,6)+'年'+info[9].slice(2,4)+'月'+info[9].slice(0,2)+'日';
info_temp.direction = info[8]+'°';

infoArr.push(info_temp);
}
}
i++;
}
alert('数据处理完成');

// 轨迹描绘
var index = 0;
var len = pointArr.length;
var points = [];
while(index<len){
var temp = new BMap.Point(pointArr[index].lon,pointArr[index].lat);
points.push(temp);
index++;
}
var options = {
size: BMAP_POINT_SIZE_SMALLER,
shape: BMAP_POINT_SHAPE_CIRCLE,
color: '#0000FF'
}
var pointCollection = new BMap.PointCollection(points, options); // 初始化PointCollection
map.addOverlay(pointCollection); // 添加Overlay
map.addOverlay(new BMap.Marker(new BMap.Point(pointArr[0].lon,pointArr[0].lat),{icon: startIcon})); //描起点
map.addOverlay(new BMap.Marker(new BMap.Point(pointArr[len-1].lon,pointArr[len-1].lat),{icon: finalIcon})); //描起点
}
}

function my_translate(lat,lon)
{
//转换经度 dddmm.mmmm转ddd.dddd
var d = lon.slice(0,3); //取出度
var m = lon.slice(3,10); //取出分
m = m/60;
lon = parseFloat(d) + parseFloat(m);

//转换纬度 ddmm.mmmm转dd.dddd
d = lat.slice(0,2);
m = lat.slice(2,9);
m = m/60;
lat = parseFloat(d) + parseFloat(m); //转成浮点型后计算

var t1 = wgs2gcj(lat, lon); // 坐标转换
var t2 = gcj2bd(t1[0],t1[1]);

var point_temp = {
lat: t2[0],
lon: t2[1],
};
pointArr.push(point_temp);
}

3、坐标转换,因为百度坐标是在原始坐标上进行了加密处理,所以不能直接使用用原始坐标。百度提供的转换方法是在线转换,这种方法使用次数有限制并且转换较慢,所以我在Github上找到了一种离线的转换方式,经测试,很准确。

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
/*
pi: 圆周率。
a: 卫星椭球坐标投影到平面地图坐标系的投影因子。
ee: 椭球的偏心率。
x_pi: 圆周率转换量。
transformLat(lat, lon): 转换方法,比较复杂,不必深究了。输入:横纵坐标,输出:转换后的横坐标。
transformLon(lat, lon): 转换方法,同样复杂,自行脑补吧。输入:横纵坐标,输出:转换后的纵坐标。
wgs2gcj(lat, lon): WGS坐标转换为GCJ坐标。
gcj2bd(lat, lon): GCJ坐标转换为百度坐标。
*/

var pi = 3.14159265358979324;
var a = 6378245.0;
var ee = 0.00669342162296594323;
var x_pi = 3.14159265358979324 * 3000.0 / 180.0;


function wgs2bd(lat, lon) {
_wgs2gcj = wgs2gcj(lat, lon);
_gcj2bd = gcj2bd(_wgs2gcj[0], _wgs2gcj[1]);
return _gcj2bd;
}

function gcj2bd(lat, lon) {
x = lon, y = lat;
z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * x_pi);
theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * x_pi);
bd_lon = z * Math.cos(theta) + 0.0065;
bd_lat = z * Math.sin(theta) + 0.006;
return [ bd_lat, bd_lon ];
}

function bd2gcj(lat, lon) {
x = lon - 0.0065, y = lat - 0.006;
z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi);
theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi);
gg_lon = z * Math.cos(theta);
gg_lat = z * Math.sin(theta);
return [ gg_lat, gg_lon ];
}

function wgs2gcj(lat, lon) {
dLat = transformLat(lon - 105.0, lat - 35.0);
dLon = transformLon(lon - 105.0, lat - 35.0);
radLat = lat / 180.0 * pi;
magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
mgLat = lat + dLat;
mgLon = lon + dLon;
return [ mgLat, mgLon ];
}

function transformLat(lat, lon) {
ret = -100.0 + 2.0 * lat + 3.0 * lon + 0.2 * lon * lon + 0.1 * lat * lon + 0.2 * Math.sqrt(Math.abs(lat));
ret += (20.0 * Math.sin(6.0 * lat * pi) + 20.0 * Math.sin(2.0 * lat * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(lon * pi) + 40.0 * Math.sin(lon / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(lon / 12.0 * pi) + 320 * Math.sin(lon * pi / 30.0)) * 2.0 / 3.0;
return ret;
}

function transformLon(lat, lon) {
ret = 300.0 + lat + 2.0 * lon + 0.1 * lat * lat + 0.1 * lat * lon + 0.1 * Math.sqrt(Math.abs(lat));
ret += (20.0 * Math.sin(6.0 * lat * pi) + 20.0 * Math.sin(2.0 * lat * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(lat * pi) + 40.0 * Math.sin(lat / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(lat / 12.0 * pi) + 300.0 * Math.sin(lat / 30.0 * pi)) * 2.0 / 3.0;
return ret;
}

4、界面布局,这个就是通过编辑CSS代码来实现的,了解一下CSS就知道了,不难,不过值得一提的是我们调试CSS代码可以直接在浏览器的开发者工具里进行,调试好了再将代码复制到css文件中,这样方便一些。

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
body, html,#allmap {width: 100%;height: 100%;overflow: hidden;margin:0;font-family:"微软雅黑";}

.info {
z-index: 999;
width: auto;
padding: 10px;
margin-left: 10px;
position: fixed;
top: 10px;
background-color: rgba(265, 265, 265, 0.9);
border-radius: 5px;
font-size: 14px;
color: #666;
box-shadow: 0 2px 6px 0 rgba(27, 142, 236, 0.5);
}

ul li {
list-style: none;
}
.btn-wrap {
z-index: 999;
width: 226px;
position: fixed;
bottom: 200px;
right: 10px;
padding: 10px;
border-radius: 5px;
background-color: rgba(265, 265, 265, 0.9);
box-shadow: 0 2px 6px 0 rgba(27, 142, 236, 0.5);
}
.btn {
width: 100px;
height: 30px;
float: left;
background-color: #fff;
color: rgba(27, 142, 236, 1);
font-size: 14px;
border:1px solid rgba(27, 142, 236, 1);
border-radius: 5px;
margin: 0 5px 6px;
text-align: center;
line-height: 30px;
}
.btn:hover {
background-color: rgba(27, 142, 236, 0.8);
color: #fff;
cursor: pointer;
}

.file_select {
margin-top: 8px;
margin-bottom: 8px;
margin-left: 5px;
}
.information {
width: 200px;
height: 25px;
float: left;
font-size: 14px;
margin-left: 5px;
}
.txt{
margin: 2px 10px 2px 10px;
}
span,textarea{
vertical-align: middle;
}

感兴趣的朋友可以直接通过百度地图API实现其它功能,源代码在文章最下方。

2、基于Leaflet的状态及轨迹重现

2.1、功能简介

  • 数据提取:提取出GPS文件中的坐标、速度、高度等信息
  • 坐标转换:原始坐标转化成其他坐标系
  • 轨迹描绘:根据提取出来的信息将轨迹逐点描绘出来,并显示相应的状态信息
  • 支持地图的放大与缩小
  • 支持图层切换,MapBox提供的图层支持国外的高级别卫星地图

2.2、效果展示

GPS_Leaflet

306

402

2.3、项目实现

因为我之前使用百度地图API有一些问题没有解决,所以改用了Leaflet,有了之前使用百度地图API的经验,现在上手Leaflet就很容易,因为很多接口都差不多。

2.3.1、准备

首先,到Leaflet官方下载源码,Leaflet - a JavaScript library for interactive maps ,当然不下载直接在线引入也是可以的。然后根据官方说明操作就可以了,也可以参考别人写的教程LeafletJS - 教程_学习LeafletJS

2.3.2、实现

与百度地图API不同的是,Leaflet需要我们自己添加瓦片图层,这个选择就多了,例如高度地图、天地图…,可以在网上找。说到这里就不得不提一下我选择的MapBox图层,这个图层的清晰度比天地图等要高,而且包含全世界的高级别卫星地图,缺点就是服务器在国外,加载速度相对较慢。

1、初始化,一开始选择了三个切片图层,但因为高德的瓦片不是很全并且坐标系和另外两个有所不同,所以就没用了。天地图的切片图层可以到这里获取天地图API ;MapBox的图层可以到这里获取Mapbox Studio,关于MapBox的操作可以参考Leaflet学习笔记【更新中】

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
// Creating map options
var mapOptions = {
center: [30.63231, 104.0820],
zoom: 16
}
// Creating a map object
var map = new L.map('map', mapOptions);

// Creating Layer objects
// 高德地图影像
var Gaode = new L.tileLayer('https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}', {
maxZoom: 20,
maxNativeZoom: 20,
minZoom: 3,
attribution: "高德地图 AutoNavi.com",
subdomains: "1234"
});

// 天地图影像
var Tiandi = L.tileLayer('http://t{s}.tianditu.gov.cn/img_w/wmts?tk=da144caca3dc9a894a921aa6c937ca71&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TileMatrix={z}&TileCol={x}&TileRow={y}', {
subdomains: [0, 1, 2, 3, 4, 5, 6, 7],
});

// MapBox
var mapBox = L.tileLayer('https://api.mapbox.com/styles/v1/deven3433/ckvi4woqi0vy615qljjvthgn9/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1IjoiZGV2ZW4zNDMzIiwiYSI6ImNrdmkzYjMxcTI2Y2Iydm55NmEwaWs2Z2wifQ.bMgbUERe8dqTan0PENXgBw');

var baseLayers = {
//"高德地图": layer1, // 高德卫星影像显示不全,故没有采用,此外高德和另外两个采用的坐标系不同
"天地图": Tiandi,
"MapBox": mapBox
};

// Adding layer to the map
map.addLayer(Tiandi);

// 添加图层选择控件到地图
L.control.layers(baseLayers, null).addTo(map);


var flag_timer = true; // 定时描点使能标志
document.onkeydown = function (event) { //在全局中绑定按下事件
var e = event || window.e;
var keyCode = e.keyCode || e.which;

switch (keyCode) {
// 按Backspace结束描点
case 8:
flag_timer = false;
break;
// 按空格暂停描点
case 32:
alert('暂停,点击确定继续');
break;
}
}

2、关键变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var r = 255, g = 0, b = 0;  // 颜色值
var pointColor = { // 描点属性
radius: 3,
stroke: false,
fillColor: 'rgb(' + r + ',' + g + ',' + b + ')',
fillOpacity: 1,
};

// 起始点标志, 0为起点、1为普通点、2为终点
var flag = 0;

// 重现速度
var displaySpeed = 50;
//最大速度
var MaxSpeed = 0;

// 上一地图级别,当前地图级别
var last = 16, current = 16;
var pointArr = []; // 坐标数组
var infoArr = []; // 信息数组

3、按钮功能实现

1
2
3
4
5
6
7
8
9
function setSpeed(){  //设置描点时间 单位:ms/点
displaySpeed = document.getElementById('spedSet').value;
}
function ZoomIn(){ // 放大地图
map.zoomIn();
}
function ZoomOut(){ //缩小地图
map.zoomOut();
}

4、自定义的一些接口

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
//设置当前可能的状态图
function stateDisplay(speed){
if(speed < 10){
document.getElementById("state").src = "icon/foot.png";
} else if(speed < 30){
document.getElementById("state").src = "icon/bike.png";
} else if(speed < 120){
document.getElementById("state").src = "icon/car.png";
} else if(speed < 200){
document.getElementById("state").src = "icon/train.png";
} else if(speed < 500){
document.getElementById("state").src = "icon/high_speed.png";
} else if(speed >= 500){
document.getElementById("state").src = "icon/plane.png";
}
}

// 坐标转换
function my_translate(lat,lon) //根据坐标信息描点
{
//转换经度 dddmm.mmmm转ddd.dddd
var d = lon.slice(0,3); //取出度
var m = lon.slice(3,10); //取出分

m = m/60;
lon = parseFloat(d) + parseFloat(m);

//转换纬度 ddmm.mmmm转dd.dddd
d = lat.slice(0,2);
m = lat.slice(2,9);

m = m/60;
lat = parseFloat(d) + parseFloat(m); //转成浮点型后计算

var point_temp = {
lat: lat,
lon: lon,
};
pointArr.push(point_temp);
}

5、描点函数

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
var start =  {
icon: L.icon({
iconUrl: 'icon/start.png',
iconSize: [48, 48],
iconAnchor: [24, 42],
})
};
var final = {
icon: L.icon({
iconUrl: 'icon/final.png',
iconSize: [48, 48],
iconAnchor: [24, 42],
})
};

var count = 0; //用于地图级别设置消抖
function display(lat,lon,speed){
stateDisplay(speed);
g = parseInt(speed/MaxSpeed * 255);
b = g;
pointColor.fillColor = 'rgb('+r+','+g+','+b+')';
if(flag == 0){
new L.Marker([lat,lon],start).addTo(map); //描起点
map.panTo([lat,lon]); // 设置地图中心点
flag = 1;
}
else if(flag == 1){
new L.circleMarker([lat,lon],pointColor).addTo(map);
if(speed<150){
current = 16;
}else if(speed<400){
current = 15;
}else if(speed<600){
current = 14;
}else if(speed>=600){
current = 13; // speed存在空值需过滤
}

map.panTo([lat,lon],{
animate: false
}); // 设置地图中心点

//设置地图级别
if(last != current){
count++;
if(count > 10){
count = 0;
last = current;
map.setZoom(current);
}
}
}
else{
new L.Marker([lat,lon],final).addTo(map); //描终点
map.panTo([lat,lon]); // 设置地图中心点
flag = 0;
}
}

6、数据提取及轨迹重现

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
//文件读取函数
function dealSelectFiles() {
var file = document.getElementById("selectFiles").files[0];
var reader = new FileReader();//这是核心,读取操作就是由它完成.
reader.readAsText(file);//读取文件的内容,也可以读取文件的URL

reader.onload = function () {
//当读取完成后回调这个函数,然后此时文件的内容存储到了result中,直接操作即可
var arr = this.result.split("$"); //数据分行处理

var i = 1;
var high,sped;
flag_timer = true;

while(arr[i]!=null && arr[i] != undefined && arr[i] != ''){
//找到返回位置,未找到返回-1
if(arr[i].indexOf('GPGGA') != -1){
var info = arr[i].split(",");
high = info[9];
}
else if(arr[i].indexOf('GPRMC') != -1 && arr[i].indexOf('V') == -1){
var info_temp = {
date:undefined,
time:undefined,
high:undefined,
speed:undefined,
direction:undefined
};
var info = arr[i].split(",");

if(info[2] == 'A'){
my_translate(info[3],info[5]);
sped = parseFloat(info[7]) * 1.852; //km/h = 海速*1.852
if(MaxSpeed < sped)MaxSpeed = sped;

info_temp.high = high;
info_temp.speed = sped;
info_temp.time = (parseInt(info[1].slice(0,2))+8)%24+':'+info[1].slice(2,4)+':'+info[1].slice(4,6);
info_temp.date = info[9].slice(4,6)+'年'+info[9].slice(2,4)+'月'+info[9].slice(0,2)+'日';
info_temp.direction = info[8]+'°';

infoArr.push(info_temp);
}
}
i++;
}
alert('数据处理完成');
//更新颜色条刻度
document.getElementById("sped1").innerText = parseInt(MaxSpeed/4);
document.getElementById("sped2").innerText = parseInt(MaxSpeed/2);
document.getElementById("sped3").innerText = parseInt(MaxSpeed/4 * 3);
document.getElementById("maxsped").innerText = parseInt(MaxSpeed);

var index = 0;
var len = pointArr.length;
var timer = setInterval(function(){

display(pointArr[index].lat,pointArr[index].lon,infoArr[index].speed);
document.getElementById("high").value = infoArr[index].high +' m';
document.getElementById("time").value = infoArr[index].time;
document.getElementById("speed").value = infoArr[index].speed.toFixed(4)+' km/h';; //保留4位小数
document.getElementById("direction").value = infoArr[index].direction;
document.getElementById("date").value = infoArr[index].date;
document.getElementById("longitude").value = pointArr[index].lon.toFixed(4)+'°E';
document.getElementById("latitude").value = pointArr[index].lat.toFixed(4)+'°N';

// 更新高度显示
if(!isNaN(infoArr[index].high))document.getElementById("highbar").value = infoArr[index].high;

index++;
// 更新进度条
var Progress = (index/len)*1000;
document.getElementById("progressbar").value = Progress;

if(index>= len){ // 描点结束
flag = 2;
display(pointArr[len-1].lat,pointArr[len-1].lon);
pointArr = []; //清空数组
infoArr = [];
clearInterval(timer);
}
if(flag_timer == false){ // 描点被按下Backspace结束
flag_timer = true;
flag = 2;
pointArr = [];
infoArr = [];
clearInterval(timer);
}

},displaySpeed);
}
}

3、项目源代码

最后把项目源代码给出来,需要的自取

Github:https://github.com/pingden/GPS_Trajectory_reproduction.git

Gitee:https://gitee.com/pingdeng/GPS.git

百度云:

​ 链接:https://pan.baidu.com/s/1Qh3G2VKrSIvBAZPRevXsEA
​ 提取码:0p9h

有问题可以在评论区留言~~~