ゆっくり開発

開発したい時に開発するブログ

Vue + D3 + Leaflet でヒートマップを描画する時に詰まったところ

概要

バイトの関係で Vue, D3, Leaflet を使ったヒートマップを作成することになった.なんとかできたけど色々手こずったので,それらをメモしとく.
下の画像は今回作成したヒートマップ.

f:id:uttnaoki:20180819024312p:plain

取り上げる項目

  • D3とLeafletのバージョン
  • jsonファイルの読み込み
  • this変数の扱い
  • 非同期処理の扱い

D3とLeafletのバージョン

D3とLeafletもバージョンでコードがかなり変わっており, 新しめなバージョンを使うと参考になるサイトがかなり少なくなる.
Leafletについては,普通にマップを描画するだけならあまり問題はないが, 今回のようにSVGを使う場合,ところどころコードが変わる.
具体的に,以下のバージョンでコードが変わる(最新=2018年8月時点の最新).

  • D3: v3とv4のあたり(最新はv5)
  • Leaflet: 0.7.0と0.8.0のあたり(最新は1.3.3)

新しいバージョンのものを使う場合,2014年あたりに書かれたサイトのコードはほとんど参考にならないと思っていい.

ちなみに,以下のサイトは,D3+Leafletで地理データを描画する時に参考にした.

また,Leafletの新しいバージョンではL.geoJSON().addTo()jsonを読み込み,描画できる.属性値(style)もゴリゴリいじれるので,D3で描画したようなヒートマップを描画できる.
D3を使うことにこだわりがないならLeaflet単体で実装した方が簡単. コードもごちゃごちゃしない.

jsonファイルの読み込み

発生した問題

  1. vue-cliコンパイル処理で jsonファイルに文句言われる
    jsonファイルをassetsなどのディレクトリに配置するとvue-cliコンパイル処理にエラーを吐かれる.コンパイルできない的なこと言われた気がする.
  2. d3.json()でファイルを読めなかった
    理由はよくわらん.

今回の対処方法

  1. jsonファイルをstaticディレクトリに置く
    ここにおいたらコンパイル処理されないっぽい.
  2. axios.get()を使う
    多分axiosじゃなくてもいいとは思う.ajaxみたいな適当なGET処理してくれるの使えばjsonファイルを読めると思う.

this変数の扱い

vueを使うとJSとHTML間で変数や関数を共有できて便利なのだが,その共有オブジェクトを使うために,this変数を使わなければならない.
例えば,flagという名前の変数を定義し,その変数を使おうと思ったら,以下の様にアクセスしなければならない.
this.flag = this.flag + 1; (flag変数をインクリメントする処理) しかし,JSでは色々な処理でthis変数が上書きされてしまい,上記のような処理を実行できなくなる.
これの対処方法として,thisが上書きされる直前にthisを退避させる方法がある.

例えとして,以下はaxios.get()でGETしたgeojsonデータをthis.geojsonに格納する例である.

const vueObj = this;
axios.get('../../../static/kurashiki.geojson')
  .then(function (json) {
    vueObj.geojson = json.data;
  });

これでもちゃんと動くが,axiosに対しては以下の様にbind()を使うことで,axios内でthisにアクセスできる.

axios.get('../../../static/kurashiki.geojson')
  .then(function (json) {
    this.geojson = json.data;
  }.bind(this));

非同期処理の扱い

ヒートマップを描画する前にデータ取得などの処理をする必要があり, そういったデータ取得処理は非同期に実行されてしまう. このため,特定の前処理が終わるまで他の処理を実行させないようにする必要がある.

今回以下のコードを使った

  • async
  • await
  • Promise.all()

詳しくは以下のサイトがわかりやすい.

qiita.com

ざっくり説明すると,関数を宣言する際,その関数名の前にasyncを付けるとその関数が非同期関数となる.返り値がPromiseになったり関数内でawaitを使えたりする.
awaitを使うことで非同期処理の実行中,他の処理を停止させられる.
以下は実際に使ってみた例.

async setGeojson () {
  await axios.get('../../../static/kurashiki.geojson')
    .then(function (json) {
      this.geojson = json.data;
    }.bind(this));
}

上記例では,非同期処理のaxios.get()の実行中,その他処理が止まる(たぶん).
上記のような非同期関数をまとめて実行し, それら全ての完了を待ちたい時に便利なのがPromise.all()
Promise.all()を使ったコードの例は以下の通り.

