クソ雑魚エンジニアのメモ帳

学んだことを書くところ

metabase3分クッキング

こんばんは。この季節サンダル通勤で足が冷えてくるmorifujiです

今回はmetabaseを使う機会があったのでサクッと構築したlogを置いておきます。

  • クライアントはBIツールが欲しいらしい
  • tableauは高い&運用費も高い(m4.2xlargeぐらい)
  • もっとサクッとかつ簡単なBIツールないんかなー

ということでmetabaseの登場です。

  • slack連携
  • メール連携

を使えばテーブルAのデータがある基準に達したときにslackやメールで通知ができるみたいです、すごいですね

環境

スクリプト

# 以下、ec2インスタンス内。

mkdir metabase
cd metabase
wget http://downloads.metabase.com/v0.30.4/metabase.jar

# javaバージョン確認
java -version

# 1.8>versionだったら
sudo yum update -y
sudo yum install -y java-1.8.0-openjdk.x86_64
# versionを1.8以上に切り替える
sudo alternatives --config java

# 確認
java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)


# `java -jar metabase.jar`でエラーが出たので修正
sudo echo "10.0.0.219 ip-10-0-0-219" | sudo tee -a /etc/hosts


# 実行
java -jar metabase.jar


# localhost:3000/setupにてGUIなセットアップができる!!

課題

  • metabaseのデータがインスタンスの停止などで消える可能性があるので何かしらでバックアップが必要
    • 手段1. ebsでマウントしておく
    • 手段2. metabaseのデータ保存先がデフォルトh2なので、rdsにする
    • 余裕あればまた別記事で書きます。 軽かったのでこの記事に描きました

metabaseのデータバックアップ

データをrdsに保存したいなら、以下の環境変数を設定するとdbに繋げてくれます。以下はmysqlの例です

export MB_DB_TYPE=mysql
export MB_DB_DBNAME=metabase
export MB_DB_PORT=3306
export MB_DB_USER=xxxxxxxxxxxx
export MB_DB_PASS=xxxxxxxxxxxx
export MB_DB_HOST=xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com

# この後にjavaコマンド

バックグラウンド化

java -jar metabase.jarしてもフォアグラウンドなのでssh切ったら止まります。(当たり前)

なのでバックグラウンドで起動できるようにしましょう

nohup java -jar metabase.jar > out.log &

この例だとout.logにログが測れますね

参考

https://www.yoheim.net/blog.php?q=20180101

jooby(kotlin)の環境構築,swagger,docker化まで

こんばんは。フォートナイトやってたら台風の爆音がうるさくて敵の足音が聞こえず殺されたmorifujiです

2018/09/19 追記

gitlabに使ったコードを載せました。開発環境もdockerで構築できるようにしたので割と便利です。

https://gitlab.com/morifuji/jooby-web

概要

Kotlinを触りたいなあと思っていたのでその練習がてらにjoobyなるものを触りました。

  • IntelliJでの環境構築
  • RESTfulAPI実装/Swgger出力
  • DBつなぎ込み(MySQL)
  • ビルド
  • docker化

までやったので知見を共有します。ちなみに僕はGradleとjavaSIer時代3ヶ月ほどだけやっていましたがほぼ忘れている状態です

jooby

公式サイト:https://jooby.org/

IntelliJでの環境構築

こちらを参考にしました。あっという間です。https://github.com/jooby-project/kotlin-gradle-starter

git clone https://github.com/jooby-project/kotlin-gradle-starter.git
cd kotlin-gradle-starter
./gradlew joobyRun

IntelliJでrunする場合は以下の手順です、

  1. IntelliJでプロジェクト開いて、右上のGradleを選択
  2. jooby>joobyRunを起動
...
[2018-09-04 23:15:13,805]-[Hotswap] INFO  starter.kotlin.App - [dev@netty]: Server started in 3873ms

  GET  /                        [*/*]     [*/*]    (/anonymous)

listening on:
  http://localhost:8080/

まだbuild.gradleにもなにも触っていない状態ですが、http://localhost:8080/を叩くとHelloWorldされているのが確認できます

ホットリロード

実はこのjoobyRunコマンドは裏でホットリロードも動いています。対象ファイルは.classと.confと.propertiesです。静的ファイルはさすがにリロードしてくれないみたいですね

こんな感じでbuild.gradleでカスタマイズも可能みたいです

joobyRun {
  mainClassName = 'com.mycompany.App'
  compiler = 'on'
  includes = ['**/*.class', '**/*.conf', '**/*.properties']
  excludes = []
  logLevel = 'info'
  srcExtensions = [".java", ".kt", ".conf", ".properties"]
}

LL系と比べると流石に遅いですが、自動でやってくれること自体に感動しますね、

https://jooby.org/doc/devtools/#gradle-hot-reload

RESTfulAPI実装

jacksonというライブラリが公式で紹介されてます。

https://jooby.org/doc/jackson/

build.gradleにライブラリを追加しましょう

dependencies {
    compile "org.jooby:jooby-lang-kotlin"
    compile "org.jooby:jooby-netty"
    compile "io.netty:netty-transport-native-epoll:${dependencyManagement.importedProperties['netty.version']}:${osdetector.classifier.contains('linux') ? 'linux-x86_64' : ''}"
    compile "io.netty:netty-tcnative-boringssl-static:${dependencyManagement.importedProperties['boringssl.version']}:${osdetector.classifier}"
    compile "org.jooby:jooby-jackson:$joobyVersion"

    testCompile "org.jetbrains.spek:spek-api:$spekVersion"
    testRuntime "org.jetbrains.spek:spek-junit-platform-engine:$spekVersion"
    testCompile "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
    testCompile "org.amshove.kluent:kluent:1.35"
    testCompile "io.rest-assured:rest-assured:3.1.0"
}

さらに、メインクラス(App.kt)を修正しましょう

data class People(val name: String, var age: Int)

/**
 * Gradle Kotlin stater project.
 */
