🚙

OR-Tools のルート最適化アプリを Bokeh + Heroku でデプロイしてみた

2022/01/28に公開

前回 Bokeh + Heroku でアプリをデプロイする記事を書きました。

https://zenn.dev/megane_otoko/articles/078_bokeh_deploy_on_heroku

もう少し工夫して、 OR-Tools でクリックした位置をドットで表示し、ドットをスライダーバーで指定した車両(アプリ中では背景に世界地図を使っているので飛行機と表現)の台数で巡回するルートを計算させるアプリを作成しました。

Bokeh は StreamlitPyWebIO よりも取り回しが難しい分、クリック や ダブルクリックイベントを認識できる等、かなり自由度が高い印象です。

作成したアプリはこちら

コードはこちら

ファイル構成

Bokeh の実装は app.py に集約しています。

今回は 最適化の実装部分は optimize.py に集約していますが、その辺はお好みで。

|-- app.py           # 主に Bokeh による UI を実装した実行ファイル
|-- optimize.py      # 最適化部分のライブラリ
|-- Procfile
|-- runtime.txt
|-- requirements.txt
|-- static           # 画像を保存するフォルダ

Procfile や runtime.txt 、requirements.txt については手前味噌ですが前回のブログをご参照ください。
https://zenn.dev/megane_otoko/articles/078_bokeh_deploy_on_heroku

クリック等のイベント処理

クリックした際の座標を集約するため ColumnDataSource でインスタンスを作成します。

callback(event)関数内にクリックした際実行したい内容、今回であれば list内にクリックした箇所の座標を追加し、その内容を先ほど作成した ColumnDataSource で作成したインスタンスに指定したりします。

ちょっと気持ち悪いですが、座標を格納する list、coord は callbak(event)内で global変数とするようです。

plot.on_event(bokehのメソッド, 作成した callback関数) とすることでイベントを認識させることができるようです。

クリックの場合は下記のように Tap、ダブルクリックの場合は DoubleTap、ボタンのクリックの場合は ButtonClick などを使います。

https://docs.bokeh.org/en/latest/docs/reference/events.html

from bokeh.events import Tap

coord = []
source = ColumnDataSource(data=dict(x=[], y=[]))

def callback_click(event):
    global coord_list    
    coord=(event.x,event.y)  # クリックした位置の座標をタプルに保存
    coord_list.append(coord) # 座標をリストに保存
    source.data = dict(      # 他の機能で共通して使えるよう座標を souse に保存
              x=[i[0] for i in coord_list], 
              y=[i[1] for i in coord_list]
            )

plot.on_event(Tap, callback_click)

https://stackoverflow.com/questions/35974133/adding-a-point-to-a-bokeh-plot-on-click

スライダーバー

飛行機の台数を自由に変えられるよう、スライダーバーを使っています。

ラジオボタンやチェックボックス等、他のヴィジェットを使う場合は下記を参照ください。

https://docs.bokeh.org/en/2.4.0/docs/user_guide/interaction/widgets.html

from bokeh.models import Slider

slider = Slider(
    title="飛行機の数", # タイトル
    start=1,            # スライダーバーの数値の最小値             
    end=10,             # スライダーバーの数値の最大値
    value=3,            # スライダーバーの数値の初期値
    step=1,             # スライダーバーの数値の間隔
  )

slider.js_on_change("value", CustomJS(code="""
    console.log('slider: value=' + this.value, this.toString())
"""))

スライダーバーで決定した数値は slider.value で取得できます。

https://docs.bokeh.org/en/2.4.0/docs/gallery/slider.html

最適化実行ボタンを押すと最適化を実行し、その結果を矢印線で表示するよう、ボタンイベント用の callback 関数内に 線と矢印を表示する Bokeh のライブラリを仕込んでいます。

plot = figure(
            title='任意のタイトル',
            tools="tap", 
            height=xxx, # 任意の数値を指定 
            width=xxx,  # 同上
            x_range=(xxx, xxx), # 任意の数値の範囲をタプルで指定
            y_range=(xxx, xxx), # 同上
          )

def callback_button():
  
    ()      
      # 各ルートの軌跡をラインで表示。次の矢印でも軌跡は表示できるが、凡例を作成するために使用。
      plot.line(
        np.array(all_route_result_coodList[i])[:, 0],
        np.array(all_route_result_coodList[i])[:, 1],
        legend_label=str(i),
        color=d3['Category10'][10][i]
      )
      
      # 各ルートの方向を矢印として表示
      for j in range(len(all_route_result_coodList[i])-1):
        
        plot.add_layout(
                Arrow(
                      end=VeeHead(line_color="firebrick", line_width=0.1),
                      x_start=all_route_result_coodList[i][j][0],
                      y_start=all_route_result_coodList[i][j][1],
                      x_end=all_route_result_coodList[i][j+1][0],
                      y_end=all_route_result_coodList[i][j+1][1],
                      line_color=d3['Category10'][10][i],
                    )
        )
        
        plot.add_layout(plot.legend[0], 'right') # 凡例の追加

上記でも触れていますが、色は bokeh.palletes のメソッドで指定することができます。

from bokeh.palettes import d3

d3['Category10']

以下のように、出力したい色の数:(カラーコードのタプル)、という辞書型データとなっているようです。

出力:

{3: ('#1f77b4', '#ff7f0e', '#2ca02c'),
 4: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'),
 5: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd'),
 6: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b'),
 7: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2'),
 8: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f'),
 9: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22'),
 10: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf')}

以下のように指定すれば、線や図形に指定した色を使うことができます。

パレットの大分類["パレットの小分類"][出力したいカラーコードの数][使いたいカラーコードのインデックス]

具体的には以下のように使用します。

plot.line(
  (省略)
  color=d3['Category10'][10][0]
)

https://docs.bokeh.org/en/2.4.2/docs/reference/palettes.html?highlight=bokeh palettes#module-bokeh.palettes

https://docs.bokeh.org/en/2.4.1/docs/first_steps/first_steps_1.html

http://ja.dochub.org/bokeh/docs/user_guide/annotations.html

背景画像の表示

シンプルに画像を表示する関数がないようで苦労しましたが、こちらのサイトを参考にさせていただきました。

https://qiita.com/samacoba/items/922b428c6716f3285e11

# 画像ファイルの読込、PIL -> numpy への変換 
img_path = "static/4564885_m.jpg"
img_file = Image.open(img_path)
img_file = np.array(img_file)

# 画像表示の設定
img = np.empty((img_file.shape[0], img_file.shape[1]), dtype=np.uint32)
view = img.view(dtype=np.uint8).reshape((img_file.shape[0], img_file.shape[1], 4))
view[:, :, 0:3] = np.flipud(img_file[:, :, 0:3])#上下反転あり
view[:, :, 3] = 255

# 画像を表示
plot.image_rgba(
            image=[img],
            x=0, 
            y=0, 
            dw=img_file.shape[1], 
            dh=img_file.shape[0],
            global_alpha=0.3
            )

ルート最適化についてはほぼ OR-Tools のサンプルコードと変わらないので下記リンクを載せるのみで詳細は割愛します。

https://developers.google.com/optimization/routing/vrp

以上になります、最後までお読みいただきありがとうございました。

Discussion