Folium Popup

Folium交互式地图及popup中文显示

本文将介绍使用Folium生成交互式地图并使popup弹框支持中文显示

Folium是一个python包,可以用Python代码非常方便的生成嵌入网页的基于Leaflet的交互式地图,介绍可见主页。最简单的安装方法还是通过pip。

Folium的说明文档不太完整,但是几个例子还是能把功能说明白的,主页中就有。当然,如果出现莫名其妙的bug,就得考验大家的智慧了。在我个人的使用过程中就遇到了一个困扰很久的bug,popup弹框不能显示中文。受python4oceanographers的一篇博文的启发,终于解决,在此特别感谢,并分享给中文世界的朋友们!

本文将通过一个应用场景尝试解决以下问题:

  1. 找到距离某地最近的若干记录(并用公里显示距离)
  2. 显示在Folium地图上(配置合适的标志)
  3. 点击显示详细信息(中文!)

数据预处理

首先还是下载中国气象站点数据,读入pandas并转换成十进制度,上一篇博文有介绍,不再赘述。

In [1]:
import pandas as pd
In [2]:
data = pd.read_excel('SURF_CHN_MUL_HOR_STATION.xlsx')
In [3]:
def min2deg(x):
    y = int(x)
    y = y + (x - y)*1.66666667
    return y

data['经度'] = data['经度'].apply(min2deg)
data['纬度'] = data['纬度'].apply(min2deg)

整理一下数据精确的有效数字

In [4]:
data['观测场拔海高度(米)'] = data['观测场拔海高度(米)'].apply(lambda x:round(x,1))

data['纬度'] = data['纬度'].apply(lambda x:round(x,2))
data['经度'] = data['经度'].apply(lambda x:round(x,2))
In [5]:
data.head(5)
Out[5]:
省份 区站号 站名 纬度 经度 气压传感器拔海高度(米) 观测场拔海高度(米)
0 安徽 58015 砀山 34.45 116.33 45.4 44.2
1 安徽 58016 萧县 34.18 116.97 35.9 34.7
2 安徽 58102 亳州 33.87 115.77 39.2 37.7
3 安徽 58107 临泉 33.02 115.28 37.0 35.8
4 安徽 58108 界首 33.23 115.33 35.0 34.0

将这个版本的数据存起来,以便下次使用

data.to_csv('中国气象站点信息_decimal.csv')

计算两点距离(公里表示)

已知两点经纬度计算两点距离有其公式,详见Movable-Type的介绍(包括JS代码和网页计算器)。以下Python代码来自Bruno Rocha的分享在此感谢!

In [6]:
# Haversine formula example in Python
# Author: Wayne Dyck

import math

def distance(origin, destination):
    lat1, lon1 = origin
    lat2, lon2 = destination
    radius = 6371 # km

    dlat = math.radians(lat2-lat1)
    dlon = math.radians(lon2-lon1)
    a = math.sin(dlat/2) * math.sin(dlat/2) + math.cos(math.radians(lat1)) \
        * math.cos(math.radians(lat2)) * math.sin(dlon/2) * math.sin(dlon/2)
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
    d = radius * c

    return d

赤道上1度约111公里,北纬30度附近1度越96公里:

In [7]:
distance((0,1),(0,0))
Out[7]:
111.19492664455873
In [8]:
distance((30,0),(30,1))
Out[8]:
96.29732567761188

用Harversine函数计算所有记录的距离:

In [9]:
data['距离雄县(km)'] = data.apply(lambda r:distance((r['纬度'],r['经度']),(39.02,116.10)),axis=1)

选择距离雄县50km以内的记录

In [10]:
data[data['距离雄县(km)']<50].sort_values('距离雄县(km)')
Out[10]:
省份 区站号 站名 纬度 经度 气压传感器拔海高度(米) 观测场拔海高度(米) 距离雄县(km)
592 河北 54636 雄县 39.02 116.10 12.2 11.1 0.000000
569 河北 54605 安新 38.93 115.93 5.6 4.4 17.779593
546 河北 54503 容城 39.07 115.82 14.0 12.8 24.811635
552 河北 54518 霸州 39.17 116.40 10.4 8.9 30.797155
574 河北 54610 任丘 38.73 116.10 9.2 8.1 32.246529
576 河北 54612 文安 38.85 116.45 5.4 4.3 35.689976
547 河北 54506 高碑店 39.32 115.95 25.7 24.6 35.777055
565 河北 54601 徐水 38.98 115.65 14.3 13.1 39.140108
567 河北 54603 高阳 38.72 115.77 11.2 10.0 43.920144
553 河北 54519 永清 39.30 116.48 13.4 12.2 45.197136
550 河北 54512 固安 39.42 116.28 22.6 21.4 47.103376

剔除雄县本身后即为要找的50km内的记录:

In [11]:
selected_st = data[data['距离雄县(km)']<50].sort_values('距离雄县(km)').iloc[1:,:]

Folium地图显示

Folium设置初始地图很简单,只需指定一个中心坐标。找到雄县的坐标:

In [12]:
import folium
In [13]:
data.ix[data['站名']=='雄县',['站名','纬度','经度']]
Out[13]:
站名 纬度 经度
592 雄县 39.02 116.1
In [14]:
xmap = folium.Map(location=[39.0,116.1])
In [15]:
xmap
Out[15]:

