2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

QtAdvent Calendar 2024

Day 23

PyQtとfoliumによる地図表示GUI

Last updated at Posted at 2025-01-03

この記事ではfoliumで書いた地図をPyQtで表示する方法について説明します。

はじめに

foliumはPythonで地図を表示させるためにとても使いやすくて人気なライブラリです。

foliumの仕組みは主にhtmlとJavaScriptコードを生成することです。ただし表示するために普段はjupyterで実行したり、ファイルに保存してブラウザで開けたりするという使い方が多いようですが、その他にも直接GUIを作ってそこで操作しながら表示させることもできます。

PyQtはPythonでGUIを作るのに人気で使い勝手がいいライブラリです。そしてfoliumで描く地図と一緒に使うこともできます。

ということで私は色々試して大体の書き方をこの記事に纏めることにしました。

なお、foliumの基本的な使い方はqiitaにも色んな記事があるので割愛します。私がfoliumの勉強で参考になった記事は全部記事の下の方に載っています。

PyQtに関しては私の以前書いた記事を参考に。

今でもPyQt5を使っている人も多いようですが、今回使うのはPyQt6にします。もしPyQt5を使う場合書き方は少しだけ違いますが、その違いについても私の記事を参考にして書き換えたらいいです。

準備

まず使うライブリのインストールです。今回の主役はPyQt6とfoliumですが、数字データを手軽に扱うためにnumpyも使います。そしてポリゴンデータが保存されている.shpファイルを読み込むためにpyshp(importの時はshapefile)というライブラリも必要です。

どれも簡単にpipでインストールできます。

pip install pyqt6 folium pyshp numpy

PyQtの中にfoliumを入れる方法

PyQtにはQWebEngineViewというウェジェットがあって、これにhtmlを入れることでウェブページを表示することができます。ということでfoliumで生成したhtmlをQWebEngineViewに入れたら地図を表示することができます。

まずは簡単な使う例を見てみましょう。

import sys
from PyQt6.QtWidgets import QApplication,QWidget
from PyQt6.QtWebEngineWidgets import QWebEngineView
import folium

class Qmado(QWidget):
    def __init__(self):
        super().__init__()
        # 地図のウィジェットを準備
        self.fowg = QWebEngineView(self)
        self.fowg.setGeometry(10,10,600,450) # 位置設定
        # ウィジェットの中で地図を表示させる
        self.chizu_hyouji()
    
    def chizu_hyouji(self):
        # 地図を設定
        foma = folium.Map(
            location=[33.9583,130.9688],
            zoom_start=15
        )
        
        # 指定した場所にマーカーを入れる
        folium.Marker(
            location=[33.9609, 130.9622],
            popup=folium.Popup('和布刈神社',show=True),
            icon=folium.Icon(color='darkred')
        ).add_to(foma)
        
        # htmlを生成する
        html = foma.get_root().render()
        
        # foliumによって生成されたhtmlを入れる
        self.fowg.setHtml(html)

qAp = QApplication(sys.argv)
qmado = Qmado()
qmado.show()
qAp.exec()

実行したらこのように九州最北端の辺りの地図と和布刈神社を指すマーカーが現れますね。因みに中心は山頂にある門司城跡に設定しています。あそこには石碑くらいしか残っていないのですが、関門海峡を見渡せる景色はいいですね。

qpf01.jpg

尚、わざわざ地図を描く部分のコードだけ個別のメソッドにしたのはわかりやすくするためだけでなく、更新がある時にこの関数の部分だけもう一度実行することになるからです。詳しくは次に説明します。

GUIの設定値によって書き換えていく地図

わざわざGUIにするというのは、やはりクリックしたりドラッグしたりすることによって地図を操作したいですね。ではその例を見てみましょう。

import sys
from PyQt6.QtWidgets import QApplication,QWidget,QHBoxLayout,QSlider
from PyQt6.QtWebEngineWidgets import QWebEngineView
import folium

