加具留矢流余

かぐるやるよ

Javascriptでキーボードショートカットを実装する

anntにキーボードショートカットを実装したいと思って調べてみても、検索に引っかかるのはライブラリの使い方ばっかりであまり具体的な仕組みが見つからない。あまりブラックボックスなライブラリを使いたくないという気持ちと、この程度簡単に実装できるだろというイキリで更に調査を進めるとshortcut.jsという非常にシンプルなライブラリが見つかった。

www.openjs.com コンパクトにまとまっているので、内容を読み解いた上で使いやすいようにクラスとして実装し直した。 注意点としてshortcut.jsはブラウザのバグやIEにも対応しているが、シンプルさを優先して今回の実装では省いている。

前提知識

キーボードに限らずブラウザのイベントは要素にaddEventListenerを登録することで監視することができる。キーボードのイベントとしてkeydown、keyupが定義されていて、keydownはその名の通りキーが押し込まれたときに発生するイベント、keyupはキーを離したときに発生するイベントとなっている。今回はkeydownイベントを監視することで、予め登録されたショートカットが押されたかを判断している。

keydownやkeyupのリスナーを登録しておくと、登録したコールバック関数にKeyboard Eventというオブジェクトが渡される。keyプロパティcodeプロパティでどのキーが押されたかを確認できる。keyもcodeプロパティも押されたキーを取得するプロパティだが、両者には明確に違いがあるので使用する際にはキーボード: keydown と keyupを確認するのが良い。

Ctrlキー、Altキー、Cmdキーが押されているかどうかはctrlKey、altKey、metaKeyプロパティで取得できる。keyプロパティで押されたボタンを、ctrlKeyなどでコントロールキーを始めとしたキーが押されているかどうかを判断できるので、これでキーボードショートカットを実装できる。

プロパティ名 概要
key 押されたキーの文字を表す
code キーボードのレイアウトに依存しない押されたキーの位置を表すコードを表す
ctrlKey Ctrlキーが押されているか
altKey Altキーが押されているか
metaKey Cmdキーが押されているか

参考資料
ブラウザイベントの紹介
KeyboardEvent - Web APIs | MDN

プログラムの概要

ShortcutController

ショートカットを管理するメインのクラス。registerShortcutでキーバインドと関数を登録した状態で、handleメソッドをaddEventListenerで登録すればショートカットが使えるようになる。registerShortcutメソッドは使いやすいようにCtrl+Sのように'+'区切りでキーバインドを登録できるようにしてある。handleメソッドでは登録されているShortcutObject(次項参照)のigniteメソッドを一つづつ呼び出していく。

ShortcutObject

一つのショートカットを管理するためのホルダークラス。igniteメソッドは入力されたキーの情報を引数に取って、ショートカットの条件に合致した場合にのみ登録された関数を実行する。

プログラムの実装


const ALLOWED_KEY = ['backspace', 'esc', 'escape', 'tab', 'space', 'return']
/**
 * 使用可能なコードか判定を行う.
 * 1文字の英字かALLOWED_KEYに含まれる特殊文字のみが登録可能です.
 * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
 * 入力は小文字である必要があります
 * @param {String} code 
 */
function allowedCode(code) {
  if (code.length === 1 && code.match(/[a-z]/i)) {
    // 長さが1
    return true;
  }
  return ALLOWED_KEY.includes(code);
}

/**
 * 1つのショートカットを表現するオブジェクト
 */
class ShortcutObject {

  /**
   * コンストラクタ
   * @param {*} char: ショートカットとして使用する英字1文字
   * @param {*} ctrl: コントロールボタンと同時に押す必要があるか
   * @param {*} meta: (Mac用): コマンドボタンと同時に押す必要があるか。
   * @param {*} propagate: Eventを伝播させるか。
   * @param {*} func: ショートカットが押されたときに実行する関数
   */
  constructor(char, ctrl, meta, propagate, func) {
    this.char = char;
    this.ctrl = ctrl;
    this.meta = meta;
    this.propagate = propagate;
    this.func = func;
  }

