前言

GPS在物流领域广泛应用,平时我们接触最多的应该是出租车服务和快递服务。一般企业级的管理平台会跟踪每个运输工具的状态,方便进行一些分析和优化运算。

每个GPS终端一般都会连接到服务器,实时发送数据。当然也有终端本身就可以实时进行处理数据,不用实时发回服务器。

今天我们可以开发一个GPS跟踪系统,用于实时跟踪我们的手机GPS信号,并且使用Python的在地图上(大屏)显示历史轨迹以及当前位置,并且实时更新。

为了完成手机实时跟踪系统,我们需要分解步骤:

  1. 获取手机GPS信息,并且实时传给Python程序
  2. Python程序实时更新
  3. 数据显示在地图上,画出轨迹图

我们分步介绍思路和技术栈。

先看动图

动图封面

实时上传手机GPS

安卓和iOS阵营的实现方法不同,安卓的方法很多,主要原因是安卓系统开放,接口容易暴露。iOS相对保守,这里我演示iOS方法,采用iPhone,iPad都可以。

iOS的实时传GPS的信息基本有两个思路。

  1. 找到一些app,可以把GPS信息暴露成一个网址,之后可以从这个网址解析GPS信息。这这个方法有缺点,如果网址是开放的,那无疑信息是不安全的。如果网址是需要授权的,那我们还需要登录到某些网站去获取授权,这样也是很麻烦。
  2. 采用iOS自带的app获取信息,这里我采用的iCloud账号。因为iCloud账户可以获取几乎所有的信息,包括相片,通讯类,设备信息,实时GPS数据等。所以我们只需要用Python登录iCloud就可以获取对应设备的GPS信息了。

这里推荐的库就是pyicloud,我们只需要调用PyiCloudService来创建API,就可以获取device的location 信息了。以下就是代码,由于我的iCloud绑定了多个设备,这里我跟踪device1的信息。

from pyicloud import PyiCloudService
import pandas as pd
import sys
import dash
from dash import Dash
import dash_leaflet as dl
from jupyter_dash import JupyterDash
from dash import html,dcc
from dash.dependencies import Input, Output,State
from datetime import datetime
import random
import numpy as np
api = PyiCloudService('[email protected]')

iOS的安全机制还是很复杂的,如果你的设备设置了Two-factor authentication,也就是还要验证码,我们也可以判断是否需要添加验证码,并提示用户输入。

通过上面的代码,我们就可以获得iCloud的所有信息了,包括GPS。

PHONE = api.devices[1]
print(PHONE.location())

搞定了 GPS的信息,我们可以写一个函数,用于更新数据,并保存在一个DataFrame里面。

这个DataFrame包含时间戳和对应的GPS 经纬度信息。

def update_gps_from_phone(phone_device,df):
    loc = phone_device.location()
    now = datetime.now()
    df.loc[now] = [loc['latitude'],loc['longitude']]
    return df

做个实时更新网站

做个网站,我首选的是plotly Dash,为什么?

因为我只想用Python搞定所有前端和后端。Dash我之前文章已经介绍过了,这里需要Interval 插件来更新页面。Interval就是一个定时组件,用于隔一段时间处理事件,比如更新画面,更新数据等。

dash的文章可以参考:

如何用python做一个简单的输入输出交互界面?

注意这个网站,可以运行在任何电脑,只需联网可以,并且对应的网址可以不用公网IP,只用127.0.01 就可以。因为我们不需远程访问该网站,iCloud自动会同步GPS信息。

实时更新地图

地图,首选的是plotly,因为适配Dash最佳,但是Plotly画轨迹不是很友好。

这里我采用Leaflet 来画轨迹地图,leaflet 号称开源第一js地图库,主要是它的设计思路较为合理,采用图层(layer)的形式来叠加,使得每个layer之间耦合度较低,控制起来更加灵活。

map 底图(tile)自身就是一个layer,你可以在tile layer 之上叠加任何layer。对于我们这个项目,我们需要叠加一个曲线layer (Polyline)以及marker layer。

Leaflet 是一个JavaScript的库,所以在python里面调用需要wrap 一下。

很多大神们已经开源了python库,Folium就是一个,但是这里我要用是另一个,Dash-leaflet。

为什么是后者,因为它的设计规范是参考Dash组件的。 我们可以调用Dash-leaflet的组件,比如Polyline,放在网页的layout里面。并且可以采用callback函数对数据进行实时更新。

总结一下,我们设计完整的网页代码如下:

  1. 创建Dash 程序
  2. 创建Dash的组件,包括以下:
  • Interval:用于实时更新dash界面
  • Input:用于控制更新周期,比如3s
  • Leaflet Map,需要包含以下图层
    • tile图层,是地图的背景层
    • PolylineDecorator 图层,是Polyline的升级版,方便我们快速画出曲线,以及marker,这里我们marker我采用的自定义的icon
  1. 创建Dash Callback 函数
  • callback函数用于对某些事件进行响应,这里我们的事件就是Interval的n_intervals值发生变化,触发GPS更新数据。
#app =  Dash(__name__)
app = JupyterDash(__name__)

input_v = dcc.Input(
            id="input_range", type="number", placeholder="input with interval",value=3,
            min=2, max=120, step=1,
        )

# interval
interval_c = dcc.Interval(id='interval_component',interval=3000,n_intervals=0)

# tile_layer

url = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
tile_layer = dl.TileLayer(url=url, maxZoom=20, attribution=attribution)

# marker_layer
iconUrl = "assets/person-running-solid.svg"
marker = dict(rotate=False, markerOptions=dict(icon=dict(iconUrl=iconUrl, iconAnchor=[16, 16])))
patterns = [dict(repeat='10', dash=dict(pixelSize=5, pathOptions=dict(color='#3388ff', weight=2, opacity=0.8))),
            dict(offset='100%', repeat='50%', marker=marker)]
polyline = dl.PolylineDecorator(positions=LOC_DF.values.tolist(), patterns=patterns,id = 'polyline_component')
map_fig = dl.Map([tile_layer,polyline], style={'width': '1000px', 'height': '500px'},center=SIM_POS,zoom=15,id = 'map_component')

app.layout = html.Div([input_v,interval_c,map_fig])

@app.callback(
    Output(component_id='polyline_component', component_property='positions'),
    Input(component_id='interval_component', component_property='n_intervals'),
)
def update_output(n_clicks):
global LOC_DF
    LOC_DF = update_gps_from_phone(PHONE,LOC_DF)
return LOC_DF.values.tolist()

@app.callback(
    Output(component_id='interval_component', component_property='interval'),
    Input(component_id='input_range', component_property='value'),
)
def update_output(value):
return value*1000

app.run_server(debug=False)

总结

以上就是个人手机的实时跟踪系统的设计,相当于Python版本的find my phone,不过因为我们可以保存所有的历史轨迹,所以用途还是很广泛的。

另外,代码包含leaflet的地图设计,还是具有参考价值的。