class Qmado(QWidget):
    def __init__(self):
        super().__init__()
        self.hankei = 500
        hbl = QHBoxLayout()
        self.setLayout(hbl)
        
        # 地図のウィジェット
        self.fowg = QWebEngineView(self)
        hbl.addWidget(self.fowg)
        self.fowg.setFixedSize(480,400)
        
        # スライダーのウィジェット
        self.qslider = QSlider()
        hbl.addWidget(self.qslider)
        self.qslider.setRange(10,1000)
        self.qslider.valueChanged.connect(self.henka)
        self.qslider.setValue(self.hankei)
    
    # スライダーの値が変わったら起動する関数
    def henka(self,x):
        self.hankei = x # 新しい値
        self.chizu_hyouji() # 地図を更新する
    
    def chizu_hyouji(self):
        latlng = [33.5898,130.4207]
        # 地図を設定
        foma = folium.Map(
            location=latlng,
            zoom_start=15
        )
        
        tip = f'博多駅から半径{self.hankei}メートル以内'
        tooltip = folium.Tooltip(tip,style='font-size: 15px;',permanent=True)
        # マーカーを入れる
        folium.Circle(
            location=latlng,
            radius=self.hankei, # スライダーの値から半径
            tooltip=tooltip,
            color='#332299',
            fill_color='#33BB77'
        ).add_to(foma)
        
        # foliumによって生成されたhtmlを入れる
        self.fowg.setHtml(foma.get_root().render())

qAp = QApplication(sys.argv)
qmado = Qmado()
qmado.show()
qAp.exec()

このように、博多駅周辺を表す地図ができて、右側のスライダーを調整することで円の半径が変わります。このような方法を使ってある場所からの距離を見積もることができますね。

qpf02.jpg

geojsonを読み込んで表示する

国や都道府県を市区町村の境界線を示すポリゴンデータはgeojsonというjsonの形で保存することが多いです。

それを読み込むにはgeojsonというライブラリがありますが、これを使わなくても不通に標準のjsonライブラリで読み込むことができます。

今回はここの市区町村データを使います。

これをダウンロードしてgeojson/40フォルダの中の40203.jsonを読み込んで福岡県久留米市の境界線を表示してみます。

import sys,json
from PyQt6.QtWidgets import QApplication,QWidget,QLabel,QVBoxLayout
from PyQt6.QtWebEngineWidgets import QWebEngineView
import folium
import numpy as np

class Qmado(QWidget):
    def __init__(self):
        super().__init__()
        # geojsonのデータを読み込む
        self.jsondata = json.load(open('JapanCityGeoJson-master/geojson/40/40203.json'))
        
        vbl = QVBoxLayout()
        self.setLayout(vbl)
        
        # 属性の値を表示
        vbl.addWidget(QLabel(' '.join(self.jsondata['features'][0]['properties'].values())))
        self.setStyleSheet('font-family: Kaiti SC; font-size: 18px; text-align: center')
        
        self.fowg = QWebEngineView(self)
        vbl.addWidget(self.fowg)
        self.fowg.setFixedSize(600,450)
        self.chizu_hyouji()
    
    def chizu_hyouji(self):
        # ポリゴンデータを取得
        pt = np.array(self.jsondata['features'][0]['geometry']['coordinates'][0][0])[:,::-1]
        # 中心とズームのレベルを決めるために極点を求める
        ptmin = pt.min(0)
        ptmax = pt.max(0)
        
        foma = folium.Map(
            location=(ptmin+ptmax)/2,
            zoom_start=9-np.log2((ptmax-ptmin).max())
        )
        
        folium.Polygon(
            locations=pt,
            color='blue',
            fill_color='violet',
            fill_opacity=0.3,
            weight=2,
        ).add_to(foma)
        
        self.fowg.setHtml(foma.get_root().render())

qAp = QApplication(sys.argv)
qmado = Qmado()
qmado.show()
qAp.exec()

qpf03.jpg