  /**
   * 条件に合致したか判定して、合致した場合のみ登録された関数を実行
   * @param {String} char 
   * @param {Boolean} ctrl 
   * @param {Boolean} meta 
   */
  ignite(event, char, ctrl, meta) {
    if (char === this.char && ctrl === this.ctrl && meta === this.meta) {
      this.func();
      if (!this.propagate) {
        event.preventDefault();
        if (event.stopPropagation()) {
          event.stopPropagation();
        }
      }
    }
  }

}
class ShortcutController {
  /**
   * コンストラクタ
   */
  constructor() {
    this.shortcutList = [];
  }

  /**
   * ショートカットを登録する 
   * @param {String} keybind: キーバインドを表す文字列, 例) Ctrl+S
   * @param {Function} callback: ショートカットが押されたときに呼び出す関数
   * @param {Boolean} propagate : デフォルトのショートカットを呼び出すか。
   */
  registerShortcut(keybind, callback, propagate=false) {
    // + で区切ってすべて小文字のリストに
    // 例)Ctrl+A => [ctrl, a]
    const keys = keybind.split('+').map((s) => s.toLowerCase());

    // キーバインドをパース
    let needCtrl = false;
    let needMeta = false;
    let char = null;
    keys.forEach((k) => {
      if (k === 'ctrl') {
        needCtrl = true;
      } else if (k === 'meta') {
        needMeta = true; 
      } else if(allowedCode(k)){
        char = k;
      } else {
        // 使用できないキーが含まれている
        throw new Error('Irrelvalent Shortcut');
      }
    });
    if (char === null) {
      throw new Error('Irrelvalent Shortcut');
    }

    // ショートカットをリストに追加する
    const shortcut = new ShortcutObject(char, needCtrl, needMeta, propagate, callback);
    this.shortcutList.push(shortcut);
  }

  /**
   * addEventListenerようのハンドラー
   * 
   * @param {Event} event 
   */
  handle(event) {
    const ctrl = event.ctrlKey;
    const meta = event.metaKey;
    const char = event.key.toLowerCase();

    this.shortcutList.forEach((shortcut) => {
      shortcut.ignite(event, char, ctrl, meta);
    });
  }
}

export default {
  ShortcutController,
}

割と短く実装出来た。流し読みして見切り発射の実装なので何か漏れがあるかも。上でも書いたがshortcut.jsではブラウザ周りのバグなどにも対応してそうなので、特段の主義主張が無いのであればこちらを使うのがおすすめです。

mikebird28.hatenablog.jp

mikebird28.hatenablog.jp

個人開発サービスの記事周りをWordpressに切り出した。

タイトルの日本語が怪しい気がします。コツコツ開発しているanntの話です。

anntはFirebaseで動いていて基本的には静的なページで動いていたので、チュートリアルなどの記事を書くときも直接htmlタグ付けしていくという苦行をしていました。最近さすがに当時の自分は何を考えてこんなの作ったんだ?と思い始めたので記事や頻繁に編集する部分をWordpressに切り出しました。

利用する環境

とにかく楽に環境を整えたかったのでWordpressGCP Market Placeで立てようと決めていました。GCP Market PlaceでWordpressを立てるのは本当に楽でクリックしていくだけで新しいインスタンスWordpressが動いている状態になります。Google様々です。

cloud.google.com

f:id:theflyingcat28:20201209002043p:plain
本当に運用開始ボタンひとつで起動できます。

全体の構成として当初はannt.aiに来た/post以下のリクエストだけをうまくWordpressに回せないか検討していました。色々方法を模索しましたがFirebaseはあくまで静的ファイルのホスティングやサーバレスでシンプルなAPIを実現するためのサービスです。来たリクエストの一部を別のサーバーに流すようなロードバランサ的な機能は簡単には実現できなさそうでした。

Firebaseの前に別のロードバランサ挟んでリクエスト割り振ればいいのでは?と思いもしましたが更に課金するのは嫌だなという気持ちになりました。嘘です。本当は面倒くさかっただけです。なので更に別の方法を模索しました。

最終的な構成

結論としてサブドメイン取ってサービス本体とは完全に切り離して運用することにしました。本体のサービスとは完全に分かれているので管理が楽です。何かに負けている気がしますが気にしません。大事なのはスピードです。