Promise.all([this.drawMap(), this.setGeojson(), this.setAreaValue()])
  .then(() => {
    this.drawFeatures()
  })

上記例では,this.drawMap(), this.setGeojson(), this.setAreaValue()の処理が全て完了した後,this.drawFeatures()を実行するようになっている.

ちなみに,上記で呼び出している関数の処理内容は以下の様になっている.

  • this.drawMap(): オープンストリートマップを描画する.
  • this.setGeojson(): geojson(地理データ)を読み込み,vueの変数に格納する.
  • this.setAreaValue(): 各エリアの値をvueの変数に格納する.この値を基に書くエリアの色の濃さを定義する.
  • this.drawFeatures(): ヒートマップを描画する.

今回のヒートマップ作成に対する感想

なんとかヒートマップを作成できた.非同期処理の扱いが前よりわかってきてなかなか勉強になった.
ただまだ理解しきれていないので,次に非同期処理問題の絡むコードを扱う時は適当なコードを書くのではなく,勉強してきれいなコードを書きたい.

vue-chartjs で二重Y軸を扱う

各単語について

  • Vue.js
    Javascriptフレームワークの一つ. HTML側とJS側で値の共有ができ(双方向データバインディング), また,HTML側でforやif文が書けるようになる便利なやつ.
  • Chart.js
    JavaScriptで洒落たグラフを簡単に書けるようにするライブラリ.
  • vue-chartjs
    Vue.js上でChart.jsを使うためにChart.jsをラップしたもの.

今回の開発の概要

vue-chartjsの二重Y軸の扱いに結構詰まったので, 今回の対処をメモする.
今回は以下の流れで開発した.

  1. vue-chartjsで同じ描画先に棒グラフを2本描画
  2. Y軸を2本描画し,各グラフで異なるY軸を参照
  3. 各グラフが紐づくY軸を,切り替えるボタンを作成

Y軸を2本描画し,各グラフで異なるY軸を参照

"vue-chartjsで同じ描画先に棒グラフを2本描画"はメモするまでもないので省略.

Y軸を複数描画する際は,グラフ描画関数の引数のoptions要素を編集する. 具体的には,以下の様にyAxes要素を配列にする.