class App : Kooby({
+    use(Jackson())

    get {
        val name = param("name").value("Kotlin")
        "Hello $name!"
    }

+    get("/array") { req ->
+        val arr = listOf(1,2,3,4,5)
+        arr
+    }
+
+    get("/map") { req ->
+        val map = mapOf("hoge" to "fuga", "りんご" to "ごりら")
+        map
+    }
+
+    get("/data_class") {req->
+        val people = People("金田哲夫", 19)
+        people
+    }

この時、IntelliJ上でパッケージの読み込みがうまくいかず、警告が表示されるはずです。後半にハマりポイントとして解消方法を書いたので参考にして下さい

この修正でホットリロードが動いた後にブラウザで叩くとわかりますがresponseのContent-Typeがjsonになっておりなおかつ、

/listのresponseはarrayのjsonとして表示され、/mapのresponseはobjectとして表示され、/data_classのresponseはobjectとして表示されます。

Swaggerを生成

今度はこのRESTfulAPIのAPI仕様書を自動生成しましょう。

swaggerとramlの自動生成が紹介されています。

https://jooby.org/doc/apitool/#API-tool

build.gradleを修正▼

dependencies {
    compile "org.jooby:jooby-lang-kotlin"
    compile "org.jooby:jooby-netty"
    compile "io.netty:netty-transport-native-epoll:${dependencyManagement.importedProperties['netty.version']}:${osdetector.classifier.contains('linux') ? 'linux-x86_64' : ''}"
    compile "io.netty:netty-tcnative-boringssl-static:${dependencyManagement.importedProperties['boringssl.version']}:${osdetector.classifier}"
    compile "org.jooby:jooby-jackson:$joobyVersion"
+    compile "org.jooby:jooby-apitool:$joobyVersion"

    testCompile "org.jetbrains.spek:spek-api:$spekVersion"
    testRuntime "org.jetbrains.spek:spek-junit-platform-engine:$spekVersion"
    testCompile "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
    testCompile "org.amshove.kluent:kluent:1.35"
    testCompile "io.rest-assured:rest-assured:3.1.0"
}

App.ktを修正▼

/**
 * Gradle Kotlin stater project.
 */
class App : Kooby({
    use(Jackson())
+    use(ApiTool().swagger().raml())

    get {
        val name = param("name").value("Kotlin")
        "Hello $name!"
    }
...

これでホットリロードを回した後、複数のエンドポイントが自動で追加されています。

  GET  /swagger/swagger.json    [*/*]     [*/*]    (/anonymous)
  GET  /swagger/swagger.yml     [*/*]     [*/*]    (/anonymous)
  GET  /swagger/static/**       [*/*]     [*/*]    (/anonymous)
  GET  /swagger/static/**       [*/*]     [*/*]    (/anonymous)
  GET  /swagger                 [*/*]     [*/*]    (/anonymous)
  GET  /raml/api.raml           [*/*]     [*/*]    (/anonymous)
  GET  /raml/static/**          [*/*]     [*/*]    (/anonymous)
  GET  /raml     

/swaggerを叩くとこんな感じで自動生成されてます、あとは煮るやり焼くなりできますね

スクリーンショット 2018-09-04 23.51.20.png

MySQLつなぎ込み

MySQLのつなぎ込みがしたかったのでやりました、SQLは書きたくないので、Hibernateを採用しました

https://jooby.org/doc/hbm/

conf/application.confを開き、追記。

# mysql
# add or override properties
# See https://github.com/typesafehub/config/blob/master/HOCON.md for more details

+ # mysql
+ db {
+   url: "jdbc:mysql://localhost:3111/test",
+   user: "root",
+   password: "password"
+ }

dependenciesに追加。僕はmysqlですが、ドライバを変えればポスグレでもなんでもいけると思います

dependencies {
    compile "org.jooby:jooby-lang-kotlin"
    compile "org.jooby:jooby-netty"
    compile "io.netty:netty-transport-native-epoll:${dependencyManagement.importedProperties['netty.version']}:${osdetector.classifier.contains('linux') ? 'linux-x86_64' : ''}"
    compile "io.netty:netty-tcnative-boringssl-static:${dependencyManagement.importedProperties['boringssl.version']}:${osdetector.classifier}"
    compile "org.jooby:jooby-jackson:$joobyVersion"
    compile "org.jooby:jooby-apitool:$joobyVersion"
+    compile "org.jooby:jooby-jdbc:$joobyVersion"
+    compile "org.jooby:jooby-hbm:$joobyVersion"
+    compile "mysql:mysql-connector-java:5.1.47"

    testCompile "org.jetbrains.spek:spek-api:$spekVersion"
    testRuntime "org.jetbrains.spek:spek-junit-platform-engine:$spekVersion"
    testCompile "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
    testCompile "org.amshove.kluent:kluent:1.35"
    testCompile "io.rest-assured:rest-assured:3.1.0"
}

まずは、entity作成(kotlin-gradle-starter/src/main/kotlin/starter/kotlin/entity/Contact.kt)

package starter.kotlin.entity

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id


@Entity(name = "contacts")
class Contact {

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private var id: Int? = null

    private val name: String? = null

    var notes: String? = null

    protected var website: String? = null

    private var starred: Int = 0

    private var password: String? = null


    fun setPassword(rawPassword: String) {
        this.password = rawPassword
    }

    fun review(isUp: Boolean) {
        if (isUp) {
            this.starred++
        } else {
            this.starred--
        }
    }
}

controllerもとりあえずメインクラス(App.kt)作成。emというのが、EntityManagerで、処理のファサードになっている

/**
 * Gradle Kotlin stater project.
 */
class App : Kooby({
    use(Jackson())
    use(ApiTool().swagger().raml())
+   use(Jdbc())
+   use(Hbm().classes(Contact::class.java))

+   get("/api/contact/") { req ->
+        require(UnitOfWork::class.java).apply { em ->
+            // 新規作成
+            val c = Contact()
+            // publicなのでOK
+            c.notes = "メモだよ!!!そのままinsert!!"
+            // privateなので
+            c.review(true)
+            c.review(true)
+            // privateなので
+            c.setPassword("ほげほげ")
+
+            // 登録
+            em.save(c)
+
+            // さらに編集
+            c.review(false)
+            c.notes = "修正済み(´・ω・`)"
+            // さらに保存
+            em.save(c)
+
+            // 一覧取得
+            em.createQuery("from contacts").resultList
+        }
+    }

    get {
        val name = param("name").value("Kotlin")
        "Hello $name!"
    }
...

DBに繋がる状態でjoobyRunしてください。起動時にmysqlへの疎通確認が走ります。と同時にcontactsテーブルが自動で生成されています!!コンパイラ型言語っぽいですよね〜

この状態で叩くと、こんな感じのresponseになると思います。(5回叩きました)

スクリーンショット 2018-09-05 0.22.17.png

各entityのプロパティがnotesしかないのは、Contactクラスのpublicなプロパティだからです。

テーブルを自動作成したり、entityクラスのアクセス修飾子によってresponse変えたりするところを見ると、php等のORMよりもさらにDBとアプリケーションが密結合になっている感じがします。

ビルド

jarファイルを出力して、jar単体で動くかテストします。

Gradle(画面右上) > build > buildからビルド

成功したら、build/libsにjarファイルが出力されてます

~/jooby
❯ java -jar ./kotlin-gradle-starter/build/libs/kotlin-gradle-starter-1.0.jar
./kotlin-gradle-starter/build/libs/kotlin-gradle-starter-1.0.jarにメイン・マニフェスト属性がありません

manifestが入っていないらしいので、build.gradleを修正。

...

jar {
    manifest {
        attributes(
                'Main-Class': "starter.kotlin.AppKt"
        )
    }
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}

特にfrom { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }この部分を忘れないよう注意してください

再ビルドしてもういちどデプロイ

❯ java -jar ./kotlin-gradle-starter/build/libs/kotlin-gradle-starter-1.0.jar
[2018-09-04 22:19:30,882]-[main] INFO  com.zaxxer.hikari.HikariDataSource -
...
  GET  /swagger/swagger.json    [*/*]     [*/*]    (/anonymous)
  GET  /swagger/swagger.yml     [*/*]     [*/*]    (/anonymous)
  GET  /swagger/static/**       [*/*]     [*/*]    (/anonymous)
  GET  /swagger/static/**       [*/*]     [*/*]    (/anonymous)
  GET  /swagger                 [*/*]     [*/*]    (/anonymous)
  GET  /raml/api.raml           [*/*]     [*/*]    (/anonymous)
  GET  /raml/static/**          [*/*]     [*/*]    (/anonymous)
  GET  /raml                    [*/*]     [*/*]    (/anonymous)
  GET  /                        [*/*]     [*/*]    (/anonymous)
  GET  /array                   [*/*]     [*/*]    (/anonymous)
  GET  /map                     [*/*]     [*/*]    (/anonymous)
  GET  /data_class/:name        [*/*]     [*/*]    (/anonymous)

listening on:
  http://localhost:8080/

キタ━━━━━━━━m9( ゚∀゚)━━━━━━━━!!

joobyの特徴の一つに、サーブレットの概念がなく、jarファイルにサーバーも含まれているため、簡単にデプロイできると書かれています。サーバーはjettty/nettyほか多数から選択できるみたいです。 こういう丸ごと入ったjarファイルをfatJarって呼ぶらしいですね。fatって悪いイメージだけどいいのか笑

https://jooby.org/doc/deployment/#deployment-intro

docker化

これが一番しんどかった、、

公式には「こんなかから適当に選んでやってみー多分できるやろ(ハナホジ」みたいな感じでgradleのプラグイン検索ページのリンクが貼っていました、どうしたらええんや、、

https://jooby.org/doc/deployment/#docker

とりあえずgradle+dockerでメジャーそうな com.palantir.docker-runを使うことにしました

これを使用できるようbuild.gradle修正

buildscript {
...
    repositories {
        mavenLocal()
        jcenter()
        mavenCentral()
+        maven {
+            url "https://plugins.gradle.org/m2/"
+        }
    }
    dependencies {
        classpath "com.google.gradle:osdetector-gradle-plugin:1.4.0"
        classpath "io.spring.gradle:dependency-management-plugin:1.0.4.RELEASE"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
        classpath "org.jooby:jooby-gradle-plugin:$joobyVersion"
        classpath "org.junit.platform:junit-platform-gradle-plugin:$junitPlatformVersion"
+        classpath "gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.13.0"
    }
}

...

apply plugin: "jooby"
apply plugin: "org.junit.platform.gradle.plugin"
+apply plugin: 'com.palantir.docker'

...

さらにこちらも参考にして、dockerプラグインの設定を記載。

docker {
    name "${project.group}/morimorikochan"  // 任意の名前で
    files "{フルパス}/kotlin-gradle-starter/build/libs"
    buildArgs(['JAR_FILE': 'kotlin-gradle-starter-1.0.jar'])
}

また、docker内からアクセスできるDBをconf/application.confに記載した上で以下のコマンドを叩きましょう。(docker内からDBにアクセスできないとエラーで落ちるので)

./gradlew docker

途中でエラーになるかたは後半にハマりポイントに解消方法を書いてるので参考にしてください、ぼくはこれで1時間とかしました

うまくいくと、プラグインnameプロパティで指定したイメージ名でdockerのイメージが作成されています。

~/jooby/kotlin-gradle-starter master* 8s
❯ docker images
REPOSITORY                                                             TAG                 IMAGE ID            CREATED             SIZE
starter.kotlin/morimorikochan                                          latest              688cabd7a3dc        8 minutes ago       1.01GB
<none>                                                                 <none>              eee987ddfeb1        3 hours ago         1.01GB
<none>                                                                 <none>              cc016de61c54        3 hours ago         1.01GB
...

あとはこれをrunさせれば

docker run -it --rm starter.kotlin/morimorikochan

キタ━━━━━━━━m9( ゚∀゚)━━━━━━━━!!

ハマったこと

IntelliJ上でbuild.gradleに追加した新しいパッケージで警告が出る

スクリーンショット 2018-09-01 18.38.25.png

この原因は、IntelliJ上でパッケージが認識されていないのが原因みたいです、

File > Invalidate Caches/Restartでも治りませんでしたが、Gradle(画面右上) > build setup > wrapperを実行すると読み込まれました。もっと簡単な方法がありそう :thinking:

dockerが実行できない(Cannot run program "docker": error=2, No such file or directory)

./gradlew dockerをしても、途中でCannot run program "docker": error=2, No such file or directoryとなるときがあります。

その時はターミナル上で

./gradlew --stop

をしましょう。gradleのデーモンが停止します。その上でもう一度./gradlew dockerをするとビルドできるはずです

https://github.com/Transmode/gradle-docker/issues/80#issuecomment-348476060

所管

  • Kotlin書き方が面白い
    • クセがあるので慣れるまで時間かかりそう
  • joobyは思ったより今風な感じがした
  • プラグインとして機能が提供されているので、カスタマイズが容易にできそう
  • xmlで設定しなくていいことに感動した
  • コードとか設定周りがわりとDRY
  • アノテーションも最小限でコードを追えばすぐわかるフレームワークだと思った
  • nettyの起動早すぎ
    • docker-composeでmysqlと連携させたら、joobyの疎通確認早すぎてmysqlが起動中でjoobyが死ぬ
  • 最近のFWなのでドキュメントが貧弱かと思ったけどそんなこともなかった。モジュールを使えば大体のユースケースを満たせそう!!!
  • 実務で使ってみたい!!!
    • 誰かjoinさせてください :pray:
  • Hibernateもちょっとクセが強そう
    • その分細かいとこまでさわれそう

yii2のCRUDGenerator(scaffold)を改良した件

こんばんは。運動しなさすぎてフィットネスジムに入会しようか悩んでいるmorifujiです

概要

構築手順

めんどくさかったのでローカルでやっちゃいました :sweat_smile:

前提環境

  • Mac HighSierra
  • php 7.1
  • mysql5.7
composer create-project --prefer-dist yiisoft/yii2-app-basic basic
cd basic
php yii serve

終了。はや。

localhost:8080/でアクセスできる

スクリーンショット 2018-08-30 23.59.48.png

解説

こんな感じ1

こんな感じ2

basic/                  アプリケーションのベース・パス
    composer.json       Composer によって使用される。パッケージ情報を記述
    config/             アプリケーションその他の構成情報を格納
        console.php     コンソール・アプリケーションの構成情報
        web.php         ウェブ・アプリケーションの構成情報
    commands/           コンソール・コマンドのクラスを格納
    controllers/        コントローラのクラスを格納
    models/             モデルのクラスを格納
    runtime/            実行時に Yii によって生成されるファイル (ログやキャッシュなど) を格納
    vendor/             インストールされた Composer パッケージ (Yii フレームワークそのものを含む) を格納
    views/              ビュー・ファイルを格納
    web/                アプリケーションのウェブ・ルート。ウェブ・アクセス可能なファイルを格納
        assets/         Yii によって発行されるアセット・ファイル (javascript と CSS) を格納
        index.php       アプリケーションのエントリ・スクリプト (ブートストラップ・スクリプト)
    yii                 Yii コンソール・コマンド実行スクリプト

こんな感じ3

DB接続

config/db.phpに以下の通り設定。

dsnってポートはport=3111;こう書くんですね

return [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;port=3111;dbname=hogehoge',
    'username' => 'root',
    'password' => 'password',
    'charset' => 'utf8'
];

出典:https://www.yiiframework.com/doc/guide/2.0/ja/start-workflow

gii

yii2のscaffoldツールはgiiと呼ばれているらしい

https://www.yiiframework.com/doc/guide/2.0/ja/start-gii

実はすでにインストールされていて、/index.php?r=giiを叩くとメニュー出てきた、思ったより多機能 :astonished:

スクリーンショット 2018-08-31 0.01.05.png

基本的にはこの2つで事足りる - Model Generator - CRUD Generator

Model Generator

DBに定義しているテーブルからモデルクラスを自動出力してくれる

この2つだけ記載すればあとはよしなにやってくれる。

  • Table Name
  • Model Class Name

TableNameに入力すると予測変換が出てきて少し感動。続けてキャメルケースでModelClassNameを書く。

最後にPreviewを押す。するとどんなファイルが出力されるか表示される。Generateで出力する

スクリーンショット 2018-08-31 0.07.01.png

CRUD Generator

ModelGeneraterによって出力されたModelを元に自動でCRUDのページ・ルーティング、果てはフォームも自動生成する

この3つを設定

  • ModelClass
  • SearchClass
  • ControllerClass

ModelClass

すでにあげたディレクトリ構成に従うとapp\models\{キャメルケース}となる

SearchClass

ドキュメントでは、すでにあげたディレクトリ構成に従い、suffixをSearchとしてapp\models\{キャメルケース}Searchとしている

ControllerClass

すでにあげたディレクトリ構成に従うとapp\controllers\{キャメルケース}Controllerとなる

スクリーンショット 2018-08-31 0.13.23.png

同じくPreviewを押してGenerate

スクリーンショット 2018-08-31 0.13.31.png

できた!

実際の画面は?

/index.php?r={モデル名}で表示されます

検索画面

スクリーンショット 2018-08-31 0.15.10.png

ご丁寧に、画面に表示できないプロパティはコメントアウトしてくれている。こういうところ嬉しいですよね :cry:

    <?php $form = ActiveForm::begin([
        'action' => ['index'],
        'method' => 'get',
    ]); ?>

    <?= $form->field($model, 'id') ?>

    <?= $form->field($model, 'email') ?>

    <?= $form->field($model, 'password') ?>

    <?= $form->field($model, 'nearest_station_id') ?>

    <?= $form->field($model, 'frequently_drinking_station_id') ?>

    <?php // echo $form->field($model, 'sex') ?>

    <?php // echo $form->field($model, 'payment_customer_token') ?>

    <?php // echo $form->field($model, 'payment_subscription_token') ?>

    <?php // echo $form->field($model, 'date_of_birth') ?>

    <?php // echo $form->field($model, 'name') ?>

    <?php // echo $form->field($model, 'role') ?>

    <?php // echo $form->field($model, 'franchise_id') ?>

    <?php // echo $form->field($model, 'corporation_id') ?>

    <?php // echo $form->field($model, 'encrypted_id') ?>

    <?php // echo $form->field($model, 'created_at') ?>

    <?php // echo $form->field($model, 'updated_at') ?>

    <?php // echo $form->field($model, 'deleted_at') ?>

    <div class="form-group">

詳細画面

So simple

スクリーンショット 2018-08-31 0.19.58.png

登録画面・更新画面

スクリーンショット 2018-08-31 0.23.30.png スクリーンショット 2018-08-31 0.23.40.png

特徴としてはこんな感じ。

  • パスワードがシークレットな表示 (画像でいうPassword)
  • enumで定義したカラムはドロップダウン表示 (画像でいうRole)
  • date型・time型は文字列として表示 (画像でいうDate Of Birth)

本題

date型・time型は文字列として表示ここがおしい。ここもきちんとdatepicker/timepicker出してくれたらわりと活躍するかなあと思ってましたが残念すぎる、。。。

ということで、この部分だけサクッとカスタマイズしましょう

テンプレートファイル編集

https://www.yiiframework.com/extension/yiisoft/yii2-gii/doc/guide/2.1/en/topics-creating-your-own-templates

これにしたがって、giiのscaffold出力ロジックをいじいじした。

結果、ここでいじいじしてるみたい。


  /**
     * Generates code for active field
     * @param string $attribute
     * @return string
     */
    public function generateActiveField($attribute)
    {
        $tableSchema = $this->getTableSchema();
        if ($tableSchema === false || !isset($tableSchema->columns[$attribute])) {
            if (preg_match('/^(password|pass|passwd|passcode)$/i', $attribute)) {
                return "\$form->field(\$model, '$attribute')->passwordInput()";
            }

            return "\$form->field(\$model, '$attribute')";
        }
        $column = $tableSchema->columns[$attribute];
        if ($column->phpType === 'boolean') {
            return "\$form->field(\$model, '$attribute')->checkbox()";
        }

        if ($column->type === 'text') {
            return "\$form->field(\$model, '$attribute')->textarea(['rows' => 6])";
        }

        if (preg_match('/^(password|pass|passwd|passcode)$/i', $column->name)) {
            $input = 'passwordInput';
        } else {
            $input = 'textInput';
        }

        if (is_array($column->enumValues) && count($column->enumValues) > 0) {
            $dropDownOptions = [];
            foreach ($column->enumValues as $enumValue) {
                $dropDownOptions[$enumValue] = Inflector::humanize($enumValue);
            }
            return "\$form->field(\$model, '$attribute')->dropDownList("
                . preg_replace("/\n\s*/", ' ', VarDumper::export($dropDownOptions)) . ", ['prompt' => ''])";
        }

        if ($column->phpType !== 'string' || $column->size === null) {
            return "\$form->field(\$model, '$attribute')->$input()";
        }

        return "\$form->field(\$model, '$attribute')->$input(['maxlength' => true])";
    }

なるほど。カラム名がpasswordとかpasswdとかならシークレットな表示にするのか笑

            return "\$form->field(\$model, '$attribute')->$input()";

日付型・時間型はここに引っかかっているらしい

となるとformを出力する必要があるので、Extensionから検索。だれか作ってるでしょ

datetimepickerプラグイン

あった。総ダウンロード数19000ぐらい、普通の感覚では少し不安かもしれないけど、他のextensionは一桁台ダウンロード数だったのでこのextensionの有用さがうかがえる

https://www.yiiframework.com/extension/zhuravljov/yii2-datetime-widgets

composerに追加

composer require zhuravljov/yii2-datetime-widgets

先ほどのGenerator.phpを編集

        if ($column->type === 'date') {
            return "\$form->field(\$model, '$attribute')->widget(\zhuravljov\yii\widgets\DatePicker::class, [
    'clientOptions' => [
        'format' => 'yyyy-mm-dd',
        'language' => 'ja',
        'autoclose' => true,
        'todayHighlight' => true,
    ],
    'clientEvents' => [],
])";
        }


        if ($column->type === 'time') {
            return "\$form->field(\$model, '$attribute')->widget(\zhuravljov\yii\widgets\DateTimePicker::class, [
    'clientOptions' => [
        'format' => 'hh:ii:00',
        'language' => 'ja',
        'autoclose' => true
    ]
])";
        }

        if (in_array($column->type, ["timestamp", "datetime"])) {
            return "\$form->field(\$model, '$attribute')->widget(\zhuravljov\yii\widgets\DateTimePicker::class, [
    'clientOptions' => [
        'format' => 'yyyy-mm-dd hh:ii:00',
        'language' => 'ja',
        'autoclose' => true
    ]
])";
        }

