vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

Kansai.kt #2で発表してきました

はじめに

11/26に行われたKansai.kt #2で、ラムダ式について発表してきました。

kansai-kt.connpass.com

資料

KotlinとLambda式

テーマについて

このテーマにした理由は、そもそも私がKotlinに触るきっかけになったことが、 Androidで使うことができること、前回のテーマとしたNull安全、そしてラムダ式Androidでも使えるということでした。

Null安全の発表については(一応)挑戦したため、もう一つのラムダ式に挑戦した、ということです。

内容について

これまでなんとなくコードを書いたことはあったものの、どういう関数がラムダ式として書けるのか、 そもそもラムダ式ってなんぞやといったところはわかっていなかったため、そこに少しでも触れられたことは良かったと思います。

ただ、20~30分の発表時間を結構余らせてしまったため、もっと色々なところに突っ込んだり、 もしくはせっかく以前Scala Kansai Summitに参加させてもらったので、関数型プログラミング的なアプローチをしてみる、といったところに挑戦しても良かったのかもしれません。

これは発表する・しないはともかく今後挑戦してみたいですね。

他の方の発表について

次期バージョンであるKotlin1.1のお話、Android開発におけるJavaからKotlinにコンバートした後にやるべき処理、IntelliJ IDEAのPostfixコード補完のお話と、どれも興味深く自分も取り入れていきたいな、と思いながら聞いていました。

そして結果的に今回のメインになったと言っても過言ではないReactiveProgrammingとRxJavaのお話。 AndroidでRxJavaに触れたり、UnityでUniRxを利用していたりと、Rx自体には普段から触れているものの、概念・考え方というところではなんとなくのところが多すぎる気はしていたので、基礎的な部分からガッツリお話していただけたのはありがたかったです。

CyberMondayでセール中だったこともあり本も購入してしまいましたので、こちらも進めていきたいです。

Reactive Programming with RxJava - O'Reilly Media

おわりに

Kotlinに限ったことではありませんが、Androidなどのアプリケーションを作る手法だけでなく、言語自体に対して学ぶ、というのもとても楽しく、また有意義だと思います。

次回は内容が変わるらしいというお話もあるので?どうなるかは分かりませんが、 発表の機会があればより内部の突っ込んだところまでいけるように頑張ります。

スポンサーとなっていただいたはてな様、主催者の皆様、そして参加者の皆様、本当にありがとうございましたm( )m

Kotlin Koansやってみたメモ Conventions編

はじめに

前回の続きです。

今回はConventionsにトライしたときのメモです。

ここではまずComparisonで下記のようなDataクラスを作り、これをベースに問題を解いていきます。

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) : Comparable {
    override fun compareTo(other: MyDate) = when {
        year != other.year -> year - other.year
        month != other.month -> month - other.month
        else -> dayOfMonth - other.dayOfMonth
    }
}

このクラスではComparableを継承し、compareToで日付の比較ができるようにしています。

Conventions - In range

KotlinではOperatorを オーバーロードすることができます。

例えば下記のTestDataクラス同士は、「+」または「plus」で足し合わせることができます。

data class TestData(var num: Int, var text: String){
    operator fun plus(addData: TestData): TestData = TestData(this.num + addData.num, this.text + addData.text)
}
@Throws(IOException::class)
override fun start(primaryStage: Stage) {
    var originaldata = TestData(0, "test")
    var adddata = TestData(1, "2")

    // どちらもOK
    originaldata = originaldata.plus(adddata)
    originaldata = originaldata + adddata
}

で、ここではある日付(MyDate)が指定の期間内に含まれるかをチェックするため、containsを持つDataRangeクラスを作成します。

class DateRange(val start: MyDate, val endInclusive: MyDate){
    operator fun contains(item: MyDate) = item >= start && item <= endInclusive
}

Kotlin Koansではこのクラスを「in」を使って利用しています。

@Throws(IOException::class)
override fun start(primaryStage: Stage) {
    val dateTarget = MyDate(2016, 10, 9)
    val dateFirst = MyDate(2016, 9, 30)
    val dateLast = MyDate(2017, 11, 1)
    // Trueが返る
    val result = checkInRange(dateTarget, dateFirst, dateLast)
}
fun checkInRange(date: MyDate, first: MyDate, last: MyDate): Boolean {
    return date in DateRange(first, last)
}

他にも下記のように呼び出すことも可能です。

@Throws(IOException::class)
override fun start(primaryStage: Stage) {
    val dateTarget = MyDate(2016, 10, 9)
    val dateFirst = MyDate(2016, 9, 30)
    val dateLast = MyDate(2017, 11, 1)

    val dataRange = DateRange(dateFirst, dateLast)
    // Trueが返る
    var result = dataRange.contains(dateTarget)
}

なお、containsで日付を比較するところでは、先に実装しているcompareToが呼ばれます。

Conventions - Range to

MyDateクラスに日付の範囲を返すrangeToを追加します。

operator fun MyDate.rangeTo(other: MyDate) = DateRange(this, other)
class DateRange(override var start: MyDate, override val endInclusive: MyDate): ClosedRange

fun checkInRange(date: MyDate, first: MyDate, last: MyDate): Boolean {
    return date in first..last
}

ある日付(date)が日付の範囲(firstからlast)に含まれるかを確認している「date in first..last」は、下記のように書くことも可能です。

val dateTarget = MyDate(2016, 10, 9)
val dateFirst = MyDate(2016, 9, 30)
val dateLast = MyDate(2017, 11, 1)

// 日付の範囲(DataRange)を作る
val dateRange = dateFirst.rangeTo(dateLast)
// Trueが返る
val res = dateRange.contains(dateTarget)

ここで気になったのがDateRangeクラス。
引数がOverrideされていて、Kotlinてこんなこともできるのかぁ(こなみかん)という感じだったのですが、これはClosedRangeクラスを継承しているためのようです。

サブクラスで引数の値を変更するにはOverrideが必要、という。