在雄县这个位置添加一个标记,设置标记的图形样式:

这个icon的设置很有趣,可以变化出很多用途,Folium.Icon类的介绍可以通过在jupyter notebook里执行Folium.Icon?显示。

简单说这个类可以设置color, icon_color, icon, angle, prefix这5个参数。color的可选项包括:['red', 'blue', 'green', 'purple', 'orange', 'darkred', 'lightred', 'beige', 'darkblue', 'darkgreen', 'cadetblue', 'darkpurple', 'white', 'pink', 'lightblue', 'lightgreen', 'gray', 'black', 'lightgray'] ,或者HTML颜色代码;icon_color同上;icon可以在Font-Awesome网站中找到对应的名字,并设置prefix参数为’fa';最后,angle以度为单位设置。

In [16]:
icon_kw = dict(prefix='fa', color='orange', icon_color='darkred', icon='cny')
icon = folium.Icon(**icon_kw)

创建一个Marker对象,然后加入xmap地图中,注意,这里popup我直接写上了汉字“雄县”:

In [17]:
folium.Marker(
    location=[39.0,116.1],
    popup='雄县',
    icon=icon
).add_to(xmap);
In [18]:
xmap
Out[18]:

点击标志,发现显示的是乱码,其原因在于这里popup只能使用ascii编码。

中文显示

我们不妨先看一下系统默认编码是什么:

In [19]:
import sys
print(sys.getdefaultencoding())
utf-8

Folium自带一个IFrame类可以在其中嵌入html代码并解释,因此给了我们一个机会借道HTML来实现中文显示。那么首先要把汉字编码为ascii xml字符的形式。经过多次试验,以下函数可以实现正确显示:

In [20]:
def utf2asc(s):
    return str(str(s).encode('ascii', 'xmlcharrefreplace'))[2:-1]
In [21]:
utf2asc('雄县')
Out[21]:
'&#38596;&#21439;'

写一个最简单的html标记:

In [22]:
heading3 = """<h3>{}</h3>""".format
In [23]:
heading3(utf2asc('雄县'))
Out[23]:
'<h3>&#38596;&#21439;</h3>'
In [24]:
from folium.element import IFrame

iframe = IFrame(html=heading3(utf2asc('雄县¥')),width=100,height=50)

加上一个¥以测试对中文符号的显示能力

In [25]:
popup = folium.Popup(iframe)
In [26]:
xmap = folium.Map(location=[39.0,116.1])

folium.Marker(
    location=[39.0,116.1],
    popup=popup,
    icon=icon
).add_to(xmap);

xmap
Out[26]:

此时popup可以正确显示中文及符号

popup美化

借鉴Filipe的博文中的HTML代码以美化popup,使其呈现表格:

列名也需要使用编码映射:

In [27]:
{k:utf2asc(k) for k in selected_st.columns.tolist()}
Out[27]:
{'区站号': '&#21306;&#31449;&#21495;',
 '气压传感器拔海高度(米)': '&#27668;&#21387;&#20256;&#24863;&#22120;&#25300;&#28023;&#39640;&#24230;&#65288;&#31859;&#65289;',
 '省份': '&#30465;&#20221;',
 '站名': '&#31449;&#21517;',
 '纬度': '&#32428;&#24230;',
 '经度': '&#32463;&#24230;',
 '观测场拔海高度(米)': '&#35266;&#27979;&#22330;&#25300;&#28023;&#39640;&#24230;&#65288;&#31859;&#65289;',
 '距离雄县(km)': '&#36317;&#31163;&#38596;&#21439;(km)'}

popup中的HTML,以站名、站号、观测场海拔的顺序构建列表,注意看<tr>部分

In [28]:
table = """
<!DOCTYPE html>
<html>
<head>
<style>
table {{
    width:100%;
}}
table, th, td {{
    border: 1px solid black;
    border-collapse: collapse;
}}
th, td {{
    padding: 5px;
    text-align: left;
}}
table#t01 tr:nth-child(odd) {{
    background-color: #eee;
}}
table#t01 tr:nth-child(even) {{
   background-color:#fff;
}}
</style>
</head>
<body>

<table id="t01">
  <tr>
    <td>&#31449;&#21517;</td>
    <td>{}</td>
  </tr>
  <tr>
    <td>&#21306;&#31449;&#21495;</td>
    <td>{}</td>
  </tr>
  <tr>
    <td>&#35266;&#27979;&#22330;&#25300;&#28023;&#39640;&#24230;&#65288;&#31859;&#65289;</td>
    <td>{}</td>
  </tr>
</table>
</body>
</html>
""".format

循环列表各行,将信息添加进地图:

In [29]:
for k,v in selected_st.iterrows():
    
    iframe = IFrame(html=table(utf2asc(v['站名']),utf2asc(v['区站号']),utf2asc(v['观测场拔海高度(米)']))
                    ,width=310,height=130)
    popup = folium.Popup(iframe,max_width=400)
    
    folium.Marker(
        location=[v.纬度,v.经度],
        popup=popup
        ).add_to(xmap);
In [30]:
xmap
Out[30]:
In [ ]:
 

注释