結果

もう一度CRUDGeneratorを使う

スクリーンショット 2018-08-31 0.38.00.png

このとき、overrideにチェックを入れてGenerate

そして登録画面・編集画面へ http://localhost:8080/index.php?r=users%2Fcreate

aaaaaa.gif

キタ━━━━━━━━m9( ゚∀゚)━━━━━━━━!!

所感

  • 普段使っていないfwは面白い
  • しかもscaffold使うとガシガシできて気持ちいい。
  • コミュニティは活発なようだがextensionの荒廃している感が否めない
  • 3コマンドで環境構築できるとか強すぎひん?
  • 地味にプロファイリングツールもついてたww

スクリーンショット 2018-08-31 0.50.16.png

RaspberryPiにAlexaをインストールしてAmazonMusicを流そうとした件

こんばんは。ボルダリングで腕がパンパンmorifujiです。

今日は久しぶりにRaspoberryPiを押入れから引っ張り出して、Alexaさんを憑依させてAmazonMusicを流させようとしたので知見を共有します。なんか憑依っていうと◯川隆法みたいですね

目次

  1. amazonで開発者アカウント作成・アプリケーション登録
  2. スピーカー・マイク準備
  3. バイス登録
  4. 初期設定

前提となる環境

  • RaspberryPiが起動している
  • RaspberryPiに接続できるスピーカーを持っている
  • RaspberryPiに接続できるマイクを持っている

