【Streamlit】アマチュア無線交信記録を地図上に可視化

Streamlit
この記事は約15分で読めます。

こんにちは、アマチュア無線局のJS2IIUです。
この記事では、StreamlitとPyDeckを使って、アマチュア無線の交信記録を地図上にプロットする方法を紹介します。交信記録はグリッドロケータで記録されていることを想定し、緯度経度に変換してArcで表示します。今回もよろしくお願いします。

st.pydeck_chart()の使い方

st.pydeck_chart()関数のシグネチャとパラメータは以下の通りです。

Python
st.pydeck_chart(pydeck_obj=None, *, use_container_width=False, width=None, height=None, selection_mode="single-object", on_select="ignore", key=None)
  • pydeck_obj: PyDeckのDeckオブジェクトを指定します。
  • use_container_width: チャートの幅をコンテナの幅に合わせるかを指定します。Trueに設定すると、チャートの幅がコンテナの幅に合わせられます。デフォルトはFalseです。
  • width: チャートの幅をピクセルで指定します。use_container_widthTrueの場合は無視されます。
  • height: チャートの高さをピクセルで指定します。
  • selection_mode: 地図上のオブジェクトの選択モードを指定します。"single-object" (単一選択)、"multi-object" (複数選択)、"none" (選択不可) から選択できます。デフォルトは"single-object"です。
  • on_select: オブジェクトが選択されたときに実行するコールバック関数を指定します。"ignore" (無視)、"callback" (コールバック関数を実行) から選択できます。デフォルトは"ignore"です。
  • key: ウィジェットの識別子となる文字列を指定します。

さらに詳しい内容を知りたい方はこちらの記事を参照して下さい。

交信状況表示プログラムの解説

プログラム全体を示します。その後に個別のプログラム要素について説明していきます。

Python
import streamlit as st
import pydeck


def alpha_to_lonlat(alpha):
    """Converts an alpha character to longitude or latitude."""
    if type(alpha) is not str:
        raise ValueError('Invalid alpha type')

    return (ord(alpha) - 65) * 20 - 180


def alpha_to_sub(alpha):
    """Converts an alpha character to sub square coefficients."""
    if type(alpha) is not str:
        raise ValueError('Invalid alpha type')

    return (ord(alpha) - 65 + 0.5) / 12


def gl_to_latlon(gridlocator):
    """Converts a grid locator to latitude and longitude."""
    if len(gridlocator) < 4 or len(gridlocator) % 2 != 0:
        return None

    gridlocator = gridlocator.upper()

    # South west corner of the FIELD - first two characters
    lon = alpha_to_lonlat(gridlocator[0])
    lat = alpha_to_lonlat(gridlocator[1]) / 2

    if len(gridlocator) < 4:
        lon += 10
        lat += 5
    elif len(gridlocator) < 6:
        # Square - next two characters
        lon += int(gridlocator[2]) * 2 + 1
        lat += int(gridlocator[3]) * 1 + 0.5
    elif len(gridlocator) == 6:
        # subsquare - next two characters
        lon += int(gridlocator[2]) * 2 + alpha_to_sub(gridlocator[4])
        lat += int(gridlocator[3]) * 1 + alpha_to_sub(gridlocator[5]) / 2

    return [lon, lat]


# Define a layer with ArcLayer
#  https://deckgl.readthedocs.io/en/latest/gallery/arc_layer.html

qso_data = [
        {"origin": 'PM85', "destination": 'PM97ej'},
        {"origin": 'PM85', "destination": 'PM64ff'},
        {"origin": 'PM85', "destination": 'PM63ih'},
        {"origin": 'PM85', "destination": 'QM09jj'},
        {"origin": 'PM85', "destination": 'QN00ef'},
        {"origin": 'PM85', "destination": 'PM95wq'},
        {"origin": 'PM85', "destination": 'PM95tw'},
    ]

# apply gl_to_latlon to each dictionary in qso_data then reassign to qso_data
for d in qso_data:
    d['origin'] = gl_to_latlon(d['origin'])
    d['destination'] = gl_to_latlon(d['destination'])
qso_data = [d for d in qso_data if d['origin'] is not None and d['destination'] is not None]


layer = pydeck.Layer(
    "ArcLayer",
    data=qso_data,
    get_source_position="origin",
    get_target_position="destination",
    get_source_color=[255, 255, 140],
    get_target_color=[255, 165, 0],
    auto_highlight=True,
    pickable=True,
    radius=100,
    width_scale=0.00001,
    width_min_pixels=1,
    width_max_pixels=3,
)

# Set the viewport location
view_state = pydeck.ViewState(
    latitude=36,
    longitude=136.0,
    zoom=4.5,
    bearing=0,
    pitch=40,
)

# Render
r = pydeck.Deck(layers=[layer], initial_view_state=view_state)

st.header('QSO data on 2025/01/01')
st.pydeck_chart(r)

必要なライブラリのインポート

Python
import streamlit as st
import pydeck
  • streamlit : Streamlitは、PythonでインタラクティブなWebアプリケーションを構築するためのライブラリです。
  • pydeck : PyDeckは、Deck.glをPythonで使えるようにしたライブラリで、地図上に様々なデータを可視化することができます。

グリッドロケータを緯度経度に変換する関数

グリッドロケータがどのような形で定義されているのか、についてはJARLの解説ページを参考にして下さい。

