Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

緯度・経度情報からMap上に位置をプロットしてみる

先日住所から緯度・経度情報を収集する方法をまとめました。
elsammit-beginnerblg.hatenablog.com

緯度・経度を取得出来ても数値だけだと、どこなのかピンときませんよね。。。
googleMapで緯度・経度を入力すれば地図上にピンが立って分かりやすいですよね!!

今回は取得した緯度・経度情報から自動でピンを立てて、地図上で分かりやすく表示させてみたいと思います!!
※今回も前回と同様にGoogle Mapは利用しない方法で実現させてみたいと思います。


■環境

コードはpython3(Python 3.7.3)を利用します。
また、住所から緯度・経度を取得するためのライブラリは前回と同様に、

https://msearch.gsi.go.jp/address-search/AddressSearch?q=" 

を用います。

■前提条件

今回のメインとなる緯度・経度からMap上にピンを配置する方法として、
folium
を用います。

【foliumtoとは】
leaflet.jsというjavascriptで使用することのできるマップをPythonライブラリ化したものです。
これにより、Pythonで簡易的にマップ上にピンを立てることが出来ます。
ライセンスはMITです。
下記に詳細がまとめられておりますので詳しくはこちらをご覧ください。
 ・documentation:https://python-visualization.github.io/folium/
 ・GitHubhttps://github.com/python-visualization/folium

本ライブラリですが、

pip3 install folium

でインストール可能です。

■foliumを用いたMap上へのプロット

緯度・経度が分かっている場合、foliumを用いることでたった数行でマップ上にプロット可能です。

import folium

map = folium.Map(location=[Latitude Number, Longitude Number], zoom_start=18)
folium.Marker(location=[Latitude Number, Longitude Number], popup=Name).add_to(map)
map.save("result.html")

folium.Map関数により引数として渡した緯度・経度を中心とした地図情報を取得します。
zoom_startは地図を描画する際の倍率になります。
今回は18倍をセットしてみました。

次に、

folium.Marker(location=[Latitude Number, Longitude Number], popup=Name).add_to(map)

により、先ほど取得した地図情報上にピンを立てます。
popupにはピンをクリックした際に表示される情報を指します。

例えば、東京駅(緯度:35.681561、経度:139.767197)の場合はこちらのようなコードになります。

import folium

map = folium.Map(location=[35.681561, 139.767197], zoom_start=18)
folium.Marker(location=[35.681561, 139.767197], popup="東京駅").add_to(map)
map.save("result.html")

こちらを実行するとresult.htmlファイルが生成されるかと思います。
こちらを開くと
f:id:Elsammit:20210723163350p:plain

といった画像が得られるかと思います。

数行で位置をプロット出来るのは便利ですね。

■住所リストからMap上にプロットをしてみる

では前回の住所リストから緯度・経度リストを取得するコードからMap上にプロットするコードはこちら。

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

import csv
import sys
import urllib.parse # pip3 install urllib3
import json         # https://note.com/masato1230/n/nba86746179ca
import requests     # pip3 install requests
import time
import os
import sqlite3
import folium