ちなみにぼくはこういう環境です。

1. amazonで開発者アカウント作成・アプリケーション登録

まずこちらから。https://developer.amazon.com/ でアカウントを作成してください。

今回僕は日本のamazonのアカウントでAmazonPrimeに入会しているので、そのアカウントのemail/passで登録しました。

:warning: amazon.comamazon.co.jp複数アカウントを持つ人は、どのアカウントを使用するか注意してください。まじでハマります。

https://dev.classmethod.jp/voice-assistant/solution-of-a-problem-amazon-com-account-conflict/

まず、トップ右上のDeveloper Consoleに飛びます スクリーンショット 2018-07-08 20.42.31.png

次に、AlexaVoiceServiceに飛びます

スクリーンショット 2018-07-08 20.43.03.png

製品を作成するに飛びます、

項目 入力内容
製品名 任意の製品名です、後から参照しないのでなんでもok。ぼくはraspberry_alexaにしました
製品ID 任意の製品IDです。後から参照するのでメモを忘れず
製品はアプリやデバイスを使用しますか ? 必ずアプリケーションを選択。はまりますので :warning:
商品カテゴリ なんでもok,ぼくはワイヤレススピーカを選びました
製品概要 参照しないのでなんでもok。ぼくはalexa on raspberrypiって書きました
エンドユーザーは、商品とどのようにやり取りするのでしょうか ? ハンズフリーとファーフィールドを選択
画像 しなくてもよい
この製品を商品として配信する予定ですか ? プライベート用なので no
これは子供向け商品、それ以外は13歳以下の子供向けですか ? よくわからんのでnoを選択

これで次へを選択

セキュリティプロファイルを選択する

初めての人はセキュリティプロファイルを作成してから、それを商品に登録します。特に難しいことないです

プラットフォーム情報

プラットフォームを作成する必要があります。他のデバイスやプラットフォームを選択し Downloadボタンからバックアップを取っておきましょう。

スクリーンショット 2018-07-08 20.54.49.png

:thinking:ちなみにこの一般IDってgenerate idって意味なのだろうか :thinking:

スクリーンショット 2018-07-08 20.55.10.png

そのファイルには以下の様なフォーマットでデータが入っています、必ず保管しましょう

{
 "deviceInfo": {
  "clientId": "amzn1.application-oa2-client.XXXXXXXXXXXXX",
  "productId": "raspberry_alexa"
 }
}

規約にチェックをつけて進むと完了するはずです。

2. スピーカー・マイク準備

RaspberryPiのssh環境を用意します。

まずはスピーカー。つなぐ前にスピーカーの状況を確認

# スピーカーデバイス一覧表示
aplay -L

スピーカーをつなぎもう一度実行すると項目が増えているはずです。

そしたら出力確認です

# スピーカーテスト
speaker-test -t sine -f 1000

音が聞こえなかったら徐々に音量あげましょう。

amixer sset PCM 100% 

次にマイクです。同じ様にデバイスとして認識されてるかチェックします

# マイクデバイス一覧
arecord -l 