GCP Market Placeで作成されたインスタンスにはIPアドレスが振り分けられますがデフォルトだとエフェメラルという設定になっていて、インスタンスを起動停止するたびに別のIPに変わってしまう可能性があります。最終的にいい感じにインスタンスに対してサブドメインを設定するには、

  • 静的IPアドレスに切り替える
  • ネームサーバーに設定を追加
  • TLSの設定

こんな感じの作業をやっていく必要があります。

静的IPアドレスの設定

調べるとたくさん記事が出てきます。 Google Cloud Platformの外部IPアドレスのURLから静的アドレスに変更できます。 条件によっては課金されるので注意してください。

GCPで作成したWordpressの静的IPアドレスの設定方法 - Qiita

ネームサーバーの設定変更

利用しているレジストラ(お名前.comやGoogleドメイン等)によってやり方は違うと思います。 やることとしては新しいサブドメインと作成した静的アドレスを対応付けるAレコードを追加するだけで大丈夫なはずです。

TLSSSL)の設定

今はTLSが正しい表現になるのでしょうか。初めてTLSの設定を行いましたがCertbotというツールを使えばLet's encryptの証明書を簡単に手に入れられます。

GCP Market PlaceでWordpressを立ち上げるとおそらくDebianApacheで動いていると思います(今後変わる可能性もあるので注意してください)。 自分はaptでcertbotをインストールしましたが(うろ覚え)CertbotのトップページにOS+サーバーを指定すれば導入方法を親切に教えてくれるので、それに従うのが一番確実かと思います。Wordpress+GCP Market Place + TLSググると何個か日本語の資料も引っかかりますが古いのかCertbotが正しく動作しなかったので素直に公式に従うのがいいと思います。 certbot.eff.org

そんなこんなで最後には無事Wordpress環境を整えることが出来ました。本当に楽にセットアップできるので、雑にブログ or それに類する記事執筆システム作るにはおすすめです。

↓ 作ったもの。

https://post.annt.ai/

Pascal VOCのフォーマットに関する調査

anntのpythonライブラリにpascalVOCの読み込み&書き込み機能を実装を検討中。 実装にあたって具体的な仕様を調べてみたもののあまり厳密に決まってなさそう。 調べてなんとなくわかったこととかメモなどを記載していく。

調べるにあたって参考にした資料

フォーマットの例

Pascal VOC 2007から抜粋したxmlファイルの例を記載する。

<annotation>
    <folder>VOC2007</folder>
    <filename>voc2007.jpg</filename>
    <source>
        <database>The VOC2007 Database</database>
        <annotation>PASCAL VOC2007</annotation>
        <image>flickr</image>
        <flickrid>325991873</flickrid>
    </source>
    <owner>
        <flickrid>archintent louisville</flickrid>
        <name>?</name>
    </owner>
    <size>
        <width>500</width>
        <height>375</height>
        <depth>3</depth>
    </size>
    <segmented>0</segmented>
    <object>
        <name>chair</name>
        <pose>Rear</pose>
        <truncated>0</truncated>
        <difficult>0</difficult>
        <bndbox>
            <xmin>263</xmin>
            <ymin>211</ymin>
            <xmax>324</xmax>
            <ymax>339</ymax>
        </bndbox>
    </object>
    <object>
        <name>chair</name>
        <pose>Unspecified</pose>
        <truncated>0</truncated>
        <difficult>0</difficult>
        <bndbox>
            <xmin>165</xmin>
            <ymin>264</ymin>
            <xmax>253</xmax>
            <ymax>372</ymax>
        </bndbox>
    </object>
    <object>
        <name>chair</name>
        <pose>Unspecified</pose>
        <truncated>1</truncated>
        <difficult>1</difficult>
        <bndbox>
            <xmin>5</xmin>
            <ymin>244</ymin>
            <xmax>67</xmax>
            <ymax>374</ymax>
        </bndbox>
    </object>
    <object>
        <name>chair</name>
        <pose>Unspecified</pose>
        <truncated>0</truncated>
        <difficult>0</difficult>
        <bndbox>
            <xmin>241</xmin>
            <ymin>194</ymin>
            <xmax>295</xmax>
            <ymax>299</ymax>
        </bndbox>
    </object>
    <object>
        <name>chair</name>
        <pose>Unspecified</pose>
        <truncated>1</truncated>
        <difficult>1</difficult>
        <bndbox>
            <xmin>277</xmin>
            <ymin>186</ymin>
            <xmax>312</xmax>
            <ymax>220</ymax>
        </bndbox>
    </object>
