ニコニコ動画で、シチュエーション1に即したカード名が
《日本語/英語》
でコメントされる流れが好きです。
しかし日本名はともかく英語までいちいち調べるのは面倒2なので、情報をさっと取れないかなと調べました。
さすが「もっともよく遊ばれているTCG」。
APIも充実していました。
あ、いまさらですがMTGはミーティングではなくMagic The Gatheringのことです。(お約束
完成品
khsk/MtG-card-suggest: Search and Completion from your input
API
MTG Developers
を日本語で検索しています。
サジェスト
アニメの名言を簡単に引用できるChrome extension『Kotoha』作りました - Konifar's WIP
で使われている
At.js
を使用しています。
コード
const API_URL = 'https://api.magicthegathering.io/v1/cards'
const mtg_callback = {
remoteFilter: (() => {
let oldQuery
let oldCards
let promise = null
return (query, callback) => {
// 気休めの接続低減
// 変換開始時の空文字queryを排除している
if (!query) {
return
}
// 気休めの接続低減
// カーソル移動でイベントが発生するので同値なら結果を使い回す
if (query === oldQuery) {
callback(oldCards)
return
}
if (promise) {
// 気休めの負荷緩和
// 英字入力やカーソル移動で通信が多発するので、古いのはabortしてやる
promise.abort()
promise = null;
}
promise = $.getJSON(
API_URL,
{
name: query,
language: 'Japanese',
},
(data) => {
let cards = $.map(data.cards, (card) => {
const foreignData = $.grep(card.foreignNames, (foreignName) => {
return (foreignName.language == 'Japanese')
})[0]
return {
name: card.name,
jname: foreignData.name || card.name,
jimageUrl: foreignData.imageUrl || card.imageUrl,
}
})
// 外部通信なので、検索中と区別がつくように結果0用のリストを追加する
if (cards.length == 0) {
cards = [{
name: 'Not matched',
jname: 'Not matched',
jimageUrl: 'Not matched',
}];
cards[0].atwho_order = 0
console.log('cards2', cards)
}
oldQuery = query
oldCards = cards
promise = null
callback(cards)
}
)
}
})(),
sorter: (query, items, searchKey) => {
var _results, i, item, len;
if (!query) {
return items;
}
_results = [];
for (i = 0, len = items.length; i < len; i++) {
item = items[i];
// remoteFilterで作ったNot matchedを活かすため、手動でordwerを決めた場合は判定しないようカスタマイズ
if (typeof item.atwho_order === 'undefined') {
item.atwho_order = item.atwho_order || new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase());
}
if (item.atwho_order > -1) {
_results.push(item);
}
}
return _results.sort(function (a, b) {
return a.atwho_order - b.atwho_order;
});
},
}
$('textarea, #nameInput').atwho({
at: "\/mtg",
displayTpl: "<li>${jname} / ${name}</li>",
insertTpl: "《 ${jname} / ${name} 》",
limit: 200,
callbacks: mtg_callback,
searchKey: 'jname', // ここnullで動かしたいのだが
})
$('#imageInput').atwho({
at: "\/imtg",
displayTpl: "<li>${jname} / ${name}</li>",
insertTpl: "${jimageUrl}",
searchKey: 'jname', // ここnullで動かしたいのだが
callbacks: mtg_callback,
})
$('#imageInput').change(function() {
$('#preview').attr('src', $('#imageInput').prop('value'))
})
At.jsの改変とコールバック実装
IMEのバグ修正?の取り消し
Return early for undefined events by markedmondson · Pull Request #534 · ichord/At.js
の変更を意図的に抜き、フィルター側で空文字のqueryを弾いて居ます。 これは検索開始時の空文字query検索を防ぐとともに、変換確定時にqueryが実行されるようにする処置です。
(逆に言えばこのプルリクが適用されていると、変換確定したときに検索されないです)
const mtg_callback = {
remoteFilter: (() => {
let oldQuery
let oldCards
let promise = null
return (query, callback) => {
// 気休めの接続低減
// 変換開始時の空文字queryを排除している
if (!query) {
return
}
}
ここのifでプルリクの代替としています。
検索回数低減
先の空文字検索を防ぐガードに加え、もう少しガードを追加しています。
// 気休めの接続低減
// カーソル移動でイベントが発生するので同値なら結果を使い回す
if (query === oldQuery) {
callback(oldCards)
return
}
if (promise) {
// 気休めの負荷緩和
// 英字入力やカーソル移動で通信が多発するので、古いのはabortしてやる
promise.abort()
promise = null;
}
コメントそのままですが、検索の発火がキャレットの移動でも走る繊細さなので、結果を一つだけキャッシュしたり、ローマ字検索などで先にしてしまっている検索をabortして最後の完成queryだけで検索しようと画策しています。
効果のほどは知らん…
APIの制限は一時間に5000回と太っ腹ですが、触っていて気になるレベルでしたのでいろいろ試しました。
検索結果なしの表示
普通?は内部の候補を表示するので高速だと思いますが、今回は外部との通信なので少しばかり待ち時間が生じます。
そしてAt.jsは候補が0のときに何も出さない作りなので、
「検索待ちなのか検索結果が0なのか」
がわからない問題がありました。
そこで、
// 外部通信なので、検索中と区別がつくように結果0用のリストを追加する
if (cards.length == 0) {
cards = [{
name: 'Not matched',
jname: 'Not matched',
jimageUrl: 'Not matched',
}];
cards[0].atwho_order = 0
console.log('cards2', cards)
}
と検索結果が0のときにダミーの候補を作ってやるようにしました。
atwho_order
が検索キーワードとindexOf
して出来たint値で、!-1
なら一致したということです。
ただ、検索結果側のみで手動でatwho_order
を偽装するだけではうまく動きませんでした。
というのも、atwho_order
が作られるのはsorter
のタイミングだったからでした。
At.js/default.coffee at master · ichord/At.js
At.js/jquery.atwho.js at 1b7a52011ec2571f73385d0c0d81a61003142050 · ichord/At.js
なので、デフォルトのsorter
から手動で作ったatwho_order
を見逃す実装を追加しました。
sorter: (query, items, searchKey) => {
var _results, i, item, len;
if (!query) {
return items;
}
_results = [];
for (i = 0, len = items.length; i < len; i++) {
item = items[i];
// remoteFilterで作ったNot matchedを活かすため、手動でordwerを決めた場合は判定しないようカスタマイズ
if (typeof item.atwho_order === 'undefined') {
item.atwho_order = item.atwho_order || new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase());
}
if (item.atwho_order > -1) {
_results.push(item);
}
}
return _results.sort(function (a, b) {
return a.atwho_order - b.atwho_order;
});
},
問題点
ダブる
APIは再録ごとに別カードとして扱うので、同じ候補がたくさんでます。
テキストやイラスト違いなどがあるので一概に悪いわけではないのですが…
オブジェクトのdistinctは
JavaScriptのオブジェクト配列に対してdistinct的な事(重複を排除した結果を返す)をする - Qiita
によると
O(n^2)らしいので、うーん…
日本語名がないカードがある
たとえば、有名なBlack Lotusは日本語版未発売?と考えられますが、
勝舞くんが使っていた筋肉スリヴァー/Muscle Sliver
などテンペストブロック周りの古いカードなどにも日本語名がありません。
https://api.magicthegathering.io/v1/cards?name=Muscle
画像も無い場合があります。
新しめのカードは最初から充実しているのですが。
なので、ダブっている場合でもリストの下の方が取れる可能性が高いです。
あと効果やフレーバーまで全言語対応してくれると…は高望みですね。
最大の問題点
画像URLを取得しようとすると、手が勝手にimgt
と打ってしまうこと…
あとアドオンにして配布しないと特に便利ではないことですねー…。
他の
At.jsを使うとき、依存物のCaret.jsを誤って
caret - cdnjs.com - The best FOSS CDN for web related libraries to speed up your websites!
accursoft/caret
にしていてハマりました。
ちゃんと
ichord/Caret.js
使おうね。