avue中地图使用实现地图的联动_使用plotly绘制中国地图,并实现出行路线规划...

前言

​ 最近为导师的本科生制作数据结构实验教材,这个过程,用白岩松老师一句话来讲:“痛并快乐着”。数据结构实验,对学生来讲,实现完每个低层算法后,如果能够做一些实际有趣的应用,相信一定会增加学习的乐趣和信心。对作为助教的笔者来说,更是一个考验——首先得会做这个东西,不至于实验时被学生问倒,再者希望能够输出尽可能优质的内容,对本科没学过计算机的自己是一种锻炼,更是对自己助教工作的负责。

​ 我们对相关的几个实验开发了GUI编程,使用了python的PyQt5框架。为了给学生界面编程的压力降到尽可能低,我们将所有GUI部分全部实现,并制作了相关教程。前两个已经做完的工作:

avue中地图使用实现地图的联动_使用plotly绘制中国地图,并实现出行路线规划..._第1张图片
栈实现计算器

avue中地图使用实现地图的联动_使用plotly绘制中国地图,并实现出行路线规划..._第2张图片
用递归实现迷宫访问

​ ​ 图的这一个章节作业要求是实现最短路径算法,并能用鼠标在中国地图上进行交互式选点,接着求最短路径。python画地图有许多库可选择,在对常用框架进行比较后,最终选择了plotly。如下图,plotly开源绘图库有四大板斧,其中笔者使用过plotly for python 和 dash。知乎上有一些介绍的帖子可以作为参考,但许多具体细节国内的内容较少,还是需要参考官方文档。处理和地图的交互是本次任务的难点,笔者没有系统学过前端知识,而网络上也没有关于这个细节的详细介绍,最后在dash官方文档找到了这个方法,并将其应用到了地图交互上。

avue中地图使用实现地图的联动_使用plotly绘制中国地图,并实现出行路线规划..._第3张图片

实现

对任务目标进行细分:

  • 实现图的数据结构和最短路径算法
  • 加载全国各省市坐标点
  • 设计地图界面编程,使得地图能够处理鼠标点击,并在点击时获得其地理位置信息
  • 设计好相关算法接口,能够处理经纬度,并使用最短路径算法求解
  • 在地图上画出结果,点和点之间用直线相连

可视化界面

​ 首先需要建立一个可视化界面,可供数据输入、展示。笔者使用plotly的dash产品,当然,不使用dash可以吗?当然可以。但相信看完dash gallery后,你会喜欢上这个框架。官网对dash的介绍是

Build beautiful, web-based analytic apps. No JavaScript required.

​ 构建一个dash app需要以下几个流程:

准备工作之一:导入相关的库

import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
from dash.dependencies import Input, Output

准备工作之二:准备相关数据

​ 地点用经纬度表示,将经度和纬度各放到一个list里,注意每个位置上要对应起来。从网上搜到了一些地名——经纬度的对应,然后给对应经、纬度、地名提取出来。

places = {
      
    '山东': [117.000923, 36.675807],
        ...
}
logs = []
lats = []
names = []
for k, v in places.items():
    names.append(k)
    logs.append(v[0])
    lats.append(v[1])

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] #app样式

画图三部曲之一:每个图层用一个类or函数表示

​ 可以看到plotly dash画图,要先把每个图层作为一个函数或类表示,并把所有图层都放到一个list里面。这个matplotlib有些许差别,后者是直接把数据放进一个大函数里面。我们使用plotly.graph_objs.Scattermapbox类来对地图上画点。官方api参考后,使用了下面几个参数:

trace = list()
trace.append(
    go.Scattermapbox(
        mode="markers",
        text=names,
        lon=logs,
        lat=lats,
        marker={
      'size': 10}, 
        marker_color='red' 
    )
)

画图三部曲之二:使用Figure

​ 将上面构建的数据列表传到plotly.graph_objs.Figure类里面,并设计其layout。

fig = go.Figure(data=trace)
fig.update_layout(
    margin={
      'l': 3, 't': 3 'b': 3, 'r': 3}, # 边缘距离
    mapbox={
      
        'center': {
      'lon': places['河南'][0], 'lat': places['河南'][1]}, #图的中心在河南这里
        'style': "stamen-terrain",
        'zoom': 3 # 放大倍数
    },
)

画图三部曲之三:使用dash.Dash构建app

​ dash app相当于有若干html组件构成,下面传入三个组件。上面得到的fig传入dcc.Graph类,这个类是dash的核心组件类。

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.layout = html.Div(children=[
        html.H1(id="title",children="中国地图"), #1
        dcc.Graph( #2
            id='example-graph',
            figure=fig  # 地图从这里传入
        ),
        html.Div(id="div", children="") #3

])

点击响应

​ 地图画好了之后,我们希望能够有点击响应,点一个marker点,得到其经纬度,还需要对点击进行响应。我们点击的其实上面app.layout层中example-graph的标签,点击响应对应的函数用app.callback()进行装饰,

输入要用一个Input类来包装,第一个参数是html对应id为example-graph,第二个参数是该组件的点击响应参数clickData。可以看example-graph 的参数有

['id', 'responsive', 'clickData', 'clickAnnotationData', 'hoverData', 'clear_on_unhover', 'selectedData', 'relayoutData', 'extendData', 'restyleData', 'figure', 'style', 'className', 'animate', 'animation_options', 'config', 'loading_state']

输出要用一个Output类来包装,分别对应id和属性

@app.callback(.
    Output('div', 'children'), #输出第一个是 ,第二是
    [Input('example-graph', 'clickData')]) #输入第一个参数是id名称,第二个是函数参数