んで。テスト。今回は、マイクで入力された音声がスピーカーから出力されるか確認します。

arecord -f S16_LE -r 44100 -D hw:1 | aplay

おうむ返しみたいな感じになりましたか?okなら次に行きましょう!

3. デバイス登録

いよいよラズパイにAlexaをインストールしていきます。

今回使うライブラリは こちらのalexa/avs-device-sdkです

ライブラリ選定にはこちらの記事が非常にわかりやすく書かれていました。ありがとうございます ☺️

https://qiita.com/Dimeiza/items/182c4847d7c1ead7df54

# ディレクトリ作成
mkdir ~/raspberry_alexa
cd ~/raspberry_alexa

# 環境構築スクリプトインストール
sudo wget https://raw.githubusercontent.com/alexa/avs-device-sdk/master/tools/Install/setup.sh && sudo wget https://raw.githubusercontent.com/alexa/avs-device-sdk/master/tools/Install/config.txt && sudo wget https://raw.githubusercontent.com/alexa/avs-device-sdk/master/tools/Install/pi.sh

ここで、config.txtというファイルを開くとこんな感じ

#NOTE: The Device Serial Number can be any unique number
DEVICE_SERIAL_NUMBER="alexa_raspberry"
CLIENT_ID=""
PRODUCT_ID=""

先ほど、1でダウンロードしたファイルに書いてあるclient_idproduct_idをここに入力します、。DEVICE_SERIAL_NUMBERは適当でok

{
 "deviceInfo": {
  "clientId": "amzn1.application-oa2-client.XXXXXXXXXXXXX",
  "productId": "raspberry_alexa"
 }
}

この設定を読み込んでセットアップします

sudo bash setup.sh config.txt

ビルドするんでめっちゃ時間かかります。30~60分ほどの感じです。。。次に、サンプルアプリケーションを実行します。

sudo bash startsample.sh

これまた時間がかかりますが、すばらくすると以下の様にブラウザからの承認待ちの状態になります。

################################################################################################
#       To authorize, browse to: 'https://amazon.com/us/code' and enter the code: B8DEA5       #
################################################################################################

2018-07-08 11:17:10.405 [  2] 5 CBLAuthDelegate:handleRequestingToken
#################################################
#       Checking for authorization (1)...       #
#################################################

2018-07-08 11:17:10.407 [  2] 5 CBLAuthDelegate:requestToken
2018-07-08 11:17:10.923 [  2] 5 HttpPost:doPostSucceeded:code=400
2018-07-08 11:17:10.923 [  2] 5 CBLAuthDelegate:receiveTokenResponse:code=400
2018-07-08 11:17:10.924 [  2] 5 CBLAuthDelegate:mapHTTPStatusToError:code=400,error=INVALID_REQUEST
2018-07-08 11:17:10.925 [  2] 5 CBLAuthDelegate:errorInLwaResponseBody:error=authorization_pending,errorCode=AUTHORIZATION_PENDING
2018-07-08 11:17:10.925 [  2] 5 CBLAuthDelegate:setAuthError:authError=AUTHORIZATION_PENDING
2018-07-08 11:17:10.926 [  2] 0 CBLAuthDelegate:receiveTokenResponseFailed:result=AUTHORIZATION_PENDING
#################################################
#       Checking for authorization (2)...       #
#################################################

2018-07-08 11:17:15.927 [  2] 5 CBLAuthDelegate:requestToken
2018-07-08 11:17:16.349 [  2] 5 HttpPost:doPostSucceeded:code=400
2018-07-08 11:17:16.349 [  2] 5 CBLAuthDelegate:receiveTokenResponse:code=400
2018-07-08 11:17:16.349 [  2] 5 CBLAuthDelegate:mapHTTPStatusToError:code=400,error=INVALID_REQUEST
2018-07-08 11:17:16.350 [  2] 5 CBLAuthDelegate:errorInLwaResponseBody:error=authorization_pending,errorCode=AUTHORIZATION_PENDING
2018-07-08 11:17:16.350 [  2] 5 CBLAuthDelegate:setAuthError:authError=AUTHORIZATION_PENDING
2018-07-08 11:17:16.350 [  2] 0 CBLAuthDelegate:receiveTokenResponseFailed:result=AUTHORIZATION_PENDING
2018-07-08 11:17:21.351 [  2] 5 CBLAuthDelegate:requestToken
#################################################
#       Checking for authorization (3)...       #
#################################################

4,5,6と続く...

この状態で、https://amazon.com/us/codeにアクセスします。日本のamazon(amazon.co.jp)のアカウントでもこちらに飛んでください。

  1. コードを入力します。下の例ではB8DEA5です。
  2. 次に進んでアカウントを確認します。ここで、必ず最初にdeveloperに登録したアカウントになっているか必ず確認してください。
  3. 地域は日本に
  4. 規約等にokをしていくと無事に登録できます、

途中で画面が進んでWoopsみたいなこと言われた場合、考えられる原因は2つです。

  • 1つめは、アカウントを間違えている可能性です。上の手順2で必ず確認してください、違った場合は右上からサインアウトしてもう一度チャレンジしてください。(もしくは、この問題かも)
  • 2つめは、 config.txtを間違えている可能性です。config.txtを修正した場合は、sudo bash setup.sh config.txtからやり直してください。

ブラウザでsucceedと表示された場合は成功しました!。sshに戻ってください。

########################################
#       Alexa is currently idle!       #
########################################

と、どこかで出力されているはずです!これでRaspberryPiとalexaアプリケーションが紐づけられました!

この状態ではすでにAlexaはあなたの言葉を待っています。

アメリカンな口調で Alexa how about you?と聞いてみましょう。きっと答えてくれるはずです

4. 初期設定

この状態だと英語の勉強にはなりますが、日本語を喋ってもらいましょう。

言語設定

iを押してEnterでコマンド情報が見れます。

+----------------------------------------------------------------------------+
|                                  Options:                                  |
| Wake word:                                                                 |
|       Simply say Alexa and begin your query.                               |
| Tap to talk:                                                               |
|       Press 't' and Enter followed by your query (no need for the 'Alexa').|
| Hold to talk:                                                              |
|       Press 'h' followed by Enter to simulate holding a button.            |
|       Then say your query (no need for the 'Alexa').                       |
|       Press 'h' followed by Enter to simulate releasing a button.          |
| Stop an interaction:                                                       |
|       Press 's' and Enter to stop an ongoing interaction.                  |
| Privacy mode (microphone off):                                             |
|       Press 'm' and Enter to turn on and off the microphone.               |
| Echo Spatial Perception (ESP): This is for testing purpose only!           |
|       Press 'e' followed by Enter at any time to adjust ESP settings.      |
| Playback Controls:                                                         |
|       Press '1' for a 'PLAY' button press.                                 |
|       Press '2' for a 'PAUSE' button press.                                |
|       Press '3' for a 'NEXT' button press.                                 |
|       Press '4' for a 'PREVIOUS' button press.                             |
| Settings:                                                                  |
|       Press 'c' followed by Enter at any time to see the settings screen.  |
| Speaker Control:                                                           |
|       Press 'p' followed by Enter at any time to adjust speaker settings.  |
| Firmware Version:                                                          |
|       Press 'f' followed by Enter at any time to report a different        |
|       firmware version.                                                    |
| Info:                                                                      |
|       Press 'i' followed by Enter at any time to see the help screen.      |
| Reset device:                                                              |
|       Press 'k' followed by Enter at any time to reset your device. This   |
|       will erase any data stored in the device and you will have to        |
|       re-register your device.                                             |
|       This option will also exit the application.                          |
| Quit:                                                                      |
|       Press 'q' followed by Enter at any time to quit the application.     |
+----------------------------------------------------------------------------+
  1. c押してEnter
  2. 1押してEnter
  3. 6を押してEnter

すると以下の様に日本語になりました!

###################################
#       locale set to ja-JP       #
###################################

この状態で Alexa 明日の天気は!?というときちんと日本語で返してくれます!

ですが、いくらAlexa! Alexa! A・L・E・X・A・!といっても反応がありませんでした。。中華製マイクだしこんなもんですかね :cry:

こういうときは、tを押しましょう。呼びかけをする必要なく、スマートに明日の天気は?と聞くことができました(スマートとはいっていない)

その他環境設定