Conventions - For loop

For-loop rangeについて。

ここではDateUtil.ktも使って解いていくのですが、面倒なので一つのクラスにまとめて書いていたらちょっとハマりましたorz

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) : Comparable {
    override fun compareTo(otherItem: MyDate) = when {
        year != otherItem.year -> year - otherItem.year
        month != otherItem.month -> month - otherItem.month
        else -> dayOfMonth - otherItem.dayOfMonth
    }
}
operator fun MyDate.rangeTo(other: MyDate) = DateRange(this, other)

inner class DateRange(val start: MyDate, val end: MyDate): Iterable{
    override fun iterator(): Iterator = DateIterator(this)
}
fun MyDate.nextDay() = addTimeIntervals(TimeInterval.DAY, 1)

enum class TimeInterval {
    DAY,
    WEEK,
    YEAR
}
fun MyDate.addTimeIntervals(timeInterval: TimeInterval, number: Int): MyDate {
    val c = Calendar.getInstance()
    c.set(year + if (timeInterval == TimeInterval.YEAR) number else 0, month, dayOfMonth)
    var timeInMillis = c.getTimeInMillis()
    val millisecondsInADay = 24 * 60 * 60 * 1000L
    timeInMillis += number * when (timeInterval) {
        TimeInterval.DAY -> millisecondsInADay
        TimeInterval.WEEK -> 7 * millisecondsInADay
        TimeInterval.YEAR -> 0L
    }
    val result = Calendar.getInstance()
    result.setTimeInMillis(timeInMillis)
    return MyDate(result.get(Calendar.YEAR), result.get(Calendar.MONTH), result.get(Calendar.DATE))
}
inner class DateIterator(val dateRange:DateRange) : Iterator {
    var current: MyDate = dateRange.start
    override fun next(): MyDate {
        val result = current
        current = current.nextDay()
        return result
    }
    override fun hasNext(): Boolean = current <= dateRange.end
}
fun iterateOverDateRange(firstDate: MyDate, secondDate: MyDate, handler: (MyDate) -> Unit) {
    for (date in firstDate..secondDate) {
        handler(date)
    }
}
  • DateRange、DateIteratorクラスは「inner」をつけて内部クラスにする必要があり、ただネストしただけでは外側にある「MyDate.nextDay()」などにアクセスできません。

Iterable、Iterator

この問題では、for文である期間(firstDate)からある期間(SecondDate)までの日をhandlerに渡す、という処理を作ります。

で、まず「date in firstDate..secondDate」を実現するために「Iterable」を継承したクラス(DateRamge)が必要で、このクラスを作るために「Iterator」を継承したクラス(DateIterator)が必要になる、ということになります。

Iteratorでは渡された日付が「secondDate」より前かを確認する「hasNext」と、次の日を返す「next」を実装しています。

Conventions - Operators overloading

日付の足し算と、それを繰り返して処理するためのクラスを作ります。

で、そうそうにギブアップしましてorz、答えを見てみました。

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)

enum class TimeInterval { DAY, WEEK, YEAR }

operator fun MyDate.plus(timeInterval: TimeInterval): MyDate = addTimeIntervals(timeInterval, 1)

fun task1(today: MyDate): MyDate {
    return today + TimeInterval.YEAR + TimeInterval.WEEK
}

class RepeatedTimeInterval(val timeInterval: TimeInterval, val number: Int)
operator fun TimeInterval.times(number: Int) = RepeatedTimeInterval(this, number)
operator fun MyDate.plus(timeIntervals: RepeatedTimeInterval) = addTimeIntervals(timeIntervals.timeInterval, timeIntervals.number)


fun task2(today: MyDate): MyDate {
    return today + TimeInterval.YEAR * 2 + TimeInterval.WEEK * 3 + TimeInterval.DAY * 5
}
fun MyDate.addTimeIntervals(timeInterval: TimeInterval, number: Int): MyDate {
    val c = Calendar.getInstance()
    c.set(year + if (timeInterval == TimeInterval.YEAR) number else 0, month, dayOfMonth)
    var timeInMillis = c.getTimeInMillis()
    val millisecondsInADay = 24 * 60 * 60 * 1000L
    timeInMillis += number * when (timeInterval) {
        TimeInterval.DAY -> millisecondsInADay
        TimeInterval.WEEK -> 7 * millisecondsInADay
        TimeInterval.YEAR -> 0L
    }
    val result = Calendar.getInstance()
    result.setTimeInMillis(timeInMillis)
    return MyDate(result.get(Calendar.YEAR), result.get(Calendar.MONTH), result.get(Calendar.DATE))
}

とりあえずtask1の方は問題なさそうですね。MyDateに対して「+」を使ったときに、関数オブジェクト「addTimeIntervals」が呼ばれるようにする、と。

ちなみに、関数オブジェクトの書き方が以前は「operator fun MyDate.plus(timeInterval: TimeInterval): MyDate = ::addTimeIntervals」のようになっていましたが、仕様変更があったのですね(今更感)。

問題はtask2の方。

MyDateに対して「* 数値」が行われた場合に、「+」をその数値文繰り返す、という内容となります。
で、この繰り返しを担う関数が「times」です。

「TimeInterval.times(number: Int)」で「RepeatedTimeInterval」というクラスが返されるようにしています。