def display_click_data(clickData):
    global two_points
    if clickData:
        point_dict = clickData['points'][0]
        lon = point_dict['lon']
        lat = point_dict['lat']
        text = point_dict['text']
        return "您选择了 {}:({},{})".format(text, lon, lat)
    else:
        return ""

最后

if __name__ == '__main__':
    app.run_server(debug=True, port=8052)

​ 到此,画图、显示、点击响应,就做完了。我们希望点击两个点后就去计算最短路径,因此需要处理点击的点。

假设我们实现了最短路径算法:

def solve_shortest_path():
    print("solving shortest path")
    return path

新增一个选点的函数

two_points = []  # 需要选择两个点
SELECTED = ""


def selectPoint(point, double_points):
    if len(double_points) == 1 and double_points[0] == point:
        return
    else:
        double_points.append(point)
        print("Points you selected:{}".format(point))

再修改点击响应:

@app.callback(
    Output('div', 'children'),
    [Input('example-graph', 'clickData')])
def display_click_data(clickData):
    global two_points, SELECTED
    if clickData:
        point_dict = clickData['points'][0]
        lon = point_dict['lon']
        lat = point_dict['lat']
        text = point_dict['text']

        selectPoint([lon, lat], two_points)
        SELECTED += "{}:({},{})   ".format(text, lon, lat)
        MSG = "您选择了" + SELECTED

        if len(two_points) == 2:
            solve_shortest_path() # 算法求解
            two_points = []
            SELECTED = ""

        return MSG
    else:
        return "请选择两个点"

到此GUI交互已经设计完毕,效果如下

avue中地图使用实现地图的联动_使用plotly绘制中国地图,并实现出行路线规划..._第4张图片

最后放一下全部代码:

# -*- coding: utf-8 -*-


import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
from dash.dependencies import Input, Output

# 1.准备数据
places = {
      
    '山东': [117.000923, 36.675807],
    '河北': [115.48333, 38.03333],
    '吉林': [125.35000, 43.88333],
    '黑龙江': [127.63333, 47.75000],
    '辽宁': [123.38333, 41.80000],
    '内蒙古': [111.670801, 41.818311],
    '新疆': [87.68333, 43.76667],
    '甘肃': [103.73333, 36.03333],
    '宁夏': [106.26667, 37.46667],
    '山西': [112.53333, 37.86667],
    '陕西': [108.95000, 34.26667],
    '河南': [113.65000, 34.76667],
    '安徽': [117.283042, 31.86119],
    '江苏': [119.78333, 32.05000],
    '浙江': [120.20000, 30.26667],
    '福建': [118.30000, 26.08333],
    '广东': [113.23333, 23.16667],
    '江西': [115.90000, 28.68333],
    '海南': [110.35000, 20.01667],
    '广西': [108.320004, 22.82402],
    '贵州': [106.71667, 26.56667],
    '湖南': [113.00000, 28.21667],
    '湖北': [114.298572, 30.584355],
    '四川': [104.06667, 30.66667],
    '云南': [102.73333, 25.05000],
    '西藏': [91.00000, 30.60000],
    '青海': [96.75000, 36.56667],
    '天津': [117.20000, 39.13333],
    '上海': [121.55333, 31.20000],
    '重庆': [106.45000, 29.56667],
    '北京': [116.41667, 39.91667],
    '台湾': [121.30, 25.03],
    '香港': [114.10000, 22.20000],
    '澳门': [113.50000, 22.20000],
}
logs = []
lats = []
names = []
for k, v in places.items():
    names.append(k)
    logs.append(v[0])
    lats.append(v[1])

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

# 2 画图三部曲
trace = list()
trace.append(
    go.Scattermapbox(
        mode="markers",
        text=names,
        lon=logs,
        lat=lats,
        marker={
      'size': 10},
        # marker_color='red'
    )
)

fig = go.Figure(data=trace)
fig.update_layout(
    margin={
      'l': 3, 't': 3, 'b': 3, 'r': 3},
    mapbox={
      
        'center': {
      'lon': places['河南'][0], 'lat': places['河南'][1]},
        'style': "stamen-terrain",
        'zoom': 3
    },
)

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.layout = html.Div(children=[
    html.H1(id="title", children="中国地图"),  # 1
    dcc.Graph(  # 2
        id='example-graph',
        figure=fig  # 地图从这里传入
    ),
    html.Div(id="div", children="")  # 3

])


def solve_shortest_path():
    print("shortest path solving")
    return None


two_points = []  # 需要选择两个点
SELECTED = ""


def selectPoint(point, double_points):
    if len(double_points) == 1 and double_points[0] == point:
        return
    else:
        double_points.append(point)
        print("Points you selected:{}".format(point))


@app.callback(
    Output('div', 'children'),
    [Input('example-graph', 'clickData')])
def display_click_data(clickData):
    global two_points, SELECTED
    if clickData:
        point_dict = clickData['points'][0]
        lon = point_dict['lon']
        lat = point_dict['lat']
        text = point_dict['text']

        selectPoint([lon, lat], two_points)
        SELECTED += "{}:({},{})   ".format(text, lon, lat)
        MSG = "您选择了" + SELECTED

        if len(two_points) == 2:
            solve_shortest_path()
            two_points = []
            SELECTED = ""

        return MSG
    else:
        return "请选择两个点"


if __name__ == '__main__':
    app.run_server(debug=True, port=8052)

你可能感兴趣的:(avue中地图使用实现地图的联动_使用plotly绘制中国地图,并实现出行路线规划...)