http://alexa.amazon.com/または https://alexa.amazon.co.jp(アカウントによる??)にアクセスし、アカウント情報を入力すると、現在つないでいるAlexaの設定や命令をブラウザ上で行えます。

スクリーンショット 2018-07-08 21.30.38.png

ぼくはAmazonPrime会員なので、ここにAmazpnMusicが自動でインポートされていました!!感動!!

スクリーンショット 2018-07-08 21.34.13.png

ということは、Alexaに喋りかけて、ぼくの珠玉のプレイリストをかけることもできるはず!と確信したので呼びかけました

ぼく:アレクサ 音楽を流して

Alexa:この端末ではAmazonMusicに対応しておりません

ぼく:😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢😢

ぼく:なんでや!管理画面からはAmazonPrimeの音楽流せたやん!!!

スクリーンショット 2018-07-08 21.50.50.png

ぼく:👊👊👊👊👊

所管

  • すごい便利
    • 一時期ゆっくりさんで、目覚ましアプリをつくってた時期を思い出した
    • それよりはるかに高機能すぎて嬉しい
  • スキル選び楽しい ヽ(゚∀゚ヽ 三 ノ゚∀゚)ノ
  • 自動起動の設定はまた今度かな
  • AmazonMusicの件に関しては金の亡者だと思った。
    • alexaの仕様よく読めよとか言わないでください
  • かわりにradikoを召喚した(代わりになってない)
  • いいマイク買ったほうがいい
    • 5000円くらいの買って試してみる予定
    • だれかうまくいった機種教えてください!!!
    • 試したら追記します

参考サイト

https://github.com/alexa/avs-device-sdk https://github.com/alexa/avs-device-sdk/wiki/Raspberry-Pi-Quick-Start-Guide-with-Script https://qiita.com/Dimeiza/items/182c4847d7c1ead7df54 http://blog.saboh.net/raspberrypiusbmaiku/ https://dev.classmethod.jp/voice-assistant/solution-of-a-problem-amazon-com-account-conflict/

RaspberryPiでのマイク・スピーカーの動作確認チートシート

# 音量変更
amixer sset PCM 100% 

# スピーカーテスト
speaker-test -t sine -f 1000

# マイクデバイス優先順位確認
arecord -l 

# マイクテスト
arecord -f S16_LE -r 44100 -D hw:1 | aplay

vue.jsでgoogleMapをPWAっぽく表示するオレオレプラクティス

こんばんは。switchを買ったけど放置してるmorifujiです。 ハマったら全てを放り出しそうで怖いです。

今日はGoogleMapとvuejsの組み合わせでごにょごにょしたのでその知見を共有します(PWA要素はほぼないです笑ごめんなさい)

とりあえず成果物▼

  • オリジナルのマーカー
  • マーカークリックするとwindow表示
  • マーカーアニメーションを設定
  • GeolocationAPIを使って現在地を取得しマーカーに追加
    • この部分の実装はこの記事には書いてません。

blog.gif

経緯

  • pwaでgoogleMapをごりごり使いたい案件が発生!?
    • 軽くしか触ったことない
    • しかも生js
  • いっちょオレオレGooglemapVueコンポーネント作るか

最低要件ライン

  • マーカーの追加ができること
  • マーカーのクリックイベントを簡単にlistenできること
  • マーカーの画像をカスタマイズできること
  • 最近のアップデートで現れた地図を移動させるには指 2 本で操作します仕様。今回はアプリ風pwaなのでこれを無効化すること
    • これがあるのとないのでPWAっぽさが出てくると考えてる!!
    • IMG_1452.PNG

調べた結果

vuejs + GoogleMapApiで色々調べてみたが、それなりに方法が出てきた。

1. vue2-google-maps

vue と googleMapApiで調べるとトップに出てくるライブラリ。

WeeklyDownloadsが1000超えてるので期待していた

<GmapMap
  :center="{lat:10, lng:10}"
  :zoom="7"
  map-type-id="terrain"
  style="width: 500px; height: 300px"
>
  <GmapMarker
    :key="index"
    v-for="(m, index) in markers"
    :position="m.position"
    :clickable="true"
    :draggable="true"
    @click="center=m.position"
  />
</GmapMap>

こんな感じでGmapコンポーネントのslotにマーカー を設定するようである。v-forで回してるからめちゃ簡単。

基準 vue2-google-maps
マーカーの追加 OK
マーカーのクリックイベント取得 OK
画像カスタマイズ OK
地図を移動させるには..問題 NG

地図を移動させるには...問題は解決しなかった。ちなみにいうとこの問題の解決方法は、 gestureHandling: 'greedy'を設定するだけだ。だがこれができない。GmapMapコンポーネントのpropsにつけてもうんともすんとも言わない。デモサイトには微塵もそんな設定がないし行き詰まった。

     var map = new google.maps.Map(document.getElementById('map'), {
          zoom: 13,
          center: locationRio,
          gestureHandling: 'greedy'
        });

出典:https://developers.google.com/maps/documentation/javascript/interaction?hl=ja

よくよく見ると、propsの項目が公式GoogleMapsJavascriptApi(以下、公式API)と大きく乖離しているような気がする。

propsに設定値を渡している時点で、公式apiとライブラリが密結合していて、このライブラリが今後もapiに柔軟に対応できるのか非常に疑問である。よって却下。

2. 生api + scriptjs

コンポーネントにすると、密結合になるから、やっぱり公式のapi普通のjsと同じように読み込むのがいいよね!(テノヒラクルー)

ですがgoogleさんからはモジュールが提供されていないので、vuejsの中でurlからjsを読込まないといけない。そうなるとindex.htmlに記載する必要がでてきて、関係ないページにも公式APIが読み込まれてパフォーマンスが下がる。たしかサイズが200kbぐらいあったしそれは避けたい。

ここでscriptjsを使うことにした。これを使うと、要するに、非常に簡単に非同期で外部jsが読み込めるというものらしい。

サンプルコード▼

// load jquery and plugin at the same time. name it 'bundle'
$script(['jquery.js', 'my-jquery-plugin.js'], 'bundle')
 
// load your usage
$script('my-app-that-uses-plugin.js')

/*--- in my-jquery-plugin.js ---*/
$script.ready('bundle', function() {
  // jquery & plugin (this file) are both ready
  // plugin code...
})

変なクセもなく、非常に使いやすい。以下のファイルは検証に使った筆者のvueのcomponent。

最低限のものだけpropsにしてるけど許してね 🤗 。

あと、let,const,var警察はあまり僕のコードを見ないでください ⚠️

<template>
  <div>
    <div
      id="map"
      :style="{width: mapWidth + 'px',height: mapHeight + 'px'}"/>
  </div>
</template>

<script>
var scriptjs = require("scriptjs");

export default {
  name: "Gmap",
  props: {
    mapWidth: {
      type: Number,
      default: 100
    },
    mapHeight: {
      type: Number,
      default: 100
    },
    lat: {
      type: Number,
      default: 34.722677
    },
    lng: {
      type: Number,
      default: 135.492364
    },
    zoom: {
      type: Number,
      default: 8
    },
    markers: {
      type: Array,
      default: () => {
        return [];
      }
    }
  },
  data() {
    return {
      map: null,
      formattedMarkers: []
    };
  },
  watch: {
    markers() {
      // マーカーを全削除
      this.formattedMarkers.forEach(marker => {
        marker.setMap(null);
      });
      // propsからも削除
      this.formattedMarkers.splice(0, this.formattedMarkers.length);

      // 再描画
      this.addMarker();
    }
  },
  created() {
    scriptjs(
      "https://maps.googleapis.com/maps/api/js?key=XXXXXXXXXXXXXXXXXXXXXXX&callback=initMap",
      "loadGoogleMap"
    );
    scriptjs.ready("loadGoogleMap", this.loadMap);
  },
  mounted() {},
  methods: {
    addMarker() {
      this.markers.forEach(markerInfo => {
        var contentString =
          '<div id="content">' +
          '<div id="siteNotice">' +
          "</div>" +
          '<h1 id="firstHeading" class="firstHeading">Uluru</h1>' +
          '<div id="bodyContent">' +
          "<p><b>Uluru</b>, also referred to as <b>Ayers Rock</b>, is a large " +
          "sandstone rock formation in the southern part of the " +
          "Northern Territory, central Australia. It lies 335&#160;km (208&#160;mi) " +
          "south west of the nearest large town, Alice Springs; 450&#160;km " +
          "(280&#160;mi) by road. Kata Tjuta and Uluru are the two major " +
          "features of the Uluru - Kata Tjuta National Park. Uluru is " +
          "sacred to the Pitjantjatjara and Yankunytjatjara, the " +
          "Aboriginal people of the area. It has many springs, waterholes, " +
          "rock caves and ancient paintings. Uluru is listed as a World " +
          "Heritage Site.</p>" +
          '<p>Attribution: Uluru, <a href="https://en.wikipedia.org/w/index.php?title=Uluru&oldid=297882194">' +
          "https://en.wikipedia.org/w/index.php?title=Uluru</a> " +
          "(last visited June 22, 2009).</p>" +
          "</div>" +
          "</div>";

        // マーカー
        let marker = new google.maps.Marker({
          position: markerInfo.position,
          icon:
            "https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png",
          map: this.map,
          // ポップなアニメーションを付与
          animation: google.maps.Animation.DROP
        });

        // マーカーのwindow
        let infowindow = new google.maps.InfoWindow({
          content: contentString
        });

        // マーカークリック時にwindow表示
        marker.addListener("click", function() {
          infowindow.open(this.map, marker);
        });
        this.formattedMarkers.push(marker);
      });
    },
    loadMap() {
      // googleMapを初期化
      this.map = new google.maps.Map(document.getElementById("map"), {
        center: { lat: this.lat, lng: this.lng },
        zoom: this.zoom,
        // スワイプ判定を強めに設定(地図を移動させるには..問題)
        gestureHandling: "greedy"
      });
      this.addMarker();
    }
  }
};
</script>