</annotation>

フォーマットの説明

1つのファイルに対して1つのアノテーション情報を記載したxmlファイルが用意されている。 xmlファイルは'annotation'タグで始まっていて、直下にメタデータに関するタグや、具体的なアノテーション情報が記載されているobjectタグなどが配置されている。

タグ名 説明
folder 画像が格納されているフォルダ名
filename アノテーションした対象の画像ファイル名
source (不明)情報源に関する情報?
owner (不明)画像の所有者に関する情報?
size タグ名通り画像サイズに関する情報(幅、高さ、チャネル数)
segmented (不明)0のことが多い気がする。
object 肝心のアノテーション情報。詳細は次の段落で説明

objectタグについて

objectタグには肝心な画像内部に写っている物体の情報が記載されています。 各タグの説明は上述のガイドラインに従って記載しています。

タグ名 説明
name 写っている物体のクラス名
pose おそらくアクション分類タスク用の人物の行動を表すタグ
truncated 物体が15~20%以上画像からはみ出しているか
difficult 認識するのが難しいオブジェクト。e.g. 理解に文脈が必要な画像等
occluded 2008以降追加 物体の5%以上が何かに隠れているか
bndbox バウンディングボックスの位置情報
part (不明)ポーズに関する情報?
actions 写っている人物の行動に関する情報。行動推測タスク用
point (不明)

殆ど不明となってしまった。明確なフォーマットが見つからずライブラリ作るのが大変そう。正直使う上で必要なタグは限られているので不要な月は実装しなくて良い気がする。 適宜、修正・追記していく予定。

AtCoder色変記事(緑)。あるいは趣味としての競プロの勧め。

何番煎じか分かりませんがこの度晴れてAtCoderで緑帯に突入することが出来ました。 もう巷には緑になるために必要な修行方法が溢れているので、「緑になるために何をしたか」ではなくて、「趣味としての競プロ楽しいよ!」という内容にフォーカスを当てて記事を書こうかなと思います。

簡単に自己紹介

院卒社会人2年目です。理系だったものの学科も勤め先もITやプログラミングとは程遠くプログラミングは完全趣味でやっています。大学時代はコツコツ独学でAIなどの勉強をしてました。その延長でkaggleなどにも参加していましたが、あまりまとまった時間が取れずメダルを取ること無く引退してしまいました。

競プロに出会ったきっかけは覚えていませんが、初めて参加したきっかけは腕試し目的でした。独学ですがそれなりにプログラムを数書いてきた自負があったので緑くらい数回参加すればなれるでしょ、と思っていましたが初参加(確か企業コン)で現実を思い知らされました。以降悔しさから積極的に参加するようになって今では虜です。

競プロって何?

自分が説明するよりWkipediaを見ていただいたほうが正確で早いです。

ja.wikipedia.org

あえて自分の言葉で説明すると、「数問の問題が出されるので、コンテスト時間内に問題を解くプログラムを正確かつ素早く解く競技」になるんでしょうか。AtCoder社が開催するABCというコンテストを例にすると、100分という制限時間の6問(A~F)の問題が出題されるので、自らの知識・技術・経験を総動員してできる限り多くの問題を素早く解くことを競います。1問目(A問題)は非常に簡単ですが、後半になると深いアルゴリズムの知識と考察力が必要とされるので全問解けるのは多くの場合参加者のごく一部です。

コンテストサイトによって細部は異なるかもしれませんが、大手のコンテストサイトには実力を示すレーティングの仕組みがあります。レーティング帯に対応する色が定義されていて、この色によってざっくりとその人の実力が分かるようになっています。AtCoderだとレーティング0~400が灰、400~800が茶、800~1200が緑と400ごとに区切りが設定されていて、初心者は多くの場合灰〜茶帯を彷徨うことになります。

chokudai.hatenablog.com

競プロの魅力