options: {
  scales: {
    yAxes: [{
      id: "y-axis-1",
      position: "left"
    },
    {
      id: "y-axis-2",
      position: "right"
    }]
...

positionの値で描画先の上下左右どこに描画するか定義できる.
また,各グラフでどちらのY軸に紐付かせるかは,上記のidを以下の様にyAxisID要素に格納することで,定義できる.

{
  type: 'bar',
  label: 'label1',
  backgroundColor: '#f87979',
  data: [11, 11, 19, 8, 8, 9, 30, 24, 25, 22, 11, 15],
  fill: false,
  tension: 0,
  yAxisID: 'y-axis-1'
},
{
  type: 'bar',
  label: 'label2',
  backgroundColor: '#f87000',
  data: [5, 25, 17, 6, 29, 5, 5, 13, 16, 10, 14, 29],
  fill: false,
  tension: 0,
  yAxisID: 'y-axis-2'
}

以下の用にY軸が左右に描画される. ピンクの棒グラフが左のY軸,オレンジの棒グラフが右のY軸に紐づいている.

f:id:uttnaoki:20180805003001p:plain

各グラフが紐づくY軸を,切り替えるボタンを作成

ここで詰まった.結局,いいやり方にはなっていないと思うが, 今回は以下の方法で対処した.

  • ボタンクリックにより,yAxisIDの値を変更
  • 修正したグラフのデータの_meta要素を削除し,グラフを再描画

ボタンクリックにより,yAxisIDの値を変更

以下のコードでyAxisIDの値を変更するラジオボタンを設置

<td v-for="col in filteredData.datasets.length">
  <!-- id には (axis_select-列番号-軸番号) を格納 -->
  <input type="radio" :id="['axis_select-' + (col-1) + '-1']" value="y-axis-1"
      v-model="filteredData.datasets[col-1].yAxisID" @input="deleteMeta">
  <label for="y-axis-1">軸1</label>
  <input type="radio" :id="['axis_select-' + (col-1) + '-2']" value="y-axis-2"
      v-model="filteredData.datasets[col-1].yAxisID" @input="deleteMeta">
  <label for="y-axis-2">軸2</label>
</td>

filteredData変数にグラフ描画に使うデータが入っている.
上記コードで設置したボタンをクリックすることにより,deleteMeta()関数を呼び出し,_meta要素を削除する.

修正したグラフのデータの_meta要素を削除し,グラフを再描画

上で設置したラジオボタンのクリックにより発火するdetaMeta()関数は以下の通り.

// データの(_meta)要素を削除し,グラフを更新
deleteMeta (e) {
  // buttonId は axis_select-0-1 の形式
  // ハイフン区切りで,(axis_select 列番号 軸番号) を意味する
  const buttonId = e.target.id;
  const index = Number(buttonId.split('-')[1]);

  delete this.localData.datasets[index]._meta;
  // グラフを更新
  this.reloadGraph();
}

動作テスト

以下は今回開発したものを実際に動かしたものです. 二つ目のグラフ(オレンジの棒)のY軸を 左->右->左 と動かしています. オレンジのグラフの最大値はピンクのグラフの最大値よりも大きいため, ピンクの方のグラフも動いています.

f:id:uttnaoki:20180805005522g:plain

感想

とりあえず,ちゃんと動くものはできたので,目的は達成しました. でも対処の仕方があまりきれいではないので,すっきりしない.

開発には関係ないことですが,今回初めて画面キャプチャで動画を作成し,GIFを作成しました. 楽しい.

PHPで簡単なAPIのモックを作る

PHPに触るきっかけ作り

Web関係のプログラムでNode(JavaScript)やPythonを触ったことはあるが, 未だにPHPのコードは一度も書いたことがないし,まともにコードを読んだことがない.
かつ,最近,アルバイト先でPHP需要が高まってる雰囲気がある.
そもそもWeb系といえばまずPHPっていう気もする.
なので,PHPをちょっとずつ勉強していきたい.

APIのモックの開発でPHPデビューする

Webページ(アプリ)のフロントエンドを開発する際, 開発してるページからまだ開発していないAPIを叩きたい時がある.
こんな時のため,とりあえず受け答えだけしてくれるような簡易版のサーバー(APIのモックサーバー)があると便利.
今回はPHPデビューのためにも,APIのモックをPHPで作成する.

実装内容

今回は今作ってるWebページで必要なGETとPOSTを受けてくれるサーバーを作る.
また,受け渡しするデータはJSONデータを想定している.
具体的な実装内容は,以下の通り.

  • GET
    • クライアントはクエリなしでGET
    • サーバーは決め打ちのJSONデータを返却
  • POST
    • クライアントは適当なJSONをPOST
    • サーバーは受け取ったJSONをそのまま返却

今回のソースコード

今回書いたソースコードは最終的にこんな感じになりました.

<?php
// Origin null is not allowed by Access-Control-Allow-Origin.とかのエラー回避の為、ヘッダー付与
header('Access-Control-Allow-Origin: *');

function api4leaflet () {
  // GET用のjson
  $spots = [
    [
      'name' => 'スポット1',
      'category' => 'A',
      'lat' => '34.598804',
      'lng' => '133.76884'
    ],
    [
      'name' => 'スポット2',
      'category' => 'B',
      'lat' => '34.606323',
      'lng' => '133.793363'
    ],
    [
      'name' => 'スポット3',
      'category' => 'A',
      'lat' => '34.61858',
      'lng' => '133.759183'
    ]
  ];

  switch ($_SERVER['REQUEST_METHOD']) {
    case 'GET':
      echo json_encode($spots, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
      break;
    case 'POST':
      echo json_encode(file_get_contents('php://input'), JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
      break;
    default:
      echo 'unexpected request';
      break;
  };
}

switch ($_SERVER['REQUEST_URI']) {
  case '/leaflet':
    api4leaflet();
    break;
  default:
    echo 'unexpected URI';
    break;
}

実行コマンド

以下のコマンドで実行しています.
$ php -S 127.0.0.1:5000 api_mock.php
この場合,ブラウザなどからhttp://127.0.0.1:5000にアクセスすることで, サーバーにアクセスできます.

各コードについて

Headerとかはよく調べてなくて,人のコードそのままパクっていますが, その他のコードの意味は以下の通り.

  • $_SERVER['REQUEST_URI']
    サーバーのルート以下のパスが入っています.
    例えば,http://127.0.0.1:5000/leafletにアクセスした場合, この変数には文字列/leafletが格納されます.
  • $_SERVER['REQUEST_METHOD']
    実行したAPIの種類が入っています. GETPOSTといった文字列がそのまま入っています.
  • file_get_contents('php://input')
    これでクライアントが送ってきたbodyが取り出せるっぽい.
  • json_encode($spots, なんやかんや);
    サーバーからクライアントにJSONなどのデータを送る際, そのデータを一旦文字列に変換する必要があります. これはその変換をするコード.
    また,第二引数のなんやかんやは省略しても送信できますが, 日本語のデータを送る際はこれを書かないとunicodeか何か謎の形式になってしまうので,今回はごちゃごちゃ書いてます.
    この引数を書かない場合と書いた場合は以下の通り.

次に

PHPからMySQLを操作したい.

ザンキゼロのシガバネをGoogleのToDoリストで管理する

ザンキゼロ

f:id:uttnaoki:20180725230729j:plain

ザンキゼロとは,ダンガンロンパでお馴染みスパイク・チュンソフトの新作ゲームです.
このゲームにはバトル要素があり,キャラクターを強化するための以下の2つの育成要素があります.

  • スキル
    レベルアップ時に取得できるスキルポイントを消費し,取得する.
    攻撃力強化やアイテムの効果を増強させるものなどがある.
  • シガバネ
    キャラクターが特定の条件で死ぬと,復活する際に取得する.
    例えば,毒状態で死ぬと,復活後に毒耐性を取得する(毒になりにくくなる).

シガバネはキャラクター毎(全8人)に独立しており,大半は内容が被っているが,中にはキャラクター固有のものもあります. また,一人あたりおよそ150個弱のシガバネがあり, このシガバネの取得が本作のやりこみ要素の1つになっています.

今回解決したい課題

↑で話した通り,シガバネの数は非常に多く, ゲームをプレイしていると,今何のシガバネを取得していて何を取得していないかわからなくなってきます.
そこで,今回は,各キャラクターの未取得のシガバネを何らかのツールを使って管理しようと考えました.

解決方針

GoogleToDoリストを使って未取得のシガバネを管理します.

  • このToDoリストはGoogleのサービス(詳しく言うとGmailのサービス)なので,Googleのアカウントがあれば誰でも利用できます.
  • また,スマホ用アプリもリリースしているので,パソコンとスマホの両方からこのサービスを手軽に利用できます.

ToDoリストでシガバネを管理するにあたって,以下の下準備を行います.

  1. ザンキゼロの攻略サイトから各キャラクターのシガバネを取得(クロール)
  2. 取得したデータを整形(GoogleのToDoリストにインポートできる形式に)
  3. ToDoリストにインポート

シガバネデータの取得とデータ整形

クロールしたサイトについて

今回は以下のサイトを利用させていただきました.

h1g.jp

このサイトのrobots.txtは以下の通りなので,クロールしてもいいサイトだと考えています.

User-agent: *
Disallow: /*/?cmd=backup
Disallow: /*/?cmd=edit
Disallow: /*/?cmd=search

Allow: /

クロール・データ整形のコード

以下のコードでデータを取得し,整形してcsvファイルに書き込みました.
今回は面倒くさがってコメントをほとんど書いていません.

# coding: UTF-8
import urllib.request
from bs4 import BeautifulSoup
from time import sleep
import re
import csv

def encode_jpurl(japanese):
    return urllib.parse.quote_plus(japanese, encoding='utf-8')

def make_url(character_name):
    query = "{0}%28{1}%29".format(encode_jpurl(character_name), encode_jpurl('シガバネ一覧'))
    return "https://h1g.jp/zanki_zero/?{0}".format(query)

def get_html(url):
    f = urllib.request.urlopen(url)
    html = f.read().decode('utf-8')
    return html

def extract_string_from_tr(tr_tag):
    td_array = tr_tag.find_all("td")
    data_array = [td.text for td in td_array]
    data_array.extend(["" for i in range(3-len(data_array))])
    return data_array

def extract_data_from_html(html):
    soup = BeautifulSoup(html, "html.parser")
    div_shigabane = soup.find("div", class_="mainbody")
    tbody_shigabane = div_shigabane.find("tbody")
    rows_shigabane = tbody_shigabane.find_all("tr")
    return [extract_string_from_tr(row_shigabane) for row_shigabane in rows_shigabane]

def fill_the_empty(data):
    pre_data = ["", "", ""]
    for i in range(len(data)):
        if data[i][1] == "": data[i][1] = pre_data[1]
        if data[i][2] == "": data[i][2] = pre_data[2]
        pre_data = data[i]
    return data

def write_data_to_csv(data, filename):
    f = open('csv/{0}.csv'.format(filename), 'w')
    writer = csv.writer(f, lineterminator='\n')
    writer.writerows(data)
    f.close()

def shape4todolist(data, listname):
    shaped_data = [["tasklist_name", "title", "notes", "status", "due", "completed", "deleted", "hidden", "depth"]]
    shaped_data.append([listname, listname, "", "needsAction", "", "", "", "", 0])
    # dataのヘッダを削除
    data.pop(0)
    shaped_data.extend([[listname, d[0], d[1], "needsAction", "", "", "", "", 1] for d in data])
    return shaped_data

if __name__ == "__main__":
    character_names = ["ハルト", "リョウ", "ゼン", "マモル",
        "リンコ", "ユマ", "ミナモ", "サチカ"]
    for character_name in character_names:
        print(character_name)
        url = make_url(character_name)
        html = get_html(url)
        data = [["名称", "解説", "効果"]]
        data.extend(extract_data_from_html(html))
        filled_data = fill_the_empty(data)
        write_data_to_csv(filled_data, character_name)
        data4todolist = shape4todolist(filled_data, character_name)
        write_data_to_csv(data4todolist, "{0}_todolist".format(character_name))
        sleep(1)

整形後のデータ

以下のような9列のデータになっています.

f:id:uttnaoki:20180725230504p:plain

tasklist_nameでラベルを指定し,depthでそのタスクの階層を指定します.
画像では,タスクの階層を1に指定していますが,後々考えたら全部0にしたらよかったかもと思いました.

GoogleのToDoリストにインポート

データをインポートするために,以下のサイトを利用しました.

https://import-tasks.appspot.com/main

信用できるサイトかどうかは正直よくわかっていません.

ここにcsvファイルを送ると,タスクがGoogleのToDoリストに追加されます.

ToDoリストの見栄え

パソコンのブラウザ上では以下の様にタスクが表示されます.

f:id:uttnaoki:20180725231855p:plain

csvファイルの時とタスクの順が一致していませんが,これは単純にブログ投稿前に操作(削除)してしまったからです.
インポート直後はcsvファイルと同じタスクの順になっています.

以下はiphoneのアプリで見た時のものです.

f:id:uttnaoki:20180725232226j:plain:w300

タスクをクリック(タップ)するとすぐに消せ,また,パソコンとスマホで連携できるので,操作感はいいと思います.

おわりに

今回はやりこみ要素(コンプリート系)のあるゲームとしてザンキゼロを取り上げましたが, 大概のゲームは何かしらのやりこみ要素があると思います.
例えば,ポケモンのまだ捕まえていないポケモン,モンハンの食材,メタルギアの武器,などなど.
今回の開発内容はザンキゼロに限らず,上記のようなやりこみ要素に対しても適用できます.
このため,今回の開発が今後自分がプレイするゲームに対しても攻略の助けになればと思います.

任意のディレクトリからDropboxのメモファイルを編集

やりたいこと

  • Dropboxを利用してメモ(テキストファイル)を各デバイスで共有したい.
  • ただし,メモを配置するディレクトリはDropbox/Memoとする.
  • Dropbox/Memoディレクトリの位置を意識せず,任意のディレクトリからメモの確認・編集を実行したい(memoコマンドの開発).

また,今回の開発は以下の記事の続きです.

uttnaoki.hatenablog.com

前回の開発により,memoコマンドにDropbox/Memoディレクトリ下のファイル名が補完されるようになっています.

memoコマンドの開発

開発手順

以下の処理を実現する.

  1. memoファイルの一覧を表示
  2. memoファイルの内容の確認(出力)
  3. memoファイルの追加
  4. memoファイルの削除
  5. memoファイルの編集

作成するコードは前回の開発のfunction memo(){}内に記述する.

memoファイルの一覧を表示

以下のコマンドでmemoファイルの一覧を出力する.
$ memo

この処理はDropbox/Memoディレクトリをlsすることで実現する.

コードは以下の通り.

local MEMO_DIR=$HOME"/Dropbox/Memo/"

ls $MEMO_DIR

実行結果は以下の通り.ちゃんとファイル名を出力できてる.

f:id:uttnaoki:20180722015414p:plain:h140

memoファイルの内容の確認(出力)

以下のコマンドでDropbox/Memo/memo.txtファイルの内容を確認する.
$ memo memo.txt

この処理は,Dropbox/Memo/memo.txtファイルをlessすることで実現する.

コードは以下の通り.

local MEMO_DIR=$HOME"/Dropbox/Memo/"

function less_memo() {
  local MEMO_NAME=$1
  # メモが既に存在する場合,less
  if [[ -f $MEMO_DIR$MEMO_NAME ]]; then
    less $MEMO_DIR$MEMO_NAME
  # メモが存在しない場合,echo で通知
  else
    echo "「memo: "$MEMO_NAME"」は存在しません."
  fi
}

less_memo $1

実行結果は省略.

memoファイルの追加

以下のコマンドでDropbox/Memo/tmp2.txtファイルを作成する.
$ memo -a tmp2.txt

この処理は,Dropbox/Memo/tmp2.txtファイルをtouchすることで実現する.

コードは以下の通り.

function add_new_memo() {
  local MEMO_NAME=$1
  # メモが既に存在する場合,echo で通知
  if [[ -f $MEMO_DIR$MEMO_NAME ]]; then
    echo "「memo: "$MEMO_NAME"」は既に作成済みです."
  # メモを新規作成
  else
    touch $MEMO_DIR$MEMO_NAME
    echo "「memo: "$MEMO_NAME"」を新規作成しました."
  fi
}
case $1 in
  # メモを新規作成
  '-a' ) add_new_memo $2 ;;
  # 第1引数に関するエラー処理
  * ) echo "error: オプションが適切ではありません." ;;
esac

実行結果は以下の通り.

f:id:uttnaoki:20180722021005p:plain:h300

memoファイルの削除

以下のコマンドでDropbox/Memo/tmp2.txtファイルを削除する.
$ memo -r tmp2.txt

この処理は,Dropbox/Memo/tmp2.txtファイルをrmすることで実現する.

コードは以下の通り.

function remove_memo() {
  local MEMO_NAME=$1
  # メモを削除
  if [[ -f $MEMO_DIR$MEMO_NAME ]]; then
    rm $MEMO_DIR$MEMO_NAME
    echo "「memo: "$MEMO_NAME"」を削除しました."
  # メモが存在しない場合,echo で通知
  else
    echo "「memo: "$MEMO_NAME"」は存在しません."
  fi
}

case $1 in
  # メモを新規作成
  '-a' ) add_new_memo $2 ;;
  # メモを削除
  '-r' ) remove_memo $2 ;;
  # 第1引数に関するエラー処理
  * ) echo "error: オプションが適切ではありません." ;;
esac

実行結果は以下の通り.

f:id:uttnaoki:20180722021441p:plain:h300

memoファイルの編集

以下のコマンドでDropbox/Memo/memo.txtファイルを編集する. $ memo -e memo.txt

コードは以下の通り.

function edit_memo() {
  local MEMO_NAME=$1

  # 規定のエディタを設定していれば,そのエディタを使用
  # 規定のエディタを設定していなければ, vim を使用
  if [[ -z "$EDITOR" ]]; then
    local DEF_EDITOR='vim'
  else
    local DEF_EDITOR=$EDITOR
  fi

  # メモを編集
  if [[ -f $MEMO_DIR$MEMO_NAME ]]; then
    $DEF_EDITOR $MEMO_DIR$MEMO_NAME
  # メモが存在しない場合,echo で通知
  else
    echo "「memo: "$MEMO_NAME"」は存在しません."
  fi
}

case $1 in
  # メモを新規作成
  '-a' ) add_new_memo $2 ;;
  # メモを削除
  '-r' ) remove_memo $2 ;;
  # メモを編集
  '-e' ) edit_memo $2 ;;
  # 第1引数に関するエラー処理
  * ) echo "error: オプションが適切ではありません." ;;
esac

実行結果は省略.

今回着手していない部分

今回でとりあえずmemoファイルの操作が一通りできたと思うが,実際に使っていかないと,ちゃんとできているのかわからない.
ただ,補完周りで,以下の内容は課題として残る.

  • memoファイルを.txtを意識せず,操作できるようにする.
  • memoコマンドを入力する際,入力している引数(番目)に応じて補完内容を変える.例えば,1つ目の引数を入力する際,「-a, -r, -e」が補完で出るようにする.(また,それぞれの内容を表示する.)

ソースコード全体

#!/usr/local/bin/zsh

function memo() {

  local MEMO_DIR=$HOME"/Dropbox/Memo/"

  # 選択したメモを less する関数
  function less_memo() {
    local MEMO_NAME=$1
    # メモが既に存在する場合,less
    if [[ -f $MEMO_DIR$MEMO_NAME ]]; then
      less $MEMO_DIR$MEMO_NAME
    # メモが存在しない場合,echo で通知
    else
      echo "「memo: "$MEMO_NAME"」は存在しません."
    fi
  }

  # メモを新規作成する関数
  function add_new_memo() {
    local MEMO_NAME=$1
    # メモが既に存在する場合,echo で通知
    if [[ -f $MEMO_DIR$MEMO_NAME ]]; then
      echo "「memo: "$MEMO_NAME"」は既に作成済みです."
    # メモを新規作成
    else
      touch $MEMO_DIR$MEMO_NAME
      echo "「memo: "$MEMO_NAME"」を新規作成しました."
    fi
  }

  # メモを削除する関数
  function remove_memo() {
    local MEMO_NAME=$1
    # メモを削除
    if [[ -f $MEMO_DIR$MEMO_NAME ]]; then
      rm $MEMO_DIR$MEMO_NAME
      echo "「memo: "$MEMO_NAME"」を削除しました."
    # メモが存在しない場合,echo で通知
    else
      echo "「memo: "$MEMO_NAME"」は存在しません."
    fi
  }

  # メモを編集する関数
  function edit_memo() {
    local MEMO_NAME=$1

    # 規定のエディタを設定していれば,そのエディタを使用
    # 規定のエディタを設定していなければ,vim を使用
    if [[ -z "$EDITOR" ]]; then
      local DEF_EDITOR='vim'
    else
      local DEF_EDITOR=$EDITOR
    fi

    # メモを編集
    if [[ -f $MEMO_DIR$MEMO_NAME ]]; then
      $DEF_EDITOR $MEMO_DIR$MEMO_NAME
    # メモが存在しない場合,echo で通知
    else
      echo "「memo: "$MEMO_NAME"」は存在しません."
    fi
  }

  # 引数がなければメモの一覧を表示
  if [[ $# -eq 0 ]]; then
    ls $MEMO_DIR
  # 引数の数が1であれば,メモ(第一引数)の中身を less
  elif [[ $# -eq 1 ]]; then
    less_memo $1
  # 引数の数が2であれば,オプションに応じた処理を実行
  elif [[ $# -eq 2 ]]; then
    case $1 in
      # メモを新規作成
      '-a' ) add_new_memo $2 ;;
      # メモを削除
      '-r' ) remove_memo $2 ;;
      # メモを編集
      '-e' ) edit_memo $2 ;;
      # 第1引数に関するエラー処理
      * ) echo "error: オプションが適切ではありません." ;;
    esac
  # 引数の数が (0 ~ 2) でなければエラーを出力
  else
    echo 'error: 引数の数が適切ではありません.'
  fi
}

_memo() {
  _values "description of completion" $(ls ~/Dropbox/Memo)
}
compdef _memo memo

Repl-AI を使って Line bot を作ってみた

やりたいこと

  • 特定の言葉に反応する簡単な Line bot の作成
  • 作成した機能が常時稼働するようにする

手順

  1. Repl-AI で受け答えの処理(API)を作成
  2. Line bot を作成
  3. Repl-AI と Line bot を連携

Repl-AI で受け答えの処理(API)を作成

Repl-AI とは,プログラム知識なしで、AIチャットボットを作成できるWebアプリです.
具体的に,以下のようにビジュアルプログラミング言語を使ってメッセージの受け答えのルールを作成できます.

f:id:uttnaoki:20180711234453p:plain

上記のルールでは,ユーザの「おはよう」「こんにちは」に対して,同じ言葉を返すことを定義しています.

Line bot を作成

まず,Line Developers に自分の LIne アカウントでログインする.
新規プロダイバーを作成し,新規チャンネルの作成から Message API を作成する.

f:id:uttnaoki:20180711225045p:plain

  • この時点でLineで友達登録でき,メッセージを送ることができる.
  • 上の画像のチャンネルをクリックした先のページにQRコードがあるので,それから友達登録できる.
  • 上の画像ではすでにアイコンが設定されているが,チャンネル作成直後は当然アイコンが設定されていない.

Repl-AI と Line bot を連携

Repl-AI と Line bot を連携させるために, Repl-AI の方に以下の項目を入力する必要がある.

  1. ID
  2. シークレット
  3. トーク

f:id:uttnaoki:20180711223916p:plain:w300

IDとシークレットについては,チャンネル(Line Dev側)作成時に定義されているが,トークンについては Line Dev 側で発行する必要がある. 発行後,指定時間内で有効期限が切れるので,トークン発行後はすぐに Repl-AI 側に入力しないといけない.(連携後にトークンの期限が切れることは問題ない)

また,上の画像のフォームについて,「2. シナリオ」を設定すると,「3. URL」にURLが生成されるので,これをコピーし,連携ボタンを押す.

コピーしたURLは Line Dev 側のチャンネルに入力する項目があるので,そこに入力する.

連携結果

以下の様に,「おはよう」と「こんにちは」に反応している.
最初に NOMATCH が出ているのは,連携前に発言したため.
Repl-AI で定義できていないユーザの発言については画像の通り,NOMATCH が出力される.

f:id:uttnaoki:20180711232013p:plain:w300

備考

自動応答メッセージ

Line Dev の方で「自動応答メッセージ」を利用しないに設定しておかないと,何を話しても以下のようなメッセージが出力される.
これは Line bot の仕様なので,Repl-AI 側では対処できない.

f:id:uttnaoki:20180711232426p:plain:w300

グループトーク

おそらく,Line bot は Slack bot と違い,複数人のチャット(トークルーム)に対応していない.(調査不足の可能性有)
bot との 個別トークで適切に応答する状態でも,他のアカウントを招待したトークルームでは応答してくれない.

展望

Repl-AI 側でユーザから入力してきたワードを覚えたり,複雑な応答ルールを作成したりできるみたいなので, そのあたり使ってみたい.

zsh の補完周りをちょっと触る

今回の内容

  • 自作コマンド(memoコマンド)の補完を編集
  • 任意のディレクトリから,$ memo <TAB>を入力すると,~/Dropbox/Memoディレクトリの中のテキストファイル名を補完する(補完リストに出す).

なぜこれをやりたいか

最終的に実現したいこと

  • Dropboxを利用してメモ(テキストファイル)を各デバイスで共有したい.
  • ただし,メモを配置するディレクトリはDropbox/Memoとする.
  • Dropbox/Memoディレクトリの位置を意識せず,任意のディレクトリからメモの確認・編集を実行したい(memoコマンドを実装).

memoコマンドを実装する上で

  • memoコマンドの引数に確認・編集するメモの名前を与えることが考えられる.
  • 補完周りを弄らないとメモ名を補完なしで入力する仕様になってしまい,不便である.
  • このため,memoの引数の補完にDropbox/Memoディレクトリ直下のファイル名を割当てたい.
  • 補完の設定方法は使ってるシェルによって違うらしいので,zsh用の方法で補完を設定する.

memoコマンドの引数の補完にDropbox/Memoディレクトリ直下のファイル名を割当てる

以下のコードをzsh実行時に読むファイル(.zshrc等)に記述する.

_memo() {
  _values "description of completion" $(ls ~/Dropbox/Memo)
}
compdef _memo memo

この"description of completion"の文字列はzshの設定で補完候補の出力時に出力したりしなかったりできるが,現状どうでもいいので自分は今の所出力していない(デフォルト).

また,下記コードの実行が前提なので,.zshrc等のファイルに下記コードが書かれているか要確認

autoload -U compinit
compinit

以下の通り,Dropbox/Memo直下のファイル名を補完できた.

f:id:uttnaoki:20180704034836p:plain:w200

次に

  • 補完の出力内容をもっといい感じにしたい.例えば,".txt"を消すとか,メモの概要を表示するとか.
  • memoコマンドの中身を実装したい.(こっち優先)