結果、要件最低ラインをクリアした。

基準 vue2-google-maps api + scriptjs
マーカーの追加 OK OK
マーカーのクリックイベント取得 OK OK
画像カスタマイズ OK OK
地図を移動させるには..問題 NG OK

3. google-maps

もういっちょ最後に検証してみた。

生のjsはなんか心がザワザワしたのでvueのコンポーネントにこだわらず、モジュール単位でもうちょいいいのがないか探した結果がこれ。ほんとにただ単にmodule化しただけのもの。

生api + scriptjsと違う主な点は以下のとおり

  • API_KEYとかLOCALEとか仕様ライブラリを定数として設定できて非常に気持ちよかった(options)
  • 非同期メソッドにloadonLoadがあるという謎仕様(loadは分かるがonLoadはどういう場面で使えるん??)
  • releaseというメソッド(API_KEYとかをclearするメソッドかな?)もあるがSPAなのでうまく動かなかった。(ここ要調査)
  • やっぱり非公式だからどこまでメンテするか謎い

使ったvueコンポーネントはこちら(let,const,var警察はあまり僕のコードを見ないでください)▼

<template>
  <div>
    <div
      id="map"
      :style="{width: mapWidth + 'px',height: mapHeight + 'px'}"/>
    <button
      class="button"
      @click="release">無効化</button>
  </div>
</template>

<script>
let GoogleMapsLoader = require("google-maps");

GoogleMapsLoader.KEY = "XXXXXXXXXXXXXXXXXXXXXx";
GoogleMapsLoader.LANGUAGE = "ja";

export default {
  name: "Gmap",
  props: {
    mapWidth: {
      type: Number,
      default: 100
    },
    mapHeight: {
      type: Number,
      default: 100
    },
    lat: {
      type: Number,
      default: 34.722677
    },
    lng: {
      type: Number,
      default: 135.492364
    },
    zoom: {
      type: Number,
      default: 8
    },
    markers: {
      type: Array,
      default: () => {
        return [];
      }
    }
  },
  data() {
    return {
      map: null,
      formattedMarkers: []
    };
  },
  watch: {
    markers() {
      // マーカーを全削除
      this.formattedMarkers.forEach(marker => {
        marker.setMap(null);
      });
      // propsからも削除
      this.formattedMarkers.splice(0, this.formattedMarkers.length);

      // 再描画
      this.addMarker();
    }
  },
  created() {},
  mounted() {
    // googleMap描画
    GoogleMapsLoader.load(this.loadMap);
  },
  methods: {
    addMarker() {
      this.markers.forEach(markerInfo => {
        var contentString =
          '<div id="content">' +
          '<div id="siteNotice">' +
          "</div>" +
          '<h1 id="firstHeading" class="firstHeading">Uluru</h1>' +
          '<div id="bodyContent">' +
          "<p><b>Uluru</b>, also referred to as <b>Ayers Rock</b>, is a large " +
          "sandstone rock formation in the southern part of the " +
          "Northern Territory, central Australia. It lies 335&#160;km (208&#160;mi) " +
          "south west of the nearest large town, Alice Springs; 450&#160;km " +
          "(280&#160;mi) by road. Kata Tjuta and Uluru are the two major " +
          "features of the Uluru - Kata Tjuta National Park. Uluru is " +
          "sacred to the Pitjantjatjara and Yankunytjatjara, the " +
          "Aboriginal people of the area. It has many springs, waterholes, " +
          "rock caves and ancient paintings. Uluru is listed as a World " +
          "Heritage Site.</p>" +
          '<p>Attribution: Uluru, <a href="https://en.wikipedia.org/w/index.php?title=Uluru&oldid=297882194">' +
          "https://en.wikipedia.org/w/index.php?title=Uluru</a> " +
          "(last visited June 22, 2009).</p>" +
          "</div>" +
          "</div>";

        // マーカー
        let marker = new google.maps.Marker({
          position: markerInfo.position,
          icon:
            "https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png",
          map: this.map,
          // ポップなアニメーションを付与
          animation: google.maps.Animation.DROP
        });

        // マーカーのwindow
        let infowindow = new google.maps.InfoWindow({
          content: contentString
        });

        // マーカークリック時にwindow表示
        marker.addListener("click", function() {
          infowindow.open(this.map, marker);
        });
        this.formattedMarkers.push(marker);
      });
    },
    loadMap(google) {
      // googleMapを初期化
      this.map = new google.maps.Map(document.getElementById("map"), {
        center: { lat: this.lat, lng: this.lng },
        zoom: this.zoom,
        // スワイプ判定を強めに設定(地図を移動させるには..問題)
        gestureHandling: "greedy"
      });
      this.addMarker();
    },
    release() {
      GoogleMapsLoader.release(function() {
        console.log("No google maps api around");
      });
    }
  }
};
</script>

こちらも要件最低ラインはクリアした。

基準 vue2-google-maps api + scriptjs google-maps
マーカーの追加 OK OK OK
マーカーのクリックイベント取得 OK OK OK
画像カスタマイズ OK OK OK
地図を移動させるには..問題 NG OK OK

まとめ

  • 生api+scriptjsgoogle-mapsだと拡張性高い!
  • なんでもかんでもvueコンポーネントにするのは良くない。
    • 公式への対応性・拡張性がなくなるため(仕様が変わったら、中の人が頑張らないと新しい仕様に対応することが難しい 😢)
    • その点、jsを軽くモジュール化したものは、要所要所はjsなので拡張性高いかな?
  • モジュール化されてなくても、頑張ればvueのsfcで非同期に使える。
  • npmのWeeklyDownloads数はあまり当てにならない 😢
  • let,const,var警察はあまり僕のコードを見ないでください

おまけ

googleMapの幅を画面いっぱいにしたい

それ、vuejsなら解決します!

  1. 画面幅を取得して
  2. styleにバインド
<template>
  <div>
  <!-- 2. styleにバインド -->
    <div
      id="map"
      :style="{width: mapWidth + 'px',height: mapHeight + 'px'}"/>
  </div>
</template>

<script>
export default {
  name: "Gmap",
  data() {
    return {
       mapHeight: 500,
       mapWidth: 1000
    };
  },
  created() {
    // 1. 画面幅を取得して
    this.mapWidth = window.innerWidth;
  }
};
</script>

サクッとjsでQRリーダー実装

こんばんは。

最近チャリンコを購入して、チャリンコ通勤始めました。道中のお弁当屋とか発見できてQOL爆上げ中のmorifujiです。

背景

  • Vuejsでできること色々調べる機会があった
  • いろんなWebAPIが存在していることに気づいた
  • 案件の仕様の中でwebアプリケーション内のみでqr読み取る必要が出てきた