で、それって面白いの?という話ですが、めちゃくちゃ楽しいです。楽しすぎて最近はABCには殆ど毎回欠かさず参加しています。これまで何で競プロが楽しいと思うのか?について言語したことがなかったので何とか言葉にしてみました。

達成感

個人的にはこれが一番のモチベーションです。おそらくですが最初数回のコンテスト参加で適正なレーティングに配置されます。アルゴリズムを少し齧ったことがある、程度だと多くの場合良くて茶色になるのではないかと思います。一度自分の適正レーティングに到達したあとに次の色を上げるためにはかなりの努力が必要です。

コンテストに参加しても思うように解けずレートが下がって(よく冷えるとか言われてます)ぐぬぬ...となります。Twitterで他の人が色変してると嫉妬で血の涙が出そうになります。それでも負けじとコンテストに参加し続けて次の色に到達したときはめちゃくちゃ嬉しいです。アドレナリンだかエンドルフィンだか分からないですけど脳内で謎の汁がいっぱい出ます。緑になったときは無意味にシャドーボクシングしてました。

f:id:theflyingcat28:20201109221837p:plain
入緑嬉しい

努力が身になっているのが実感できる

競プロはコンテスト自体は2時間程度です。それ以外の時間は過去問を説いて勉強したり、解けなかった問題を振り返ったりという修行の時間になります。コンテスト自体は頻繁に開催されているので、努力したら努力した分見える形で結果として帰ってきます。最初自分は数学系の問題だったり幅優先探索系が苦手過ぎてコンテストの度に絶望する日々を送っていたのですが、精選100問やったり類題解きまくったりしてると「はいはいこのパターンね」みたいな感じになって俺TUEEEEE(弱い)が出来ます。

f:id:theflyingcat28:20201109222354p:plain
有志によって運営されるAtCoder Problemsで修行の成果が見れます

短期間で努力の結果がフィードバックされるのはモチベーションの維持としては良い仕組みだなーと思って修行してます。kaggleの数ヶ月に渡るマラソン的なコンテストと違って競プロは短時間なので忙しくて修行できなくても、落ち着いた段階で気楽に再開できるのも良い点だなと思ってます(この辺は個々人の好みによるところが大きいかもしれないです)。

感情を揺さぶられる

上2つの項目で書いたことの言い換えになってしまっているところはありますが、コンテストに参加すると感情が揺さぶられます。今までD問題までしか解けなかったときにE問題まで解けたときは嬉しくて情緒不安定になりました。逆にC問題が解けずレートが下がったときに回答を見たらめちゃくちゃ簡単な問題だったときは辛くて情緒不安定になりました。こんな感じでコンテストに参加していると喜怒哀楽大体の感情が楽しめます。コンテストに参加することによって平坦な生活に刺激が発生します。

コンテスト終わったあとにTwitter眺めているのも楽しいです。コンテスト後は喜んでる人、落ちこんでる人、解説してくれる人、いろんな人が居てお祭りみたいな感じです。普段Twitter見ないだけかも知れませんが。

社会人にこそおすすめの趣味

なんか参加している人学生が多そうだし社会人になってから始めるのもな...と感じているかも知れません。というか自分がそうでした。大して時間も取れないし学生に遅れを取りそうと思っていました。でも実際に初めて見ると社会人の方もたくさんいらっしゃいますし、時間的な面もあまりハンデになっていないなと思っています。

AtCoderのコンテストであれば大体土日の夜開催

初心者向けのABCを始めとしてAtCoderはだいたい土日の夜に開催されます。飲み会とかをセットしない限り独身であれば高確率で参加できます。突如コンテストが生えることもありますが、基本的に数日前には開催がFIXしてるのでそれなりに予定も組みやすいです。

隙間時間でも修行できる

修行するしないは個人の自由なので、仕事終わりに一日一問みたいな感じに練習してみてもいいですし、土日にまとめて修行みたいな感じでも好きなようにできます。自分はやる気がある日にぐぉーって自分の適正より難しい問題を数問説く感じでやっています。

というような感じで割と柔軟にできます。そういう意味ではまとまった時間が取りにくい社会人にこそおすすめの趣味なのでは?と思っています。