グリッドロケーター
Python
def alpha_to_lonlat(alpha):
    """Converts an alpha character to longitude or latitude."""
    if type(alpha) is not str:
        raise ValueError('Invalid alpha type')

    return (ord(alpha) - 65) * 20 - 180


def alpha_to_sub(alpha):
    """Converts an alpha character to sub square coefficients."""
    if type(alpha) is not str:
        raise ValueError('Invalid alpha type')

    return (ord(alpha) - 65 + 0.5) / 12


def gl_to_latlon(gridlocator):
    """Converts a grid locator to latitude and longitude."""
    if len(gridlocator) < 4 or len(gridlocator) % 2 != 0:
        return None

    gridlocator = gridlocator.upper()

    # South west corner of the FIELD - first two characters
    lon = alpha_to_lonlat(gridlocator[0])
    lat = alpha_to_lonlat(gridlocator[1]) / 2

    if len(gridlocator) < 4:
        lon += 10
        lat += 5
    elif len(gridlocator) < 6:
        # Square - next two characters
        lon += int(gridlocator[2]) * 2 + 1
        lat += int(gridlocator[3]) * 1 + 0.5
    elif len(gridlocator) == 6:
        # subsquare - next two characters
        lon += int(gridlocator[2]) * 2 + alpha_to_sub(gridlocator[4])
        lat += int(gridlocator[3]) * 1 + alpha_to_sub(gridlocator[5]) / 2

    return [lon, lat]
  • alpha_to_lonlat(alpha) : グリッドロケータのアルファベット部分を経度または緯度の計算に使う変数に変換します。
  • alpha_to_sub(alpha) : グリッドロケータのサブスクエアのアルファベット部分を経度または緯度の計算に使う変数に変換します。
  • gl_to_latlon(gridlocator) : グリッドロケータ全体を緯度経度に変換します。

交信記録データ

データの形式は色々なパターンがあると思います。紙の交信ログから転記する場合もあれば、ADIFから変換する場合もあるかもしれません。ADIFからプログラムを使って変換したい方は、adiftoolsというPython向けのパッケージをご活用ください。

Python
qso_data = [
        {"origin": 'PM85', "destination": 'PM97ej'},
        {"origin": 'PM85', "destination": 'PM64ff'},
        {"origin": 'PM85', "destination": 'PM63ih'},
        {"origin": 'PM85', "destination": 'QM09jj'},
        {"origin": 'PM85', "destination": 'QN00ef'},
        {"origin": 'PM85', "destination": 'PM95wq'},
        {"origin": 'PM85', "destination": 'PM95tw'},
    ]

# apply gl_to_latlon to each dictionary in qso_data then reassign to qso_data
for d in qso_data:
    d['origin'] = gl_to_latlon(d['origin'])
    d['destination'] = gl_to_latlon(d['destination'])
qso_data = [d for d in qso_data if d['origin'] is not None and d['destination'] is not None]
  • qso_data : アマチュア無線の交信記録をリストとして定義します。各要素は辞書で、origin (発信地) と destination (宛先) をグリッドロケータで指定します。
  • gl_to_latlon 関数を使って、グリッドロケータを緯度経度に変換します。

PyDeckのレイヤー設定

Python
layer = pydeck.Layer(
    "ArcLayer",
    data=qso_data,
    get_source_position="origin",
    get_target_position="destination",
    get_source_color=[255, 255, 140],
    get_target_color=[255, 165, 0],
    auto_highlight=True,
    pickable=True,
    radius=100,
    width_scale=0.00001,
    width_min_pixels=1,
    width_max_pixels=3,
)
  • ArcLayer を使って、発信地と宛先を弧で繋いで表示します。
  • get_source_positionget_target_position で、弧の始点と終点を指定します。
  • get_source_colorget_target_color で、弧の色を指定します。
  • auto_highlightTrue にすると、マウスオーバーで弧がハイライトされます。
  • pickableTrue にすると、弧をクリックして選択できます。
  • radiuswidth_scalewidth_min_pixelswidth_max_pixels で、弧の太さなどを調整します。

地図の表示設定

Python
# Set the viewport location
view_state = pydeck.ViewState(
    latitude=36,
    longitude=136.0,
    zoom=4.5,
    bearing=0,
    pitch=40,
)

# Render
r = pydeck.Deck(layers=[layer], initial_view_state=view_state)

st.header('QSO data on 2025/01/01')
st.pydeck_chart(r)
  • ViewState で、地図の初期表示位置、ズームレベル、回転角度などを設定します。
  • Deck で、レイヤーと表示設定をまとめて r に格納します。
  • st.header で、Streamlitアプリにヘッダーを表示します。
  • st.pydeck_chart(r) で、PyDeckの地図をStreamlitアプリに表示します。

まとめ

この記事では、StreamlitとPyDeckを使ってアマチュア無線の交信記録を地図上に可視化する方法を紹介しました。グリッドロケータを緯度経度に変換し、ArcLayerを使って交信の様子をわかりやすく表示することができました。

参考になるWEBサイト

Pythonに関する書籍の<PR>です。

24年9月に出版された「ハイパーモダンPython-信頼性の高いワークフローを構築するモダンテクニック」、Claudio Jolowicz著、嶋田、鈴木訳。開発環境の構築、プロジェクトの管理、テストに関して実践的な内容でとても参考になる一冊です。

最後まで読んでいただきありがとうございました。

コメント

タイトルとURLをコピーしました