で、このままだと「+」の型が違う(´;ω;`)とエラーになるので、「MyDate.plus(timeIntervals: RepeatedTimeInterval)」で対応、という流れです。

これ、今もなお答え見ないと回答できる気がしない...orz

Conventions - Destructuring declarations

Data クラスは分割して個別に扱うことができます、という内容でした。

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)

@Throws(IOException::class)
override fun start(primaryStage: Stage) {
    val date = MyDate1(2016, 11, 12)
    val (year, month, dayOfMonth) = date

    println(year.toString())
    println(month.toString())
    println(dayOfMonth.toString())
}

こんな感じ。

Conventions - Invoke

最後はInvokeの問題。

class Invokable {
    public var numberOfInvocations: Int = 0
        private set
    operator public fun invoke(): Invokable {
        numberOfInvocations++
        return this
    }
}
fun invokeTwice(invokable: Invokable) = invokable()()

例えば下記のようにすると「invoke()」が2回呼ばれるので「numberOfInvocations」の値は2になります。

@Throws(IOException::class)
    override fun start(primaryStage: Stage) {
    // 初期化時はnumberOfInvocations = 0
    var invokable = Invokable()
    // invoke()が二回呼ばれるのでnumberOfInvocations = 2
    invokable()()
}

このInvokeですが、ググってみるとカリー化と呼ばれる複数の引数を持つ一つの関数を、一つの引数を持つ複数の関数に置き換える際に登場しているようです。

これについてはこの辺りが参考になりそうです。

おわりに

ConventionsではKotlin特有のテクニック、というよりはKotlinを使って「+」などのオペレーターを自分で作ることで言語仕様に触れる、という内容だった気がします。

正直まだ回答を見ながらふむふむそういう風に書くのか、と思っているだけの状態ではあるので、時間を置いてもう少し理解できるようになったら追記するか別に訂正記事を書こうかと思います。

本当は11/26のKansai.ktまでに、次のCollectionsまで行きたかったところですが、このペースだと資料作る時間もなくなりそうなので一旦ストップしてそちらの準備を進めたいと思います。

kansai-kt.connpass.com

参考

Operator overloading

ClosedRange

Iterator

分解宣言

カリー化

Kotlin Koansやってみたメモ Introduction編

はじめに

今月11月26日に2回目のKansai.ktが行われます。

kansai-kt.connpass.com

ワーパチパチ

ということで、今更ながらKotlin Koansにトライしてみました。
すると結構答えまで見ても内容がよくわからず、あれこれ調べてみたりしたのでそのあたりをまとめていきます。

いつも以上にあやふやな内容になるかと思いますので、正しくは公式ドキュメント赤べこ本など参照ということで。 (内容へのツッコミはお受けしますのでぜひどうぞm( )m)

Introduction - Extension functions

拡張関数について。

Int、Pairに対して有理数(RationalNumber)を返す「r」という関数を追加しています。
回答は下記です。

fun Int.r(): RationalNumber = RationalNumber(this, 1)
fun Pair.r(): RationalNumber = RationalNumber(first, second)

data class RationalNumber(val numerator: Int, val denominator: Int)

まず気になったのは、Int.r()にRationalNumberを渡すときの第二引数が1であること。
これは有理数の特徴である「分数であらわすことのできる数」を「第一引数/1」として元の値(第一引数)を変化させずに表現するため、ということのようです。

Pairは任意の2つの値を一纏めにするので、それぞれの値を第一、第二引数としています。
実際の利用には第二引数が0でないか、無理数でないかなどのチェックは必要そうです。

Introduction - Object expressions

オブジェクト式を使う問題。

回答はコチラ。

val arrayList = arrayListOf(1, 5, 2)
Collections.sort(arrayList, object : Comparator {
    override fun compare(x: Int, y: Int) = y - x
})

Comparator の compare を使って要素をソートしており、「y - x」の結果がマイナスの場合はyをxより前方に配置する、といった感じで並び替えを行います。

Introduction - SAM conversions

先程オブジェクト式を使ったコード、実は無名関数を使って以下のように書くこともできます。

val arrayList = arrayListOf(1, 2, 3)
Collections.sort(arrayList, { x, y ->
    y - x
})

Comparator の compareのように、オーバーライドする関数が一つだけ(Single Abstract Method: SAM)の場合はそれを省略して記述することができます。

なお、引数の型を明示することも可能です。

val arrayList = arrayListOf(1, 2, 3)
Collections.sort(arrayList, { x: Int, y: Int ->
    y - x
})

ただ、無名関数を使うと呼び出しのたびにインスタンスが作成されることになるため、一度しか実行されない場合を除き、オブジェクト式を使う方が良いようです。

別件ですが、無名関数の引数をカッコ「()」でくくる((x: Int, y: Int) -> のようにする)とエラーになります(下記コメントも参照)。

Introduction - Extension functions on collections

readonlyとmutableについて。

Listはreadonlyのため、下記のように後から要素を追加しようとするとエラーになります。

val arrayList: List = arrayListOf(1, 2, 3)
arrayList.add(0)

ただしImmutableではないので、ソートするなど値を変更することは可能です。というお話。

参考

有理数

Pair

オブジェクト式

Comparator

抽象メソッド

関モバ #19で発表してきました

はじめに

10/26に行われた関モバ #19で発表してきました。

kanmoba.connpass.com

今回のネタは前回のWebVR。

https://masanori840816.github.io/KanMoba19/slidepage.html

緊張もしたし、時間も少しオーバーしたしで反省点も多いですが、まぁやりたいことはやれたかなと。
何より冒頭の ハロウィン -> 仮装 -> 仮想 -> 仮想現実 のネタがカブらなくて良かったww

準備

10月に入って多少は軽減されたものの、なんだか微妙に忙しく、資料の準備の時間が取れていなかったこともあり、思い切って一日休んで準備に当てることにしました。

で、作業する場所が必要ってことでコワーキングスペースに行くことにしました。

oinai-karasuma.jp

場所的に烏丸御池に近く、なかなか良かったと思います。
何より利用するのに何も聞かれなかったのが良かったw (以前利用した、住所など聞いてきたところが特殊だったのかもですが)

スライドについて

発表で使用するスライドは、いつもLibreOffice Impressで作ったものをPDFに変換してSlideShareにアップロードしていました。

ただ正直結構面倒で、内容がコード主体になるのだから、もっとシンプルで良いのでは?と思ったので、今回はGitHub PagesとRemark.jsを使って作成してみました。

GitHub Pages

GitHub PagesはGitHubを使って静的なWebページを表示できるサービスです。

GitHubで ユーザー名.github.io というリポジトリを作成し、「index.html」というファイルを直下に置いてやると、ユーザー名.github.ioというページにアクセスできるようになります。

https://masanori840816.github.io/
※空のindex.htmlを置いているので、アクセスしても真っ白なページが表示されるだけですが。

で、それは一旦置いておいて、もう一つ別にリポジトリを作成します。

そのリポジトリをcloneし、「git checkout gh-pages」でブランチを切り(オプション--orphanはつけている場合とつけていない場合がありました)、そこに後述するWebページの素材を置くことで、下記のようなページにアクセスできるようになります。

https://masanori840816.github.io/KanMoba19/slidepage.html

Remark.js

Remark.jsは、Markdown形式のファイルを読み込んでスライドっぽく表示してくれる、Javascriptのライブラリです。

使い方としては、Remark.jsを読み込んで、コンストラクタ生成時に読み込むファイルを指定するだけです。

slidepage.html

< !DOCTYPE html >
< html lang="jp" >
< head >
    < meta charset="UTF-8" / >
    < title >Mobile + Web + VR< /title >
    < style type="text/css" >
        @import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz);
        @import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic);
        @import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700,400italic);
    < /style >
< /head >
< body style="color: #000000;">
    < script src="js/remark-latest.min.js" type="text/javascript" >< /script >
    < script type="text/javascript" src="js/slidepage.js" >< /script >
< /body >
< /html >

slidepage.js

(function () {
    var slideshow = remark.create({sourceUrl: "md/mobilewebvr.md"});
}).call(this);

とってもシンプルですね。

で、あとはMarkdownで内容を書いていくだけです。

mobilewebvr.md

< div style='font-size: 80px; position:absolute; top:30%; left: 10%;' >
Mobile + Web + VR
< /div >
< div style='font-size:40px; position: absolute; bottom: 5%; left: 5%;' >
2016.10.26 @関モバ
< /div >
< img id='top_image' src='img/pumpkin.png' style='width: 256px; height: 256px; position:absolute; bottom:2%; right:4%;' >< /img >
---
# Who?
### Name: Masui Masanori

### Twitter: [@masanori_msl](https://twitter.com/masanori_msl)

### Blog: [vaguely](http://mslgt.hatenablog.com/)

### GitHub: https://github.com/masanori840816

### App: [SearchWakayamaToilet](https://play.google.com/store/apps/details?id=jp.searchwakayamatoilet)
---
~省略~
  • ページとページを区切るには、「---」を追加します。
  • 縦幅から勝手にページを分けてくれるわけではないため、実際に表示させながら確認が必要となります。

1ページ目はあれこれ場所指定したり、画像を読み込んでいるためあんまりMarkdownっぽくはないですね…。
まぁでも、要素部分だけを書けば良いのは結構楽でもあります。

なお、コードを書く部分は`(バッククォテーションを使用します)

` ` ` xml
body {
    width: 100%;
    height: 100%;
    background-color: #000;
    color: #fff;
    margin: 0px;
    padding: 0;
    overflow: hidden;
}
` ` `

※各'の間のスペースは取り除いてください

この書き方が最初わからず、あれこれ試行錯誤してしまいまいした。

あと、作成中はWebVRと同じくSpringBootの雛形プロジェクトを使って動かしていました。

ローカルで動かすとWebブラウザのセキュリティで引っかからないようにする必要があったことと、GitHubPagesにアップしながら調整するのも不便なので。

とりあえず各ページの内容をザザッと書いてしまって、細かい見た目の調整はWebブラウザの開発ツールを使うと便利かと思います。

読み込んだ時点で表示しているページのソースしかいじれないとはいえ、リアルタイムで調整していけるのはすごいですね。

しかし、このシンプルさで問題ない場合はかなり便利に資料が作れてしまうのではないでしょうか。

いやぁ、Javascriptってスゴイですねぇ(小並感)

おわりに

会場提供及びスポンサーとなっていただいたはてな様、主催者の皆様、そして温かく発表を見守っていただいた皆様、ありがとうございましたm( )m

次回は(間に合えば)epubの読み込みとかやってみたいかも…?

参考

Remark.js

GitHubPages

three.js + WebVR BoilerplateでモバイルVR

はじめに

前回Spring Bootを使ってHTMLを表示するところまでやりました。

そのままAngularJSで何かサイトを作ろうと思っていたのですが、ちょっと予定を変更して、Webページ上でお手軽?なVRを試してみることにしました。

今回、下記のページを参考にthree.jswebvr-boilerplateを使って試してみました。

なおGitHubに上げてみました。

MobileWebVrDemo - GitHub

準備

まずthree.jswebvr-boilerplatewebvr-polyfill(webvr-boilerplateが依存)から、以下のファイルを取得してSpringBootのプロジェクト/src/main/resources/static/js に追加します。

three.js

  • three.js-master/build/three.min.js
  • three.js-master/examples/js/controls/VRControls.js
  • three.js-master/examples/js/effects/VREffect.js
  • three.js-master/examples/js/loaders/MTLLoader.js
  • three.js-master/examples/js/loaders/OBJLoader.js

webvr-polyfill

  • webvr-polyfill-master/build/webvr-polyfill.js

webvr-boilerplate

  • webvr-boilerplate-master/build/webvr-manager.js

VRに限らず、three.jsはHTMLのcanvas上で動き、HTML自体に記述する処理は画面サイズの指定やJavascriptの読み込みくらいです。

mainpage.html

< !DOCTYPE html >
< html lang="jp" >
    < head >
        < meta charset="UTF-8" / >
        < title>MainPage< /title >
        < meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no"/ >
        < meta name="apple-mobile-web-app-capable" content="yes" / >
        < meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" / >
        < link rel='stylesheet' type="text/css" href="css/mainpage.css" / >
    < /head >
    < body >
        < script src='js/three.min.js' >< /script >
        < script src='js/VRControls.js' >< /script >
        < script src="js/VREffect.js" >< /script >
        < script src="js/webvr-polyfill.js" >< /script >
        < script src="js/webvr-manager.js" >< /script >
        < script src="js/MTLLoader.js" >< /script >
        < script src="js/OBJLoader.js" >< /script >
        < script src="js/mainpage.js" >< /script >
    < /body >
< /html >

mainpage.css

body {
    width: 100%;
    height: 100%;
    background-color: #000;
    color: #fff;
    margin: 0px;
    padding: 0;
    overflow: hidden;
}

3Dモデルを作る

画面に表示したい3Dモデルを作ります。

私はBlenderを使って作成してみました。Booleanを覚えるだけでも簡単なものならそれっぽく作成できて良いですね:)

