機械学習下準備〜CaboChaで係り受けの解析〜

CaboChaインストール

取り敢えず形態素解析はできたので文法沼に沈む前にCaboChaのインストールに移ります。
この時点で「係り受けの判定が入ったらまた大幅に書き直すことになるのでは?」と気が付きちょっと暗澹としています。
目論見としては

  1. 係り受けを意識した主語の判定
  2. 係り受けを意識した述語/形容の判定

あたりができるようになるといいなと。
「佐藤さんの家の猫は白くて可愛い」のような文章があったときに、「佐藤さんの家の猫」「白い」「可愛い」あたりを取得したいのだけど、MeCabだけでやると「猫」「白い」「可愛い」になってしまうのでこの辺の調整です。猫は必ずしも白くない、みたいな。例文が思いつかなくて青空文庫をうろうろしましたがちょうどいい文章が見つからなかった……。

参考サイトはこちら:係り受け解析器CaboChaをPythonから使う – Spot
2016年で更新止まってるし見た感じ消し忘れた過去のページって雰囲気(ヘッダのリンク切れてるし)なのでここ近い内に消えるのかもな……

CaboChaオフィシャルサイトはこちら:CaboCha/南瓜
titleタグがカオボチャになっとる……なんだか悲惨な響き……っていうかこれ最終更新15年って大丈夫か? と思ったらダウンロードページには17年版がありました。サイトが更新されてないだけか。

インストールコマンド

$ tar xvfz cabocha-0.69.tar.bz2 
$ cd cabocha-0.69
$ ./configure --with-mecab-config=`which mecab-config` --with-charset=UTF8
$ make && make check
$ sudo make install
$ cd python
$ python setup.py build
$ python setup.py install

tarコマンドは専門学校時代から合わせて八年は叩いてるはずなんですが未だにxvfz部分が覚えられない。頻度が低いとはいえそろそろ何も見ずに打てるようになりたい……。

んでまあインストールが終わったのでコンソールから適当に叩いてみたのですけど