def main():
    args = sys.argv     # 実行時の引数取得
    if len(args) < 2:   # 実行時にcsvファイルを指定していなかった場合はエラーとする
        print("[Error] csvファイルを引数に与えてください")
        return

    if os.path.exists(args[1]) == False:        # 指定したファイルが存在しない場合にはエラーとする
        print("[Error] 指定したファイルがみつかりませんでした")
        return        

    fileName = os.path.splitext(args[1])        # 指定したファイルがcsvファイルでなければエラーとする
    if fileName[1] != ".csv":
        print("拡張子はcsvファイルにしてください")
        return

    i = 0               # ヘッダーとbody切り替え用
    AddressNum = -1     # 住所 or Addressが書かれた番号
    locationNum = -1
    locationName = ""
    MapLists = []       # csvから読み出しかつ緯度・経度を追加する用変数

    # 国土地理院URL
    # APIの使用方法等は下記を参考のこと
    # https://libraries.io/github/gsi-cyberjapan/internal-search
    makeUrl = "https://msearch.gsi.go.jp/address-search/AddressSearch?q="    
    map = folium.Map(location=[35.681561, 139.767197], zoom_start=8)
    with open(args[1],encoding='utf-8') as f:   # 引数に与えたcsvファイル読み込み
        reader = csv.reader(f)
        for line in reader:                     # 各行読み取り
            buf = []
            if i == 0:                          # ヘッダーの場合
                j = 0                           # 列番号カウント用変数
                for item in line:               # 列読み取り
                    if item == "住所" or item == "Address" or item == "address":
                        AddressNum = j          # 住所を見つけたらその時の番号を変数に格納
                    elif item == "店名" or item == "場所名":
                        locationNum = j
                    j+=1                        # 列番号カウンタをインクリメント
                    buf.append(item)            # 取得した列をbufに格納
                if AddressNum < 0:              # 住所やアドレスがなかったらエラーとして修了
                    print("[Error] 住所もしくはAddressが項目にありません")
                    return

                buf.append("latitude")          # ヘッダーの列にlatitudeを追加
                buf.append("longitude")         # ヘッダーの列にlongitudeを追加
                MapLists.append(buf)            # ヘッダー行をMapListsに格納
            else:                               # body側の各行情報
                for item in line:               # 各行ごとのカラムを取得
                    buf.append(item)            # 取得したカラムを変数に格納
                print(line[AddressNum])
                s_quote = urllib.parse.quote(line[AddressNum])          # 住所の文字列をURLエンコード
                response = response = requests.get(makeUrl + s_quote)   # エンコードした文字列を国土地理院APIの引数として与えてget request
                if response.json() == []:                               # レスポンスされたjsonデータの中身を確認し空だったら
                    print("[Error] 住所がよくわかりませんでした")          # 判定できなかった旨を出力し緯度・経度は空文字を格納
                    buf.append("")                                      
                    buf.append("")                    
                elif len(response.json()) >1:                           # 候補が複数あった場合、判定出来ないためスキップ
                    print("[Error] 住所の絞り込みが出来ず複数候補が出ました \n 住所の絞り込みをおこなってください ")
                    buf.append("")                                      
                    buf.append("") 
                else:                                                   # レスポンスされたjsonデータが空でなかった場合
                    print(response.json()[0]["geometry"]["coordinates"]) 
                    buf.append(response.json()[0]["geometry"]["coordinates"][0])    # 緯度情報をbufに格納
                    buf.append(response.json()[0]["geometry"]["coordinates"][1])    # 経度情報をbufに格納
                    if locationNum < 0:
                        locationName = ""
                    else:
                        locationName = line[locationNum]
                    
                    folium.Marker(location=[response.json()[0]["geometry"]["coordinates"][1], response.json()[0]["geometry"]["coordinates"][0]], popup=locationName).add_to(map)
                    
                MapLists.append(buf)            # 各行ごとの情報をMapListsに格納                                    
                time.sleep(1)                   # 国土地理院APIに負荷を掛けないように
            i+=1
    map.save("result.html")

    with open("output.csv", mode='w',newline='',encoding='shift_jis') as f: # データをcsvファイルへ格納
        writer = csv.writer(f)
        for maplist in MapLists:                # 各行の情報を格納した変数を読み出す
            writer.writerow(maplist)            # 読み出したデータをcsvへ書き込み
    print("Write Finish !!")
            
if __name__ == "__main__":
    main()

細かいところは割愛しますが、、、
前回から追加した点は、ファイル読み出し前に

map = folium.Map(location=[35.681561, 139.767197], zoom_start=8)

でmapを定義し、
map上に取得した緯度・経度のプロットを

folium.Marker(location=[response.json()[0]["geometry"]["coordinates"][1], response.json()[0]["geometry"]["coordinates"][0]], popup=locationName).add_to(map)

にてセット。
最後に、

map.save("result.html")

で保存しております。
住所から緯度・経度を取得するコードはこちらにまとめておりますのでこちらをご確認下さい。
elsammit-beginnerblg.hatenablog.com

例えばこちらのようなcsvを作成し、本コードを実行してみます。

id,店名,住所
1,札幌駅,北海道札幌市北区北6条西4丁目
2,東京駅,東京都千代田区丸の内1丁目
3,蒲郡駅,愛知県蒲郡市元町1

結果、result.htmlを開くとこちらのような画像が得られるかと思います。
f:id:Elsammit:20210723164438p:plain

■最後に

今回は緯度・経度からMap上に位置をプロットしてみました。
緯度・経度さえわかれば簡単にプロットが出来ますね!!
foliumはピンをあってるだけでなく、領域を塗ることが出来たり線が書けたりするみたいです。
まだまだ機能使いこなせていないので、もう少し調べて使ってみたいと思います!!