f:id:mslGt:20161025013647j:plain

ここで注意が必要なのは、モデルを三角形ポリゴンで構成することで、これを忘れて多角形ポリゴンが含まれた状態で読み込んでしまうと、面の一部が描画されなかったりします。

なおBlenderでの三角形化は Edit Modeで Mesh -> Faces -> Triangulate Faces でできます。
blender上では問題なく表示できるだけに、見逃しやすいポイントかと思います。

また今回は、obj形式でExportしておきます(pumpkin.obj、pumpkin.mtlのようなファイルが作成されます)。

作成したファイルはプロジェクトの /src/main/resources/static/models に置きます。

Sceneを作る

いよいよJavascriptのお話です。

まずはUnityと同じように、3Dモデルを表示するためのSceneを作ります。

mainpage.js

(function () {
    var renderer;
    var camera;
    var controls;
    var effect;
    var scene;
    var manager;
    var objectModel;
    var obj;

    this.initialize = function(){
        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(window.devicePixelRatio);

        document.body.appendChild(renderer.domElement);

        // Sceneを作る
        scene = new THREE.Scene();

        // カメラを生成
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
        camera.position.set(0, 0, 0);

        // VR用コントローラを生成
        controls = new THREE.VRControls(camera);

        // VR用エフェクトを生成(2分割の画面を構築する)
        effect = new THREE.VREffect(renderer);
        effect.setSize(window.innerWidth, window.innerHeight);

        // VRマネージャの生成
        manager = new WebVRManager(renderer, effect);

        // 補助線を2000px表示する
        var axis = new THREE.AxisHelper(2000);
        // 中央に合わせるため 補助線の幅/2 分ずらす 
        axis.position.set(0, -1, 0);
        scene.add(axis);

        ~省略~

    };
    this.animate = function(timestamp) {
        // VRコントローラを更新
        controls.update();
        // VRマネージャからシーンを描画
        manager.render(scene, camera, timestamp);

        // ループして実行
        requestAnimationFrame(animate);
    };
    initialize();
    // アニメーションの開始
    animate(performance ? performance.now() : Date.now());
}).call(this);