実利的な話をするとAtCoder Jobsなどで転職の助けになったりすることもあるみたいです。あとアルゴリズムの知識が仕事の役に立つかもしれないです。競プロは仕事の役に立たない!という意見も見かけますが、職場に寄るところが大きい気がします。「役にたたない...」と思ってやるよりは「役に立つ!」と思ってやったほうが確実にモチベーションは繋がるので役に立つと思っておくのがいいかと思います。

始めてみたい!

始めましょう。まずは日本語で参加できるAtCoderのコンテストがおすすめです。初回参加して絶望したら先人の色変記事を読みましょう。やるべきことが見えてきます。おすすめの記事を貼っておきます。

ntk-ta01.hatenablog.com qiita.com qiita.com

終わりに

もう色変記事じゃない気がしてきました。それにAtCoderの回し者みたいになってしまいました。この記事を見てAtCoder見て始めてくれる人がいたら嬉しいです。こんな楽しみ方もあるよ!という意見があれば是非記事にしてください!

多角形の内外判定(javascriptでの実装を添えて)

アノテーションツールでセグメンテーション用に多角形での範囲選択を実装しようとしている。 四角形の場合には簡単に内外判定ができるが、多角形だとそう簡単にはいかない。 今回実装にあたって多角形の内外判定を勉強した。

f:id:theflyingcat28:20200919204057g:plain
完成した多角形の描画&内外判定機能

TL;DR

  • Winding Number Algorithmは割と楽に実装できる
  • 実装するときは角度の符号の計算を忘れない

はじめに参考資料

理論だったり詳しい実装のポイントは以下の資料を見てください。 この資料を見て理解できればこの記事は読まなくて大丈夫です。

www.nttpc.co.jp

Winding Number Algorighmとは

多角形の角2つと対象となる点が成す角度を順番に計算していき、それらの和が360°以上であれば(一周以上するのであれば)点は多角形内部にある。 一方で和が0であれば、点は多角形外部にある。

最初角度の和が0になればってどういうこと?と悩んだが、角度を符号つきで計算すると外部にあるときは角度同士がキャンセルしあって和は0になるよ、と理解すれば良さそうだ。

「360°以上」というところは自己交差領域があると和が一周以上になる場合が出てくる(上記のNTTの資料参考)。あまり数学的なところは理解できないが他にありえるのだろうか。

f:id:theflyingcat28:20200923002137j:plain
簡単な図解

実装する上での注意点

角度を符号付きで計算することを忘れなければ実装はかなり簡単。符号の計算を忘れるとうまく動かなくて沼にハマる。 角度の絶対値はベクトルの内積を使えば簡単に計算できる。符号は外積を計算して、その符号だけを取ればOK。

javascriptでの実装

コメントを多めに入れたので長くなったが、やっていることは非常に簡単。 バグがあれば教えてください。

/**
 * 指定した点が多角形の中に含まれるかどうかを判定する
 *
 * @param {Array} polygon 多角形を構成する点(X, Y)の配列. 多角形を一周するように並んでいる必要がある.
 * @param {Number} x 判定する点のX座標
 * @param {Number} y 判定する点のY座標
 * @returns {Boolean} 点が多角形に含まれていればtrue
 */
function hit(polygon, x, y) {
  let thetaSum = 0;
  const n = polygon.length;

  // i-1 => 指定された点 =>  i の成す角度の和をthetaSumに足し込んでいく
  for (let i = 1; i < n; i++) {
    if (polygon[i][0] === x && polygon[i][1] === y) {
      // 指定された点が多角形の角の場合うまく角度が計算できないので、判明した時点でtrueを返す
      return true;
    }
    const v1x = polygon[i - 1][0] - x;
    const v1y = polygon[i - 1][1] - y;
    const v2x = polygon[i][0] - x;
    const v2y = polygon[i][1] - y;
    thetaSum += computeDegree(v1x, v1y, v2x, v2y);
  }
  // 0とN番目の成す角度
  if (polygon[0][0] === x && polygon[0][1] === y) {
    return true;
  }
  const v1x = polygon[n - 1][0] - x;
  const v1y = polygon[n - 1][1] - y;
  const v2x = polygon[0][0] - x;
  const v2y = polygon[0][1] - y;
  thetaSum += computeDegree(v1x, v1y, v2x, v2y);
  thetaSum = Math.abs(thetaSum);

  if (thetaSum >= 0.1) {
    return true;
  }
  return false;
}