python
Python 3.6.5 (default, Aug  6 2018, 11:02:37) 
[GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import CaboCha
>>> import itertools
>>> cp = CaboCha.Parser('-f1')
>>> tree = cp.parse('東京のラーメン屋がいつも混雑しているわけではない')
>>> tokens = [tree.token(i) for i in range(0, tree.size())]
>>> print(tokens)
[<CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c02b600> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c02b090> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c02b630> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c02b660> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3060> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3240> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3300> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b32d0> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3330> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b33f0> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3420> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3450> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3480> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b34b0> >]

んん。
MeCabのようにサクッと中身が見えたりはしません。参考サイトのソースを拝借してcabocha.pyとしてファイル保存してテストします。
どうでもいいけどかぼちゃパイって美味しそうですね。ふかした南瓜をなめらかに潰してはちみつとかで甘みを足して、焼いた後でかけるのは生クリームでもカラメルソースでもいいですね。中身に歯ごたえがないので生地にかぼちゃの種とかナッツ類を混ぜ込んでも美味しいと思います。何の話?

ただし参考サイトのソースはPython2系で書かれているらしく、そのまま動かそうとしても 'filter' object is not subscriptable と言われて落ちます。

Python3用に書き直したソース

# -*- coding: utf-8 -*-
import CaboCha
import itertools

def chunk_by(func, col):
    '''
    `func`の要素が正のアイテムで区切る
    '''
    result = []
    for item in col:
        if func(item):
            result.append([])
        else:
            result[len(result) - 1].append(item)
    return result

def has_chunk(token):
    '''
    チャンクがあるかどうか
    チャンクがある場合、その単語が先頭になる
    '''
    return token.chunk is not None

def to_tokens(tree):
    '''
    解析済みの木からトークンを取得する
    '''
    return [tree.token(i) for i in range(0, tree.size())]

def concat_tokens(i, tokens, lasts):
    '''
    単語を意味のある単位にまとめる
    '''
    if i == -1:
        return None
    word = tokens[i].surface
 -     last_words = map(lambda x: x.surface, lasts[i])
 +     last_words = [x.surface for x in lasts[i]]
    return word + ''.join(last_words)

raw_string = u'東京のラーメン屋がいつも混雑しているわけではない'

cp = CaboCha.Parser('-f1')
tree = cp.parse(raw_string)
tokens = to_tokens(tree)

 - head_tokens = filter(has_chunk, tokens)
 + head_tokens = [token for token in tokens if has_chunk(token)]
 - words = map(lambda x: x.surface, head_tokens)
 + words = [x.surface for x in head_tokens]

lasts = chunk_by(has_chunk, tokens)

 - links = map(lambda x: x.chunk.link, head_tokens)
 + links = [x.chunk.link for x in head_tokens]
 - link_words = map(lambda x: concat_tokens(x, head_tokens, lasts), links)
 + link_words = [concat_tokens(x, head_tokens, lasts) for x in links]

for (i, to_word) in enumerate(link_words):
    from_word = concat_tokens(i, head_tokens, lasts)
    print("{0} => {1}".format(from_word, to_word))
$ python cabocha.py
東京の => ラーメン屋が
ラーメン屋が => 混雑しているわけではない
いつも => 混雑しているわけではない
混雑しているわけではない => None

よろしい。
次回はこれを人工知能に組み込んでいきます。

続きを読む...

機械学習下準備〜MeCabで形態素解析〜

MeCabインストール

参考サイトはこちら:今更ながらPythonとMeCabで形態素解析してみた – イノベーション エンジニアブログ

$ brew install mecab-ipadic

$ mecab
すもももももももものうち
すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も   助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も   助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
の   助詞,連体化,*,*,*,*,の,ノ,ノ
うち  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

すっもっもっもっももっもっ。
あとMeCabをPythonから使うために mecab-python3 も入れます。

$ pip install mecab-python3

$ python
Python 3.6.5 (default, Aug  6 2018, 11:02:37) 
[GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import MeCab
>>> m = MeCab.Tagger()
>>> print(m.parse("すもももももももものうち"))
すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も   助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も   助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
の   助詞,連体化,*,*,*,*,の,ノ,ノ
うち  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

参考サイトのソースをMeCabで書き直し

先人のリソースありがたやありがたや。
ちなみに途中からやりたかったのでMasterからコピーするのではなく途中のコミットからコピーしてます。→79b4b990da6220543208879551737ac31e6cddf0
一番困ったのはjanome.tokenizer.Tokenオブジェクトが無くなったことです。これにより t.surface とかのあたりが使えなくなり、かつMeCabの解析結果がStringだったのでなんかごにょごにょとパースするはめになりました。このあたりはjanomeの方が便利なんだなという印象。
Pythonの標準では複数文字を指定したsplitもできなかったのでreを使って下記のごとく処理しました。

[re.split('[\t,]',line) for line in re.split('[\n]',m.parse("すもももももももものうち"))]

"""結果"""
[
    ['すもも', '名詞', '一般', '*', '*', '*', '*', 'すもも', 'スモモ', 'スモモ'], 
    ['も', '助詞', '係助詞', '*', '*', '*', '*', 'も', 'モ', 'モ'], 
    ['もも', '名詞', '一般', '*', '*', '*', '*', 'もも', 'モモ', 'モモ'], 
    ['も', '助詞', '係助詞', '*', '*', '*', '*', 'も', 'モ', 'モ'], 
    ['もも', '名詞', '一般', '*', '*', '*', '*', 'もも', 'モモ', 'モモ'], 
    ['の', '助詞', '連体化', '*', '*', '*', '*', 'の', 'ノ', 'ノ'], 
    ['うち', '名詞', '非自立', '副詞可能', '*', '*', '*', 'うち', 'ウチ', 'ウチ'], 
    ['EOS'], 
    ['']
]

なんかEOSとか空行とかいらないものもありますがこの辺は手動でアレしましょう。

修正前(janome)

def analyze(text):
    """文字列textを形態素解析し、[(surface, parts)]の形にして返す。"""
    return [(t.surface, t.part_of_speech) for t in TOKENIZER.tokenize(text)]

修正後(MeCab)

def analyze(text):
    """文字列textを形態素解析し、[(surface, parts)]の形にして返す。"""
    tokens = [re.split('[\t,]',line) for line in re.split('[\n]',Dictionary.TAGGER.parse(text))]
    return [(t[0], ','.join([t[1], t[2], t[3], t[4]])) for t in tokens if len(t)==10]

ゴミを取り除くために if len(t)==10 とか書いてるんですけどどうなんですかねこれ。初心者なのでいまいち正しさがわかりません。Pythoner(Pythonist?)てきには気持ち悪い書き方だったりするかしら。
おとなしくオブジェクトクラス自作しちゃった方がソースが綺麗だったかもしれない……。後の課題にしましょう。
ちょっと動かしてみた感じだと形容動詞連体詞周りがヤバい。「大きな桃」だと連体詞判定、「大きい桃」だと名詞,形容動詞語幹になるのでこのあたりをパースして辞書に突っ込むのがはちゃめちゃに面倒臭そう。

形態素解析周りで「すもももももももものうち」をいっぱい見たせいで最強○×計画を聞きたくなり最強○×計画→ふぃぎゅ@メイト→巫女みこナースからニコニコ組曲に飛んでYoutubeの再生履歴がここだけ00年代。

続きを読む...

バーチャル彼女を造る〜機械学習編〜

 モデルの調整はまだまだ終わりませんが(というか終りが見えない)取り敢えず中身の作成に入ります。
 今回は概ねキャラクターの方向性を決め打ちで作るので人工知能とはいえ学習もある程度恣意的に行います。

  • 一人称および語尾などの情報はパターン辞書で管理する
  • よつばちゃんの好み(甘いものが好き、苦いものは苦手)については連想辞書で管理する
  • 知らない単語(辞書にない単語)については聞き返し、説明を覚える(連想辞書に記録する)

 書き出してみると機械学習の出番あんまり無いな……。中の人が低学歴なのでいまいち活かしきれていない感じがします。一応学習は組み込む予定ですけど教わったこと鵜呑みにするアホの子になるのはちょっと避けがたい感じもします。理論を組めば組むほど五感ってすごいな〜と思わざるを得ない。こんなもんがプリインストールされてる人体しゅごい。

 閑話休題。

 基本的には「人との文章コミュニケーションのみに頼って知識を蓄積するAI」になります。
 こうなるとちょっと問題なのが、基本的にこのAIは「ユーザーと一対一の会話」しかインプットとして扱えないということです。つまり、「ただいま」に対しては「おかえり」と返すのだというようなことを学べない。コミュニケーションのお手本が無いわけです。
 このあたりは人力で辞書作っちゃってもいいかな〜とか思ってるんですがさて人工知能とは?
 まあ本題は「可愛い彼女を造る」なので人工知能である必要もぜんぜん無いんですけどね。でもほら、タップすると一定の返事が返ってくるだけのアプリとか寂しいしちょっとランダム感ほしいじゃない? 「おはよう、私のセイ」とか「虹色カノジョ2d」とかちまちま触りはしたんですけどそのあたり寂しい。疎通できてる感じが足りないというか。Siriさんのレベルまで行けなくても多少ああいう疎通できてる感が欲しい。
 特に挨拶回りは形態素解析とかじゃどうにもならない感じがするので、かつ絶対ほしい機能なのでなんとかします。ただいまって言ったらおかえりとかお疲れ様とか言われたい。人工知能でいいから労ってほしい。

辞書について

 前述の通り、今回はキャラクターがある程度固まっているのでパターン辞書をほぼ人力で用意します。
 その上で、「〇〇は××だよね」という文章を生成するために連想辞書を用意します。連想辞書は概ね「名詞+形容詞(形容動詞)」の形式を取り、たとえば「ケーキ+甘い」「猫+可愛い」という具合です。これによって「ケーキは苦い」などの文章的には間違っていないような変な齟齬が発生するのを防ぎます。まあ、ケーキは苦いと教わったらケーキは苦いと覚えてしまうのであくまで原理的な話ですけど。あと素直に全部学習しちゃうと「ケーキ+甘いor苦い」みたいなデータができちゃって何やねんという感じになりかねない……。

 あとなんか卑猥な言葉は覚えさせたくないのでブラックリストてき辞書も作っておこうかなという気持ちです。

 最終的にはAPI化して、

{
    text:"おはよう",
    morph:"smile"
}

てきな形式でデータを返却、値に合わせてモーフ起動とかできねえかな〜〜〜〜〜〜〜〜と見積もってる状態です。UnityとSwiftはまだ触ってないですけどまあ原理的に大丈夫じゃろたぶん。

環境

  • Python
    • MeCab:形態素解析
    • CaboCha:係り受け解析

参考にしてたブログはjanomeで作ってたんですけど係り受け解析とか感情解析とかを考えたときにMeCab前提のライブラリが必要になったのでMeCabで書き直しています。
Python初心者に送る「人工知能の作り方」 – すなぶろ / github

読みは「めかぶ」と「かぼちゃ」でいいのかしらね?

続きを読む...

小さい会社では教えてもらえなかったりフォーマットがなかったりする議事録の書き方についておさらいしてみよう

議事録とは?

 読んで字のごとく、議事の録です。
【議事】会合して相談すること。また、その内容。

 中の人も今まで正社員で三社、契約社員とか派遣社員も含めると六社ほど所属していますが議事録の書き方について教わったことはありません。零細企業ばかり渡り歩いているせいか、フォーマットも見たことがありません。あるところにはあります。っていうか見たところでは前回の議事録のコピーから作成していたので秘伝のソースてきサムシングでしたけど。
 最近久し振りに議事録を書く機会があったので書きながら思い出したこととかをざっとまとめておきます。

必要なもの

  • 日時
  • 参加者一覧(敬称略である場合が多く、順番は偉い順だったり名前順だったり)(自分の名前は一番最後に)
  • 場所
  • 議題(複数ある場合は都度見出しとして書いてもよい)
  • 作成者名
  • 本文

フォーマット

 同業者(エンジニア)ならマークダウンが個人的におすすめです。Dropbox Paperはいいぞ。
 Microsoft Wordが入っているならそれもいいと思います。なにしろ会社員のパソコンには入っている可能性が高く、体裁も整えやすい。PDFにできると尚良しですが古い体質の会社だとAdobeが入ってなかったりして開けないとか文句が出たりするらしいです。しらないけど。
 実は注意しなきゃいけないのがテキストファイルで、文字化けとかするとちょっとうざい。

 Excelとかで作られたら私は卒倒します。

本文に書くもの

 ここは言ってしまえば環境によりけりなのですが、

  1. 責任の在り処を明確にするもの
  2. 決定事項とその決定者
  3. 決定が保留になったものがあればそれも書いておく

くらいが妥当な線でしょうか。
「何が決まったのか」「何がまだ決まっていないのか」「誰が決めたのか」「誰が決めるのか」あたりは大事です。

 発言がめちゃくちゃ迂遠というか無駄な言葉が多い人がいると手こずるところですが、議事録はできるだけ簡潔に書きます。比喩表現とか全カットでよろしい。議事録を書いていると上司の発言に対してよくもこんなぺらぺらの内容でそんな長ったらしく喋れるなとかいう客観的な評価を行うことができます。
 あと面倒なのが人の話にかぶせて喋り始める人……。まとめるときにだいぶ困ってしまう存在です。そういう人に限って発言に中身が無(ry

議事録はすばやく仕上げる

 大事です。具体的に、 会議の時間以上の時間を議事録に使ってはいけない
 二時間の会議なら会議が終わってから二時間以内の提出が望ましいです。
 まあこれ昔の上司の受け売りなんですけどね。そのために大事なのが「要点をまとめる」ことと「フォーマットを予め作成しておくこと」です。上司のクソ長い話を全部書き起こしてたら文字通り無駄に時間を食います。

アフィ

 見やすさ→読みやすさ→わかりやすさについて詳らかに解説してある良書です。まあ今いる会社はフォーマットガチガチ(※Excel方眼紙)なので読んでもヘイトが溜まっただけでしたが……。
 処理流暢性の話をするとですね、「見やすさ→読みやすさ→わかりやすさ」も真なんですけど同時に「見慣れたものが一番わかりやすい」も真なんですよね。だからExcel方眼紙が死なない。ばーかばーか。

続きを読む...

サイトを更新しました

タイトルの通り、サイトを更新しました。
以下変更点と忘備録です。

カテゴリアーカイブと個別記事にパンくずリストを追加

個別記事

<?php

  $categories = get_the_category();
  $categories = array_reverse($categories);
  $breadclumbs = array("<a href='".site_url()."'>架空会社ぱあぷう</a>");
  foreach($categories as $category){
    $breadclumbs[] = "<a href='".esc_url(get_category_link($category->cat_ID))."'>".$category->name."</a>";
  }
?>

    <?= implode(" > ", $breadclumbs) ?>

カテゴリアーカイブ

<?php
  $categories = get_the_category();
  $breadclumbs = array();
  foreach($categories as $category){
    if($category->cat_ID == $cat){
        $breadclumbs[] = "<a href='".esc_url(get_category_link($category->cat_ID))."'>".$category->name."</a>";
        while($category->category_parent){
            $category = get_category($category->category_parent);
            $breadclumbs[] = "<a href='".esc_url(get_category_link($category->cat_ID))."'>".$category->name."</a>";
        }
        break;
    }
  }
  $breadclumbs[] = "<a href='".site_url()."'>架空会社ぱあぷう</a>";
  $breadclumbs = array_reverse($breadclumbs);
?>

    <?= implode(" > ", $breadclumbs) ?>

子カテゴリ→親カテゴリはサクッと取れたんですけど親カテゴリ表示の際に自分のカテゴリを取得するのがちょっと大変でした。
get_the_category()って自分のカテゴリだけ取ってくれるわけじゃないのね……。

All in One SEO Pack導入

言わずと知れた有名プラグイン。便利すぎて毎回使いこなせないな〜と思いながら毎回入れます。

All in One SEO Pack

OGPを設定


twitterに共有すると画像が出るようになりました。
肝心の共有ボタンは無いのであとで追加します。

ファビコンを追加

弊社ロゴ(サイト左上)は本職のデザイナーさんに格安で作成していただいたもので、これを32×32サイズに切り抜いてファビコンとしました。

  1. 32×32サイズのキャンパスを作成する
  2. ロゴのデータファイル(.ai)からオブジェクトをコピー&ペーストして位置を合わせる
  3. 見切れてるパス削ったりする
  4. キャンパスと同サイズの長方形を新規作成、キャンパスに重ねる
  5. 長方形とロゴオブジェクトを選択、変形→パスファインダー→切り出し
  6. 完成!

ファビコン反映されてるのまだ見えてないけど。
以上、更新報告でした。SNSシェアボタン系は多分プラグイン入れるだけなので記事書かないです。

続きを読む...

9月2日の進捗

気合と根性と性癖の眼鏡作り込み。

レンズの厚みがあって鼻あてがないのはそもそもモデル本体に鼻梁の概念が無いためちょっと意味わかんなくなってしまったせいです。
あと昨日時点では眼鏡自体が垂れ目方向に歪んでいたためそれを修正。

デレステやってから見たら「なんか下膨れだな……」と思ったので顔全体もちょっとスリムに修正。唇ちょっとやりすぎかな……ちょうどいい具合が見つかりません。ポリゴンあんま増やしたくないしなあ。

せっかくぐりぐり回して観察できるクオリティの高いモデルがあるので超見てるんですけどハゲキャラはいないのでこめかみ周りの処理がわからない……。髪作ったら隠れるだろうか。むーん?
あの二次元キャラに特有の、こめかみの辺りで一旦くぼむアレの処理がわからないのですよね……単に彫りを深くしちゃうとかわいくないし。謎。

あと今日は欅坂のアプリやってました。ポチポチゲーかと思ったらパズルゲーという謎。ゲームとしてのクオリティは別に高くないけどボイスいっぱい聞けるからいっか〜という気がします。

続きを読む...

9月1日時点の進捗

目にハイライトが入りました。

口の中に見えているのは歯ではなく襟ぐりです。ここからどうやって口の中を造るのだろう……先に作っておくべきだったのでは……?

そしてメガネを追加&輪郭をちょっと修正。

凹レンズ表現やりたかったけどポリゴンがまた増えることになるので諦めました。横から見るとまっ平らです。

続きを読む...

VagrantでAngularサーバーを立てる

割と無駄にハマったのでセルフ墓標。

Angularのインストール
順序としてはNode.jsのインストールが先。

curl --silent --location https://rpm.nodesource.com/setup_8.x | bash -
yum install -y nodejs
npm install -g @angular/cli

AngularJSのインストールのためにはnpmのバージョンアップをしなきゃいけなくてそっちでも結構ハマったのだけどまあそれはいい。nって何さあのググラビリティ低いコマンド……

サーバー立ち上げ
インストールが終わったらアプリを作成、とりあえずサーバーを起動

ng new app
cd app
ng serve

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **

問題発生
よし、と思ってブラウザからhttp://192.168.33.10:4200にアクセスしてみたらConnection Refusedとか言われる。
まあわりといつものことだし〜と思ってSELinuxやっつけたりfirewalldやっつけたりしたんですがまだつながらない。うん?

・Vagrantのforwarded_portは設定した
・なんならprivate_networkも設定している
ip aしてみてもやっぱりIPは正しい
・Vagrant内部からcurlしてみたらつながった→つまり外部からのアクセスがだめっぽい
・SELinuxやっつけた
・firewalldやっつけた
→でもつながらない。

解決
あれー……他何かあるっけー……?と思いつつあれそれ試して数時間。ふとnetstat -anpしてみたら

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:4200 0.0.0.0:* LISTEN 4159/ng

あっこれかっってなってとりあえずローカルアドレスを変更するためにAngularServerの設定変更方法〜〜〜〜つってggってたんですけど(無駄な時間)、なんてことはない、 ng serve --host 0.0.0.0 で解決した。
進捗のない作業は本当に疲れる……。

続きを読む...

Laravel5.4でCookieを使うメモ

 仕事で作って案の定却下されたけど勿体無いので供養だオラァ!!

経緯

  • Remember meの機能をつけることになる
  • なるほどと思ってつける
  • テスト仕様書を見たら想定されている機能が違う(語の定義が食い違っていた)
  • 気合で組み込む
  • コードレビュー時点で怒られが発生、棄却

語の定義の話

上司「リメンバーミーって画面にIDとパスワード保存してくれるやつじゃろ」
私「リメンバーミーってログイン状態保持してくれるやつじゃろ」
→事故発生

ちなみにLaravelのリメンバーミーはログイン状態を保持してくれるやつ。だからこそ上司との意見の食い違いに気付けなかった。
説明するより組んで見せた方が早いと判断して取り敢えず組んだ(冒頭の「案の定」はそのため)
っていうか画面にIDとパスワード覚えてくれるやつはリメンバーミーっていうかブラウザの機能では?(素)

ソース

クッキー保存

ちなみに今回はログインも独自ソースなのでこれで済んでるけどAuthenticatesUsersはtraitなのでそのへんはなんとかしてください。まあコピペでなんとかなるじゃろたぶん。

# LoginController.php

public function login(Request $request)
{
    // 〜ログイン処理〜
    // 〜ログイン成功〜
    if($request->input('remember')){
        $name = cookie()->make("name", encrypt($request->input('name')), 129600);
        $password = cookie()->make("password", encrypt($request->input('password')), 129600);
        return redirect('/home')->withCookie($name)->withCookie($password);
    }
}

クッキー展開

#LoginController.php

public function showLoginForm(Request $request)
{
    $name=$password="";
    if(old('name')!=null){
        $name=old('name');
        $password=old('password');
    }else if($request->hasCookie('name')){
        $name=decrypt($request->cookie('name'));
        $password=decrypt($request->cookie('password'));
    }
    return view('auth.login',compact('name','password'));
}

 優先順位は前回入力値が上。しかしなんで画面にIDとパス残ってるのに入力を間違うなんてことがあろうか(反語)
 あとはこのnameとかpasswordをvalueに設定してやるだけ。エラーで戻ってきてもちゃんと復元してくれるよ!

続きを読む...

GoogleMapsJavascriptAPIのUIを改造する

プロマネさん! 死ですよ! 死!!(裏声)

デザイナーさんからすごくきれいなデザインモックが飛んできてお客様確認済みで内部設計とか実装は丸投げで取り敢えずマーカーのクラスタリングとラベル改造は必須だということになったらそれぞれいい感じのライブラリはあったんですけどそこの食い合わせがなかなかに悪かったりして血泡吹きながらあれそれしています。しかもデザインがXDだからHTMLから起こさなきゃいけないとかいうバックエンドエンジニア殺し。保守性は死んだ! もういない! 貴様の行為は炎上案件に油を注ぐだけだ!

Q.火を付けたのは誰ですか?
A.空気を圧縮すると温度が上がりますね。納期も圧縮すると温度が上がって燃えますね。それだけです。はいでは行ってみよう〜〜〜〜

ピンにマウスをかざした時にピンの画像を変えたい

はい。

var normalIcon = "hogehoge.png";
var hoverIcon  = "fugafuga.png";

google.maps.event.addListener(icon.marker, 'mouseover', (function(icon){
      return function(){
    this.setIcon({
      url: icon
    });
      };
    })(hoverIcon));

google.maps.event.addListener(icon.marker, 'mouseout', (function(icon){
      return function(){
    this.setIcon({
      url: icon
    });
      };
    })(normalIcon));

表示範囲で拡大縮小ボタンを付けたい

つまるところ「1km」ってボタンを押したらマップ中心から1kmの範囲が表示されてほしいと。
地球上の任意の座標を中心とした円を描き座標を取れと。

$(".zoom_m").on('click',function(){
  var distance = $(this).attr('data-distance');
  var center = gmap.getCenter();
  // 緯度一度110942.97m
  var lngRadix=110942.97;
  var lngDiff = distance / lngRadix;
  var west = center.lng() - lngDiff;
  var east = center.lng() + lngDiff;

  // var pi = 3.1415926535898;                      // 円周率近似値
  var r = 6378137;                            // 地球の半径(m)
  var c = Math.cos(center.lat() / 180 * Math.PI) * 2 * Math.PI * r;    // 任意の緯度の円周(km)
  var latRadix = c / 360;                          // 経度 1度あたりの km

  var latDiff = distance / latRadix;
  var north = center.lat() - latDiff;
  var south = center.lat() + latDiff;

  //北西端の座標を設定
  var sw = new google.maps.LatLng(south,west);
  //東南端の座標を設定
  var ne = new google.maps.LatLng(north,east);
   
  //範囲を設定
  bounds = new google.maps.LatLngBounds(sw, ne);

  gmap.fitBounds(bounds,5);
});

これもっとマシな実装は無かったんだろうか……。

マーカーのラベルをきれいにしたい

デザイナさんのデザインはとてもきれいなのでフォントも色も縁取りも指定されていますが素のGoogleMapにそんな機能はないのでLabeledMarkerを導入します。

LabeledMarker

参考はこちら : GoogleMapでカスタム可能なラベルを持つマーカー – Qiita
先人の叡智……うめ……うめ……

ソース編集

LabeledMarkerを追加してもまだまだ要件に足りないのでソースに手を突っ込みます。まあCSSクラス追加しただけなんですけどコピペだけで動くなんてそんなオイシイ話は無いわね。

 

LabeledMarker.prototype.createLabel = function (labelOptions) {
    var label = new google.maps.OverlayView();
    label.parent = this;
    label.bindTo('map', this);
    label.bindTo('position', this);
    label.onAdd = function () {
        var el = document.createElement('div');
        el.style.position = labelOptions.style.position || 'absolute';
     // el.innerHTML = labelOptions.innerHTML || '<div>' + labelOptions.text + '</div>';
        el.innerHTML = labelOptions.innerHTML || '<div class="label">' + labelOptions.text + '</div>';
        this.el = el;
        var panes = this.getPanes();
        panes.overlayLayer.appendChild(el);
    }
...

あとなんか使ってないソースは消した。
あとは.labelに対してデザイン作るだけなので余裕(余裕とは言ってない)。

近隣のマーカーをクラスタリングしたい

MarkerClusterer

MarkerClusterer自体は有名なので探せばいくらでも資料があるので大丈夫です。使い方もそんなに難しくない。
ただし今回の場合LabeledMarkerとの兼ね合いがあったので暫定的にこう書いた(取り敢えず動いた)

var markers = [];
// LabeledMarker
var icon = new LabeledMarker(gmap,{
  icon: {
    url: normalIcon
  },
  map: gmap,
  position: latLng
},{
  text: data.title,
  style:{

  }
});

// LabeledMarkerがインスタンス変数(って言うのはJavaScriptてきに正しいんだろうか?)として持っているmarkerだけを渡す
markers.push(icon.marker);

var markerCluster = new MarkerClusterer(gmap, markers,
    {
      styles:  [
      {
        textColor: 'white',
        url: '<?= get_stylesheet_directory_uri() ?>/images/piyopiyo.png',
        height: 30,
        width: 30
      }
      ],
      zoomOnClick:false
  });

クラスタリングされたマーカーのラベルを非表示にしたい

ただ今より毒ガス訓練を開始する!!(カッ)
現状だとクラスタリングされたマーカーのラベルが出っぱなしで、ごちゃごちゃして見苦しいのです。縮小に合わせてラベルを消し、拡大に合わせてラベルを表示します。
いろいろ調べた結果(二日くらいかかった)、MarkerClustererからラベルが見えていないという割と当たり前の事実に突き当り、ソースを修正。

markers.push(icon);

LabeledMarkerの中のmarkerだけをMarkerClustererに渡していたところをまるごと渡すように修正。
それからMarkerClustererも修正。具体的には、markerを参照しているところをmarker.markerに書き換え。ちなみに一発置換とかやっても動かないです。はははちょっとした地獄だった。道は前にしか開かれません。進みましょう。
google.maps.Markerの内容が見たいときはmarker.markerにアクセス、管理はmarkerごとっていう具合になると思います。変数名はちゃんと変えた方がいいと思いますけどいっぺんにやると何でバグってるかわからなくなって血を吹くので注意。

そしてこう

 

Cluster.prototype.addMarker = function(marker) {
  if (this.isMarkerAlreadyAdded(marker)) {
    return false;
  }

  if (!this.center_) {
    this.center_ = marker.marker.getPosition();
    this.calculateBounds_();
  } else {
    if (this.averageCenter_) {
      var l = this.markers_.length + 1;
      var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l;
      var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l;
      this.center_ = new google.maps.LatLng(lat, lng);
      this.calculateBounds_();
    }
  }

  marker.marker.isAdded = true;
  this.markers_.push(marker);

  var len = this.markers_.length;
  if (len < this.minClusterSize_ && marker.marker.getMap() != this.map_) {
    // Min cluster size not reached so show the marker.
    marker.marker.setMap(this.map_);
    // クラスタリングされていないピンのラベルを表示
    $(marker.label.el).children('.label').show();
  }

  if (len == this.minClusterSize_) {
    // Hide the markers that were showing.
    for (var i = 0; i < len; i++) {
      this.markers_[i].marker.setMap(null);
      // クラスタリングされたピンのラベルは非表示
      $(this.markers_[i].label.el).children('.label').hide();
    }
  }

  if (len >= this.minClusterSize_) {
    marker.marker.setMap(null);
    // クラスタリングされたピンのラベルは非表示
    $(marker.label.el).children('.label').hide();
  }

  this.updateIcon();
  return true;
};

同じ住所の複数テナントの処理

同じビルに複数のテナントが入っている場合がある。どれだけ拡大してもピンがクラスタリングされたままになって、いつまでも情報を取ることができないので困る。
参考はこちら : GoogleMapAPIでMarkerClustererを利用した時のマーカークリックについて – teratail
上記を参考にしながらクラスタの横にマーカーを追加してデータの取得はそっちで行うみたいな(伝われ)ことにしました。

google.maps.event.addListener(markerCluster, 'clusterclick', function(cluster) {
    gmap.panTo(new google.maps.LatLng({
    lat: cluster.center_.lat(),
    lng: cluster.center_.lng()
  }));
  var zoom=gmap.getZoom()
  if(zoom<18){
    // ズームレベル18までは普通にズームする
    // 一個ずつズームするのちょっとだるいのでいい方法があれば修正はしたい
    gmap.setZoom(++zoom);
  }else{
        var i=0;
        // 同じ住所でも微妙に座標が違うとかめんどくさかったので近所の住所は同じ座標で扱う
        var lat=cluster.markers_[0].marker.data.latitude;
        var lng=cluster.markers_[0].marker.data.longitude;
      cluster.markers_.forEach(function(e){
        var latLng = new google.maps.LatLng({
        lat: parseFloat(lat)-(0.1+i)/5000,
        lng: parseFloat(lng)+1/5000
      });
      i++;
    });
  }
});

MarkerClustererの自動ズームは切ってズームレベルをいちいち確認しながら挙動を分けました。
そもそも登録画面を住所だけにして座標は自動取得とかにしておけばいいのに座標まで手作業で設定するから微妙に数字がズレていたり緯度と経度がひっくり返っていたりして死です死。
座標変えて表示しちゃってるけどまあどうせ詳細見る場所はあるのでだいじょうぶだいじょうぶ。
これは常々思っていることなんですけど営業とプログラマーの間にシステムエンジニアが生まれたようにデザイナーとプログラマーの間にも何かのクッションが必要だ。そうじゃなければ下流のプログラマがしぬ

続きを読む...