上記を実行すると、3Dモデルがないので真っ黒ではありますが、もうCardboardのようなVR環境が整ってしまいます。

3Dモデルの読み込み

次は3Dモデルの読み込みです。

this.initializeに以下を追加します。

// まずMaterialの読み込み
var mtlLoader = new THREE.MTLLoader();
mtlLoader.load("models/pumpkin.mtl",
    function(targetMaterial) {
        targetMaterial.preload();

        // 読み込みが完了したらObjectを読み込む.
        var objLoader = new THREE.OBJLoader();
        // 読み込んだMaterialをセット.
        objLoader.setMaterials(targetMaterial);
        objLoader.load("models/pumpkin.obj",
            function (targetObject) {
                // 読み込んだObjectのスケール値などをセット.
                objectModel = targetObject.clone();
                objectModel.scale.set(1, 1, 1);
                objectModel.rotation.set(0, 3, 0);
                objectModel.position.set(0, 0, 0);
                // 読み込んだObjectをObject3Dに追加.
                obj = new THREE.Object3D();
                obj.add(objectModel);
                // sceneに追加
                scene.add(obj);
        });
    });
  • 情報によってはMaterialとObjectをTHREE.OBJMTLLoaderというクラスを使って一度に読み込んでいるものもありましたが、少なくとも私の環境ではエラーになってしまうため、別々に読み込んでいます。
  • Objectを読み込んでいる最中に処理を行ったり、エラー発生時に何か処理を実行したい場合はObjLoader.loadの第三、第四引数にそれぞれメソッドを指定できます。

ライティング

先程追加した3Dモデルを照らすライトを追加します。こちらもthis.initializeに追加します。

// DirectionalLight - 指向性を持った光.
var light = new THREE.DirectionalLight(0xffffff, 0.5);
light.position.set(0, 0, 0);
light.rotation.set(1, 1, 0);
light.castShadow = true;
scene.add(light);

// AmbientLight - 光が当たる・当たらないにかかわらず各3Dモデルの色に影響を与える.
var ambient = new THREE.AmbientLight(0xffffff, 0.5);
ambient.castShadow = true;
ambient.position.set(0,0,0);
scene.add(ambient);

// PointLight - 全方向に向けた豆電球のような?光.
var pointlight = new THREE.PointLight(0xffffff, 0.8);
pointlight.position.set(0.3, 0.4, -3);
pointlight.scale.set(1, 1, 1);
scene.add(pointlight);
  • DirectionalLight、AmbientLightは場所による影響を受けないため、原点においています。
  • PointLightは影を持たないため、castShadowは特に指定していません。

結果

このような結果になります。

f:id:mslGt:20161025013912j:plain

画像はPCで表示しているため右下は最大化ボタンだけが表示されていますが、スマートフォンで表示させるとCardboardのボタンも表示され、押すと画面が二分割されます。

Javascriptのコードをまとめて載せておきます。

mainpage.js