/**
 * 2つのベクトルの成す角度を符号付きで計算する
 * 角度の絶対値の計算には内積を、符号の計算には外戚を利用する 
 *
 * @param {Number} x1 
 * @param {Number} y1 
 * @param {Number} x2 
 * @param {Number} y2 
 */
function computeDegree(x1, y1, x2, y2) {
  const abs1 = Math.sqrt(x1*x1+y1*y1);
  const abs2 = Math.sqrt(x2*x2+y2*y2);
  let theta = Math.acos((x1*x2+y1*y2)/(abs1*abs2)); // 内積を使って角度を計算
  let sign = Math.sign(x1*y2-y1*x2); // 外積を使って符号を計算
  theta *= sign;
  return theta
}

function main() {
  const pol1 = [[0, 0], [100, 20], [100, 120], [0, 100]];
  // 通常の判定
  console.log(hit(pol1, 50, 50)); // true
  console.log(hit(pol1, 101, 50)); // false
  console.log(hit(pol1, 100, 121)); // false

  // コーナーケース(多角形の辺上の場合)
  console.log(hit(pol1, 50, 10)); // true
  console.log(hit(pol1, 100, 60)); // true

  // コーナーケース(多角形の角上の場合)
  console.log(hit(pol1, 0, 0)); // true
  console.log(hit(pol1, 100, 20)); // true
}

main();

mikebird28.hatenablog.jp

mikebird28.hatenablog.jp

anntを大きくリファクタした

前公開したanntをちまちま改修してたのでリリース

https://annt.ai/ja

前:

f:id:theflyingcat28:20200906203927j:plain
改修前

後:

f:id:theflyingcat28:20200906203758p:plain
改修後

主な改修内容は

の3点

Google Drive対応

Google DriveDropboxと違って、階層構造のファイルシステムじゃないので実装が手探りになってしまった。 Google Driveは普段使い慣れているWindowsMacみたいにパスでファイルやフォルダを指定できない。(フラットファイルシステムというらしい) 各ファイルが親フォルダの情報を持っているので、SQLのSELECT~WHERE文みたいなイメージで指定したフォルダを親とするファイルをリストアップする。

mikebird28.hatenablog.jp

深いパスのファイルを取得しようとすると階層分だけAPIを呼び出さないといけないので、余計なオーバーヘッドが発生している気がする。 Dropboxに比べて遅いのと、若干バグが多いのでここは今後の課題。 あとプライバシーポリシー作ってないので、OAuthの認証画面でやばいですよアピールされるのも課題

内部的なリファクタリング

リファクタリングをやって大分今後の改修がしやすくなった。 今後多角形のアノテーションの実装を検討しているが、以前までの絶望感が無い。 この調子なら割とすぐに実装できそうなので、次の課題にしてみようと思う。

デザイン的な変更

デザインの変更は前からの課題だったので、割と大きく直したつもりでいる。 前回のデザインをださいださいと思ってて直したが、直しても相変わらずしょっぱい気がしている。 バウンディングボックスがすごい見ずらいのでこれもどうにかしたい。

小さな改善点としてはタブ操作だけでタグを選択できるようになった点。 Vue使えばかんたんなインクリメントサーチを本当に楽に実装できる。 公式チュートリアルに例があるので、興味ある型はそちらを参照。

f:id:theflyingcat28:20200906203202g:plain
タグのインクリメントサーチ

今後もちまちま直していこうと思う。

twitterはじめました ⇒ @toritoritori29

Google Drive APIでファイルをアップロード - Multipart/Related

前回に引き続きGoogleDriveAPIのお話です。 今回はファイルの新規アップロードについてです。

createエンドポイント

ファイルを新規にアップロードするにはcreateエンドポイントを使用します。

https://developers.google.com/drive/api/v3/reference/files/create

このエンドポイントが曲者で、アップロード時に3つのアップロードタイプから一つを選んで明示的に指定する必要があります。

  • media - ファイルのみをアップロード
  • multipart - ファイルとメタデータを同時にアップロード
  • resumable - 複数回のリクエストでファイルとメタデータをアップロード