尚、保存されている座標は「経度、緯度」の順ですが、foliumの入力は「緯度、経度」なので、逆にする必要があります。そこでnumpyを使うと便利です。

.shpのシェープデータを読み込んで表示する

地理情報システム(GIS)のデータは.shpファイルの形で保存されることが多いです。geojsonと違って.shpファイルはバイナリファイルなので、直接読み込むことはできません。QGISArcGISなどのGISソフトでは簡単に開けて使えますが、Pythonでも読み込めるライブラリが色々あります。ここではpyshpを使います。

今回はe-Statのデータを使います。ここで「40000 福岡県全域」をダウンロードします。

その中は.shpを含め色んなファイルがありますが、直接読み込むのは.shpだけで、他は.shpを読み込む時一緒に使われます。

このデータを使って福岡市各区の町丁・字の境界線を表示するGUIを作ってみます。

import sys
from PyQt6.QtWidgets import QApplication,QWidget,QButtonGroup,QRadioButton,QVBoxLayout,QHBoxLayout
from PyQt6.QtWebEngineWidgets import QWebEngineView
import folium
import shapefile
import numpy as np

lis_ku = ['東区','博多区','中央区','南区','西区','城南区','早良区']

class Qmado(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('福岡市の町丁・字')
        self.data_junbi()
        
        vbl = QVBoxLayout()
        self.setLayout(vbl)
        
        hbl = QHBoxLayout()
        vbl.addLayout(hbl)
        
        # 区を選択するラジオボタン
        self.sentakushi = QButtonGroup()
        for ku in lis_ku:
            rbtn = QRadioButton(ku)
            hbl.addWidget(rbtn)
            self.sentakushi.addButton(rbtn)
        
        # 地図のウェジェット
        self.fowg = QWebEngineView(self)
        vbl.addWidget(self.fowg)
        self.fowg.setFixedSize(500,500)
        
        self.sentakushi.buttonToggled.connect(self.erandara)
        self.sentakushi.buttons()[1].setChecked(True)
    
    # 使うポリゴンデータを準備しておく
    def data_junbi(self):
        # ポリゴンデータを取得
        sf = shapefile.Reader('A002005212015DDSWC40/h27ka40.shp',encoding='cp932')
        self.lis_ptrc = []
        for shp,rc in zip(sf.shapes(),sf.records()):
            if(rc[16]=='福岡市'):
                pt = np.array(shp.points)[:,::-1]
                self.lis_ptrc.append([pt,rc])
    
    # 区を選んだら起動する関数
    def erandara(self,rbtn):
        self.eranda_ku = rbtn.text()
        self.chizu_hyouji()
    
    # 選んだ区の地図を表示する
    def chizu_hyouji(self):
        lis_pt = []
        lis_namae = []
        for pt,rc in self.lis_ptrc:
            if(rc[5]==self.eranda_ku):
                lis_pt.append(pt)
                namae = rc[6]
                if(not namae):
                    namae = None
                lis_namae.append(namae)
        
        pt = np.vstack(lis_pt)
        ptmin = pt.min(0)
        ptmax = pt.max(0)
        foma = folium.Map(
            location=(ptmin+ptmax)/2,
            zoom_start=9-np.log2((ptmax-ptmin).max())
        )
        
        # 一つずつポリゴンを追加していく
        for pt,namae in zip(lis_pt,lis_namae):
            folium.Polygon(
                locations=pt,
                color='darkgreen',
                fill_color='blue',
                fill_opacity=0.3,
                weight=1.5,
                tooltip=namae
            ).add_to(foma)
        
        self.fowg.setHtml(foma.get_root().render())

qAp = QApplication(sys.argv)
qmado = Qmado()
qmado.show()
qAp.exec()

このように博多区の町丁・字の分け方が見えて、マウスを置いたら名前が表示されます。他の区を選んで切り替えられます。

qpf04.jpg

地図のイベントをPyQt側で受け取って使う方法

PyQtのQWebEngineViewはただfoliumによって作成された地図のウェブページを表示させるので、地図の中に起きた行動は直接PyQtのGUIの方に影響を与えることができません。

それでも調べてみたら、かなり面倒くさいですが方法があります。QWebChannelを通じてJavaScriptで起こされたイベントを受け取るという方法です。

コードはかなり複雑でわかりにくくなりますが、仕方ないですね。

例としてポップアップの中にボタンを入れて、そのボタンを押したらGUIの中で場所の名前を表示させるように実装してみます。

import sys
from PyQt6.QtWidgets import QApplication,QWidget,QVBoxLayout,QLabel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import QObject,pyqtSlot
from PyQt6.QtWebChannel import QWebChannel
import folium

class Qmado(QWidget):
    def __init__(self):
        super().__init__()
        
        vbl = QVBoxLayout()
        self.setLayout(vbl)
        
        self.label = QLabel('')
        vbl.addWidget(self.label)
        
        self.fowg = QWebEngineView(self)
        vbl.addWidget(self.fowg)
        self.fowg.setFixedSize(500,400)
        
        # 繋ぐためのチャンネル
        self.channel = QWebChannel()
        self.channel.registerObject('event_obj',EventObj(self))
        self.fowg.page().setWebChannel(self.channel)
        
        self.chizu_hyouji()
    
    def chizu_hyouji(self):
        foma = folium.Map(
            location=(33.589,130.386),
            zoom_start=15
        )
        lis_basho = [
            ('福岡城',(33.5844,130.3832)),
            ('鴻臚館',(33.5860,130.3853)),
            ('赤坂駅',(33.5891,130.3907)),
        ]
        for basho,latlng in lis_basho:
            # ボタンが入っているポップアップ
            popup = folium.Popup(f'<button onclick="btnclick(\'{basho}\')">{basho}</button>',show=True)
            folium.Marker(
                location=latlng,
                popup=popup,
            ).add_to(foma)
            
            html = foma.get_root().render()
            # ボタンのイベントをQt側に送るためのスクリプト
            html += '''
            <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
            <script>
            btnclick = (s)=>{
                new QWebChannel(qt.webChannelTransport, (channel)=>{
                    channel.objects.event_obj.btnclick(s);
                });
            }
            </script>
            '''
        
        self.fowg.setHtml(html)

class EventObj(QObject):
    @pyqtSlot(str)
    def btnclick(self,s):
        self.parent().label.setText(f'{s}」が選択された')
        

qAp = QApplication(sys.argv)
qmado = Qmado()
qmado.show()
qAp.exec()

qpf05.jpg

このようにポップアップの中にJavaScriptを入れて、個別でJavaScript関数の定義も後ろに追加します。更にQObjectを継承するクラスを定義してその中に実行したいメソッドを書くのです。

もっと簡単な方法があればいいのですが、調べたところこれだけでも精一杯みたいです。

マーカーやポリゴンをクリックする時に発動する関数を作ることもできますが、それはかなり遠回しな書き方で更に不気味に見えるなので、これくらいにしておきます。

逆にPyQt GUIの方から地図なイベントを送る方法は、残念ながら今のところ思いつきません。GUIの操作で地図を書き直すことができますが、描いた地図を直接更新することはできません。つまりGUIによる地図の操作は毎回書き直すという形で更新するしかないのです。

終わりに

こんな感じでPyQtとfoliumで地図を弄って色んな処理をするGUIを作ることができます。ただし地図とGUIの間のイベントのやり取りが難しいので、複雑なことをやりたい場合は限界を感じます。

そもそもfoliumの中身はただleafletというJavaScriptライブラリです。foliumでleafletのコードは自動的に生成されますが、全部のleafletの機能が使えるわけではありません。本格的に使いたい場合はやはりfoliumを使うよりも直接leafletで書いた方がいいかもしれません。

それでも簡単な操作だけならfoliumを使うと手軽にPythonで書けるから便利です。

参考

folium

pyshp

PyQt

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?