(function () {
    var renderer;
    var camera;
    var controls;
    var effect;
    var scene;
    var manager;
    var objectModel;
    var obj;

    this.initialize = function(){
        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(window.devicePixelRatio);

        document.body.appendChild(renderer.domElement);

        // Sceneを作る
        scene = new THREE.Scene();

        // カメラを生成
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
        camera.position.set(0, 0, 0);

        // VR用コントローラを生成
        controls = new THREE.VRControls(camera);

        // VR用エフェクトを生成(2分割の画面を構築する)
        effect = new THREE.VREffect(renderer);
        effect.setSize(window.innerWidth, window.innerHeight);

        // VRマネージャの生成
        manager = new WebVRManager(renderer, effect);

        // 補助線を2000px表示する
        var axis = new THREE.AxisHelper(2000);
        // 中央に合わせるため 補助線の幅/2 分ずらす 
        axis.position.set(0, -1, 0);
        scene.add(axis);

        // まずMaterialの読み込み
    var mtlLoader = new THREE.MTLLoader();
    mtlLoader.load("models/pumpkin.mtl",
        function(targetMaterial) {
            targetMaterial.preload();

            // 読み込みが完了したらObjectを読み込む.
            var objLoader = new THREE.OBJLoader();
            // 読み込んだMaterialをセット.
            objLoader.setMaterials(targetMaterial);
            objLoader.load("models/pumpkin.obj",
                function (targetObject) {
                    // 読み込んだObjectのスケール値などをセット.
                    objectModel = targetObject.clone();
                    objectModel.scale.set(1, 1, 1);
                    objectModel.rotation.set(0, 3, 0);
                    objectModel.position.set(0, 0, 0);
                    // 読み込んだObjectをObject3Dに追加.
                    obj = new THREE.Object3D();
                    obj.add(objectModel);
                    // sceneに追加
                    scene.add(obj);
            });
        });
        // DirectionalLight - 指向性を持った光.
        var light = new THREE.DirectionalLight(0xffffff, 0.5);
        light.position.set(0, 0, 0);
        light.rotation.set(1, 1, 0);
        light.castShadow = true;
        scene.add(light);

        // AmbientLight - 光が当たる・当たらないにかかわらず各3Dモデルの色に影響を与える.
        var ambient = new THREE.AmbientLight(0xffffff, 0.5);
        ambient.castShadow = true;
        ambient.position.set(0,0,0);
        scene.add(ambient);

        // PointLight - 全方向に向けた豆電球のような?光.
        var pointlight = new THREE.PointLight(0xffffff, 0.8);
        pointlight.position.set(0.3, 0.4, -3);
        pointlight.scale.set(1, 1, 1);
        scene.add(pointlight);

    };
    this.animate = function(timestamp) {
        // VRコントローラを更新
        controls.update();
        // VRマネージャからシーンを描画
        manager.render(scene, camera, timestamp);

        // ループして実行
        requestAnimationFrame(animate);
    };
    initialize();
    // アニメーションの開始
    animate(performance ? performance.now() : Date.now());
}).call(this);

おわりに

割と簡単にそれっぽいものが表示できてしまうわけですが、いくつか問題点があります。

  1. ポートレートモードでCardboardボタンを押して、そのあとランドスケープモードに表示切り替えすると、カメラが90度回転してしまう
  2. Rotationの値が角度ではない
  3. 描画速度がモバイルVRの要求に満たない

1,は画面の向きによってカメラを回転させれば対応できそうですし、2.もまぁなんとか。

問題は3.で、モバイルVRでは60FPSを実現させる必要があると言われますが、(計測はしていないものの)現状でもそんな速度はでていないのではないかと思われます。
画面を回転させていると引っかかりがあるのも気になるところです。

まぁブラウザアプリ上で動かしているだけに困難ではあります。

が、Webで実現できる最大のメリットは、ただURLにアクセスするだけで見られる、ということだと思いますので、VIVEやRiftなどの専用端末やアプリだけでなく、Web上で表示するVRの技術も進歩していくと面白そうだな、と思っています。

参考

SpringBoot + IntelliJ+ Gradle(とりあえずHTML表示するところまで)

はじめに

IntelliJ IDEA Ultimate Editionのパーソナルアカウント購入しました。
これは先日行われたScala関西 Summit 2016に参加した影響もあったりなかったりします。

それは良いとして。

ここ最近のJava(特にver.8以降)の仕様に触れてみたい、ということもあってSpringBootに入門してみることにしました。
SpringではSpring Tool Suite(STS)というEclipseベースの開発ツールが用意されています。

素直にそれを使えば良いのですが、できればAndroid Studioと操作感を揃えたいと思ったため、IntelliJ(当時はCommunity Edition)を使ってみたらハマったためここに書き残して置くことにします。

Projectを作る

SpringBootのプロジェクトの雛形は、Spring Initializrで作ることができます。
IntelliJのUltimate Editionだと File/New/Project からも同じ機能が利用できるため、そちらから作成してもOKです。

ここでは「Gradle Project」を選択し、SpringBootのバージョンは「1.4.1」としました。
またDependenciesとして「Web」を指定しています。

上記Webサイトで作成した場合はZipファイルのダウンロードが始まるので、適当なところに展開します。

Gradleの編集

プロジェクト直下にあるbuild.gradleを以下のように変更します(SpringLoadedとThymeleafについては後述)。

build.gradle

buildscript {
    ext {
        springBootVersion = '1.4.1.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("org.springframework:springloaded:1.2.5.RELEASE")
    }
}
apply plugin: 'java'
apply plugin: 'spring-boot'

jar {
    baseName = 'springboottest'
    version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}