現実問題としてメタデータ無しでファイルをアップロードしたいという局面は殆ど無いと思われます。Google Driveではフォルダの階層構造をファイルのメタデータで管理しているため、メタデータ抜きでは親ディレクトリの指定すらできません。

ということで多くの場合においてアップロードタイプとしてmultipartかresumableを指定することになるでしょう。複数回リクエストを投げるよりは一回のリクエストで片付けてしまいたいので、自分の中でmultipartを使ってみようということになりました。ところがこのmultipart、RFC 2387というプロトコルに則ってリクエストを作成する必要があります。日本語の文献もほぼ無いということで素直にRFCを流し読みしました。

RFC 2387 The MIME Multipart/Related Content-type

tools.ietf.org

複数の異なるMimeTypeのデータをまとめてアップロードするための「Multipart/Related」というMimeTypeについてまとめてたものです。あまりしっかりと読めているわけではないですが、要点は以下の通りとなっています。

  • Content-TypeでMultipart/Relatedを指定し、同時にBoundary(境界文字列)も指定する。
  • リクエストボディは"--"+Boundaryで区切って、異なるデータのリクエストボディを指定する。
  • リクエストの終わりは"--"+Boundary+"--"で締める

これだけだと抽象的で少し分かりづらいですが、RFCの原本に載っている例がわかりやすいです。

Content-Type: Multipart/Related; boundary=example-1 start="950120.aaCC@XIson.com"; type="Application/X-FixedRecord" start-info="-o ps"

 --example-1
 Content-Type: Application/X-FixedRecord
 Content-ID: <950120.aaCC@XIson.com>

 25
 10
 34
 10
 25
 21
 26
 10
 --example-1
 Content-Type: Application/octet-stream
 Content-Description: The fixed length records
 Content-Transfer-Encoding: base64
 Content-ID: <950120.aaCB@XIson.com>

 T2xkIE1hY0RvbmFsZCBoYWQgYSBmYXJtCkUgSS
 BFIEkgTwpBbmQgb24gaGlzIGZhcm0gaGUgaGFk
 IHNvbWUgZHVja3MKRSBJIEUgSSBPCldpdGggYS
 BxdWFjayBxdWFjayBoZXJlLAphIHF1YWNrIHF1
 YWNrIHRoZXJlLApldmVyeSB3aGVyZSBhIHF1YW
 NrIHF1YWNrCkUgSSBFIEkgTwo=

 --example-1--

HTTP本体のリクエストヘッダにはMultipart/Relatedを指定しています。startなどはどのデータから読み込めばいいかを示す情報のようです(それっぽいことが書いてありましたが良く理解できませんでした)。リクエストボディの各セクションでは、それぞれのデータに対して改めてヘッダを定義しています。

javascriptでの実装

Multipart/Relatedのリクエストボディを生成する関数を作成した。この関数でエンコードした文字列をリクエストボディにして、リクエストヘッダにはMultipart/Relatedを指定すれば

/**
 * 引数で指定したオブジェクトをMultipart/Relatedのリクエストボディに変換
 * 以下のような配列を第一引数として指定する必要がある
 * e.g)
 * let objects = [
 *   { header: ['Content-Type': 'text/plane'], body: '内容1' },
 *   { header: ['Content-Type': 'text/plane'], body: '内容2' },
 * ]
 * 
 * @param {Array} エンコードしたいオブジェクトのリスト 
 * @param {String} 境界文字列
 * @returns {String} エンコード済みの文字列
 */
function encodeMutipart(objects, separator) {
  let encoded = '';
  for (let o of objects) {
    encoded += `--${separator}`;
    encoded += '\n';
    Object.keys(o.headers).forEach((key) => {
      encoded += key;
      encoded += ': ';
      encoded += o.headers[key];
      encoded += '\n';
    });
    encoded += '\n';
    if (typeof o.body === 'object') {
      encoded += JSON.stringify(o.body);
    } else if (typeof o.body === 'string') {
      encoded += o.body;
    }
    encoded += '\n';
  }
  encoded += `--${separator}--`;
  return encoded;
}

mikebird28.hatenablog.jp