目標

  1. ブラウザで指定のURLを開き
  2. QRリーダーを起動(スマホのカメラ画面にならずに)
  3. QRを撮影すると、瞬時に解析して画面に表示

スマートフォンでの検証はまだ。確認し次第追記します m( )m

実装後デモ▼ chat_2.gif

shape detection API (不採用)

リアルタイムに物体を認識するためのAPI

人間の顔(FaceDetection)とバーコード(BarcodeDetection)の二種類がある。

https://wicg.github.io/shape-detection-api/

試してみた。

// 顔認識
if (window.FaceDetector == undefined) {
  console.error('Face Detection not supported on this platform');
}

// バーコード認識
if (window.BarcodeDetector == undefined) {
  console.error('Barcode Detection not supported on this platform');
}


// 今回はバーコード認識
let barcodeDetector = new BarcodeDetector();
// Assuming |theImage| is e.g. a <img> content, or a Blob.
barcodeDetector.detect(theImage)
.then(detectedCodes => {
  for (const barcode of detectedCodes) {
    console.log(' Barcode ${barcode.rawValue}' +
        ' @ (${barcode.boundingBox.x}, ${barcode.boundingBox.y}) with size' +
        ' ${barcode.boundingBox.width}x${barcode.boundingBox.height}');
  }
}).catch(() => {
  console.error("Barcode Detection failed, boo.");
})

結果。どれも未対応。悲しい

ブラウザ 対応有無
firefox X
iOSsafari X
chrome X

getUserMedia

WebRTCを実現するためのAPIの一種のようだ。以下の二種類あるが、前者Navigatorはバグが発見されているので、廃止予定となっている。

試して見た。

js部分▼

let p = navigator.mediaDevices.getUserMedia({ audio: false, video: true });

p.then(function(stream) {
   var video = document.querySelector('video');
   video.src = window.URL.createObjectURL(mediaStream);
   video.onloadedmetadata = function(e) {
      // Do something with the video here.
   };
};

html部分▼

        <div class="container">
          <div class="columns">
            <div class="column">
              <video
                :width="width"
                :height="height"
                autoplay/>
            </div>
           </div>
         </div>

firefoxでできた!!

スクリーンショット 2018-06-13 23.21.13.png

しかし、createObjectURLは廃止予定だから使うな!という警告をconsoleログで受けた。。。 調べてみるとバグがあるらしい。

スクリーンショット 2018-06-13 23.22.46.png

createObjectURLではなく、videoのdomのsrcObjectに直接ぶっ込めばいいとのこと。修正▼

// var video = document.querySelector('video');
// video.src = window.URL.createObjectURL(mediaStream);
document.querySelector("video").srcObject = mediaStream;

canvasにコピー

qrを読み取るライブラリはあまたあるようだが、そのどれも、 canvasからimageを取得している。 なのでまずはwebカメラを表示しているvideoタグの動画を、canvasにコピーしないといけない。

以下、先ほどのコードに追記

 const video = document.querySelector("video");
 const canv = document.querySelector("canvas");
 const context = canv.getContext("2d");

// canvasに描写
 context.drawImage(video, 0, 0, this.width, this.height);

これでボタンを押すたびにvideoタグからcanvasにコピーする(スクショする)ような感じになった。

chat.gif

※上のgifだと、domのサイズ指定してないのでcanvasが引き伸ばされてます

LazarSoft/jsqrcode (不採用)

こちらの記事を参考にQR画像からデータをデコードするライブラリを探した。

start数につられてLazarSoft/jsqrcodeを使おうとしたが、良いサンプルがなかったのと、最終更新が古かったので、やめた、

cozmo/jsQR

最近も開発が進んでいるようなので、このライブラリに決めた。使い方はこんな感じ

const code = jsQR(imageData, width, height);

if (code) {
  console.log("Found QR code", code);
}

めっちゃ簡単。今までのと合わせると、以下のような感じ

      const video = document.querySelector("video");
      let canv = document.querySelector("canvas");

      const context = canv.getContext("2d");
      context.drawImage(video, 0, 0, this.width, this.height);
      const imageData = context.getImageData(0, 0, this.width, this.height);
      const code = jsQR(imageData.data, imageData.width, imageData.height);
      if (code) {
        console.log("Found QR code", code, code.data);
      }

jsQR()の第一引数には、Uint8ClampedArray型が来るはずだが、どうやら canvas.getContext("2d").getImageData(0, 0, this.width, this.height);で取得できるようだ。

定期的に画像を認識

今の状態だと、QRを認識するには毎回ボタンを押す必要があるので、setIntervalで毎時間(0.5秒)ごとにcanvas描写&qr読取できるように修正

      const video = document.querySelector("video");
      let canv = document.querySelector("canvas");

      const context = canv.getContext("2d");
      setInterval(() => {
          context.drawImage(video, 0, 0, this.width, this.height);
          const imageData = context.getImageData(0, 0, this.width, this.height);
          const code = jsQR(imageData.data, imageData.width, imageData.height);
          if (code) {
              console.log("Found QR code", code, code.data);
        }
      }, 500);

負荷を軽減

macがホカホカになっていたので、負荷を軽減したい。

videoタグとcanvasタグで同時に描写が起こってるので、canvasタグの方をjs内で仮想DOMとして扱うことにした。修正したもの▼

// let canv = document.querySelector("canvas");
let canv = document.createElement("canvas");
canv.height = this.height;
canv.width = this.width;

videoタグのフレームレートを落とす

getUserMedia()の引数を以下のように修正

   
    // let p = navigator.mediaDevices.getUserMedia({ audio: false, video: true });
    let p = navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          width: this.width,
          height: this.height,
          frameRate: { ideal: 5, max: 15 }
        }
    });

まとめ

筆者はvuejsのsfcでwebを作っているので、こんな感じのvueコンポーネントにまとめることができました。

<template>
  <div>
    <section>
      <div class="container">
        <div class="columns">
          <div class="column">
            <b-message
              :active.sync="isReadQr"
              title="読み取り内容">
              {{ json }}
            </b-message>
            {{ isReadQr }}
          </div>
        </div>
      </div>
    </section>
    <section class="hero is-medium has-text-centered">
      <div class="hero-body hero-body-hp-main">
        <div class="container">
          <div class="columns">
            <div class="column">
              <button
                class="button"
                @click="cameraStart">カメラスタート</button>
            </div>
            <div class="column">
              <button
                class="button"
                @click="readImage">読み取りスタート</button>
            </div>
          </div>
          <div class="columns">
            <div class="column">
              <video
                :width="width"
                :height="height"
                autoplay/>
            </div>
            <div class="column">
              <div :style="{ width: width + 'px', height: height+ 'px' }">
                <!-- canvasなしでも仮想DOMを作成して描画 -->
                <!-- <canvas
                  :width="width"
                  :height="height"
                /> -->
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  </div>
</template>

<script>
import jsQR from "jsQR";

export default {
  name: "Qr",
  data() {
    return {
      srcObject: "",
      width: 500,
      height: 500,
      json: null
    };
  },
  computed: {
    isReadQr: () => {
      return Boolean(this.json);
    }
  },
  methods: {
    cameraStart() {
      const p = navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          width: this.width,
          height: this.height,
          frameRate: { ideal: 5, max: 15 }
        }
      });
      p.then(function(mediaStream) {
        document.querySelector("video").srcObject = mediaStream;
      });
    },
    readImage() {
      const video = document.querySelector("video");
      const canv = document.createElement("canvas");
      canv.height = this.height;
      canv.width = this.width;

      const context = canv.getContext("2d");

      setInterval(() => {
        console.log("search .....");
        context.drawImage(video, 0, 0, this.width, this.height);
        const imageData = context.getImageData(0, 0, this.width, this.height);
        const code = jsQR(imageData.data, imageData.width, imageData.height);
        if (code) {
          console.log("Found QR code", code, code.data);
          this.json = code.data;
        }
      }, 500);
    }
  }
};
</script>

デモ▼ chat_2.gif

行きつけの美容院さんのQRコードを使わさせていただきました。感謝 m( )m

所感

  • もはやwebでなんでもできるなあと思った。こういうところからpwaが流行りそうな理由なのか?
  • QRが遠いと思ったより認識が弱い。
    • 他のライブラリと比較したい(するとはいっていない)
  • アンドロイド・iPhoneでの検証は未検証なのできちんとやりたい。
  • Web Bluetooth APIも触りたい