dependencies {
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

ビルドの設定

次はGradleとSpringBootのビルド設定です。

右上のメニュー -> EditConfigrations -> Run/Debug Configrationsを開きます。 f:id:mslGt:20161015101358j:plain

Gradle

左上の+ボタンをクリックしてGradleを選択します。

以下のように入力します。 f:id:mslGt:20161015101302j:plain

SpringBoot

左上の+ボタンをクリックしてSpringBootを選択します。

以下のように入力します。 f:id:mslGt:20161015101313j:plain

VM optionsで「-Dserver.port=8099」と指定することで、実行時に「localhost:8080」ではなく「localhost:8099」に変更できます。
他のサーバーが8080ポートを使っている場合などに便利です。

Controllerの追加

特定のURLにアクセスされた場合に指定のページを表示する、ルーティングを行うControllerクラスを追加します。

MainController.java

package jp.masanori;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MainController {
    @RequestMapping("/home")
    public String mainpage(Model model){
        return "mainpage";
    }
}
  • 「@Controller」を付けることでControllerクラスとして振る舞うことができるようになります。
  • 「@RequestMapping("/home")」によって、「localhost:8099/home」にアクセスしたときにmainpage.htmlが表示されるようになります。

ページを追加する

Thymeleafを使ってHTMLを表示してみます。

build.gradle

~省略~
dependencies {
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

templates

Controllerで指定したmainpage.htmlを、src/main/resources/templates に追加します。

mainpage.html

< !DOCTYPE html >
< html lang="jp" >
    < head >
        < meta charset="UTF-8" / >
        < title >MainPage< / title >
    < / head >
    < body >
        < div >世界さん、チーッス< / div >
    < / body >
< / html >
  • File -> New -> HTML からHTMLファイルを作成するとmetaタグが閉じていない状態で作成されますが、実行時にエラーになるので忘れずに閉じておきます。
  • 今回は特に何もしていませんが、Controllerクラスから値を渡してページ上に反映させる、ということも可能です。

static

ルーティングやControllerからデータを渡す必要がない場合は、src/main/resources/static に置きます。

staticディレクトリ直下が「localhost:8099」として扱われるため、subpage.htmlを置いた場合、「localhost:8099/subpage.html」にアクセスするとページが表示されます。

また、JavascriptCSSもここに置くようです。

SpringLoaded

SpringBootのPlayボタンを押すとWebブラウザからアクセスできるようになるわけですが、何かコードを変更するたびに再起動するのは面倒なもの。

ということで、SplingLoadedを使ってみることにしました。

build.gradle

buildscript {
    ext {
        springBootVersion = '1.4.1.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("org.springframework:springloaded:1.2.5.RELEASE")
    }
}
~省略~

が、実際に動かしてみても変更が反映されないorz

どうやらIntelliJ上で動作させると、SplingLoadedが効かないようです。

Typescript

AngularJS2.0を使ってみたい、ということでまずはTypescriptを使ってみます。

Node.jsをインストールしておきます。

そのあとFile -> Settings -> Languages & Frameworks -> Typescript -> Common -> Node interpreter にnode.exeをセットします。

src/main/resources/static/js にmainpage.tsを追加します。
で、コードを書いていきます。

は良いのですが、TypescriptはそのままHTMLから呼び出せるわけではなく、Javascriptへのトランスパイルが必要となります。

その方法を調べようとしたところ...

おや?

f:id:mslGt:20161015101414j:plain

ということで、特に何もしなくてもトランスパイルは自動で実行されました。

HTMLから呼び出すには、「js/mainpage.js」を指定すればOKです。

おわりに

とりあえずページを表示することはできました。

IntelliJ IDEAのUltimate Editionではちょっと戸惑ってしまうくらい機能があるため、引き続きその力を借りつつあれこれやってみようかと思います。

参考

Tymeleaf

Gsonとか(和歌山トイレマップで遊んでみる 10)

はじめに

ちょこちょこいじっていたつもりではありましたが、ブログとしては結構間が空いていましたね。

以前Spreadsheetに入力していた値をJsonファイルとして出力できるようにしましたが、今回はこれをAndroidアプリ側で取り込んでみることにします。

github.com

Gsonを使う

Jsonファイルを出力する時と同じく、Gsonを使ってファイルからデータを読み込みます。

インストール

app/build.gradle

~省略~
dependencies {
~省略~
    compile "com.google.code.gson:gson:2.7"
~省略~
}
~省略~

何の事はない、dependenciesにgsonの項目を追加しただけですね。
簡単にできるのは素晴らしい。

データを格納するクラスを作る

Jsonファイルを作った時と同じく、Jsonから取得したデータを格納するクラスを作成します。

ToiletInfoClass.java

import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;

public class ToiletInfoClass {
    @SerializedName("toiletInfo")
    private ArrayList toiletInfoList;

    public ArrayList getToiletInfoList(){
        return toiletInfoList;
    }
    public void setToiletInfoList(ArrayList newValue){
        toiletInfoList = newValue;
    }
    public int getInfoCount(){
        if(toiletInfoList == null){
            return 0;
        }
        return toiletInfoList.size();
    }
    public class ToiletInfo{
        @SerializedName("toiletName")
        public String toiletName;

        @SerializedName("district")
        public String district;

        @SerializedName("municipality")
        public String municipality;

        @SerializedName("address")
        public String address;

        @SerializedName("latitude")
        public double latitude;

        @SerializedName("longitude")
        public double longitude;

        @SerializedName("availableTime")
        public String availableTime;

        @SerializedName("hasMultiPurposeToilet")
        public boolean hasMultiPurposeToilet;
    }
}

ここで以前と異なっているのは、Jsonファイルのデータが以下のように配列で格納されている、ということです。

toiletdata.json

  "toiletInfo": [{
        "toiletName":"友ヶ島野奈浦公衆トイレ"
        ,"district":"和歌山県"
        ,"municipality":"和歌山市"
        ,"address":"和歌山市加太笘ヶ沖島2673-3"
        ,"latitude":34.282891
        ,"longitude":135.008677
        ,"availableTime":"終日"
        ,"hasMultiPurposeToilet":true
    }
    ,{
        "toiletName":"友ヶ島南垂水公衆トイレ"
        ,"district":"和歌山県"
        ,"municipality":"和歌山市"
        ,"address":"和歌山市加太苫ケ沖島2673-1"
        ,"latitude":34.28255
        ,"longitude":135.013597
        ,"availableTime":"終日"
        ,"hasMultiPurposeToilet":false
    }
    ,{
    ~省略~

そのためToiletInfoClass自体は配列(ArrayList)のみを持ち、その中身は内部クラスであるToiletInfoが入っている、という状態にしました。
なお、@SerializedNameを使うことで「@SerializedName("toiletInfo")」のようにJsonファイルの項目名を指定できます。

データの読み込み

Jsonデータの読み込み

それではJsonをロードして先ほどのクラスに値をセットします。
Jsonの置き場所は以前のCSVと同じく、app\src\main\assetsに置いています。

ToiletInfoAccesser.java

~省略~
    AssetManager assetManager = currentActivity.getResources().getAssets();
    if(assetManager == null){
        // TODO: Assetの取得に失敗した時の処理.
    }
    else{
        try {
            // Jsonの読み込み.
            InputStream inputStream = assetManager.open("toiletdata.json");
            JsonReader jsonReader = new JsonReader(new InputStreamReader(inputStream));

            // JsonデータをToiletInfoClassとして取得.
            ToiletInfoClass jsonToiletInfoClass = new Gson().fromJson(jsonReader, ToiletInfoClass.class);

            // 取得したデータをDBに挿入.
            toiletInfoModel.insertInfo(sqlite, jsonToiletInfoClass);

            jsonReader.close();
            inputStream.close();

            // DBへのデータ挿入後、データを検索してマーカー設置.
            ToiletInfoClass toiletInfoClass = toiletInfoModel.search(sqlite);
            subscriber.onNext(toiletInfoClass.getToiletInfoList());
            subscriber.onCompleted();
        } catch (IOException ex) {
            subscriber.onError(ex);
        }
    }
~省略~

かなりシンプルですね。
以前CSVを読み込んでいた時と比較するとえらい違いが...。

データの挿入

なお、DBへのデータ挿入はこのような感じに。
以前は登録済みのデータはそのまま無視していたのですが、今後データを修正する必要があった場合などを考えて上書き処理をすることとしました。

ToiletInfoModel.java

~省略~
    public void  insertInfo(SQLiteDatabase db, ToiletInfoClass toiletInfoClass) {
        Cursor cursorMaxId = db.query("toiletinfo"
                , new String[]{"MAX(id)"}
                , null
                , null
                , null, null, null);
        cursorMaxId.moveToFirst();
        int maxId = cursorMaxId.getInt(cursorMaxId.getColumnIndex("MAX(id)"));
        cursorMaxId.close();

        for(ToiletInfoClass.ToiletInfo toiletInfo: toiletInfoClass.getToiletInfoList()){
            Cursor cursorExistence = db.query("toiletinfo"
                    , new String[]{"id"}
                    , "toiletname = ? AND address = ?"
                    , new String[]{toiletInfo.toiletName, toiletInfo.address}
                    , null, null, null);
            cursorExistence.moveToFirst();

            if(cursorExistence.getCount() > 0){
                // 既存データをアップデート.
                update(db, toiletInfo, cursorExistence.getInt(cursorExistence.getColumnIndex("id")));
            }
            else{
                // 新規追加.
                maxId++;
                insert(db, toiletInfo, maxId);
            }
            cursorExistence.close();
        }
    }
~省略~
    private void insert(SQLiteDatabase db, ToiletInfoClass.ToiletInfo toiletInfo, int newId){
        // Transactionの開始.
        db.beginTransaction();

        // Transactionの開始・終了は呼び出し元で実行.
        ContentValues contentValues = new ContentValues();

        contentValues.put("id", newId);
        contentValues.put("toiletname", toiletInfo.toiletName);
        contentValues.put("district", toiletInfo.district);
        contentValues.put("municipality", toiletInfo.municipality);
        contentValues.put("address", toiletInfo.address);
        contentValues.put("latitude", toiletInfo.latitude);
        contentValues.put("longitude", toiletInfo.longitude);
        contentValues.put("availabletime", toiletInfo.availableTime);
        contentValues.put("hasMultiPurposeToilet", toiletInfo.hasMultiPurposeToilet);
        db.insert("toiletinfo", null, contentValues);

        // CommitしてTransactionを終了.

        db.setTransactionSuccessful();
        db.endTransaction();
    }
    private void update(SQLiteDatabase db, ToiletInfoClass.ToiletInfo toiletInfo, int targetId){
        // Transactionの開始.
        db.beginTransaction();

        // Transactionの開始・終了は呼び出し元で実行.
        ContentValues contentValues = new ContentValues();

        contentValues.put("id", targetId);
        contentValues.put("toiletname", toiletInfo.toiletName);
        contentValues.put("district", toiletInfo.district);
        contentValues.put("municipality", toiletInfo.municipality);
        contentValues.put("address", toiletInfo.address);
        contentValues.put("latitude", toiletInfo.latitude);
        contentValues.put("longitude", toiletInfo.longitude);
        contentValues.put("availabletime", toiletInfo.availableTime);
        contentValues.put("hasMultiPurposeToilet", toiletInfo.hasMultiPurposeToilet);

        db.update("toiletinfo", contentValues, "id = ?", new String[]{String.valueOf(targetId)});
        // CommitしてTransactionを終了.

        db.setTransactionSuccessful();
        db.endTransaction();
    }
}

以前に引き続きSQLiteを使用していますが、せっかく勉強会に参加したこともありRealmも試してみたいところ。

おわりに

コードがシンプルになるとスッキリ感も加わって気持ちが良いものですね。
ただ、取り組んでいる内容はずっとリファクタリングだけなので、そろそろ機能追加をしたいところ。

とりあえずサジェスト機能を付けるかなぁ。

今はアプリ内に入れているJsonも、外部に出してインターネット経由でデータを更新するようにもしたいのですが。

参考

Gson