vaguely

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

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

【Unity】【LeapMotion】【Windows】ジェスチャー検出 その1

はじめに

諸事情により、Leap Motionを購入しました。
まぁ大した理由もないのですが、前からモーションセンサーは購入したいと思っていたので。

Leap Motionはハードウェアは据え置きでソフトウェアの進化のみでトラッキングの精度が上がっていくという稀有な存在だと思います(あまり自由にお金を使えない勢としてはありがたいことこの上ない)。

今回は、Orion BetaUnity Core Assets ver.4.1.2を使ってトラッキングの開始・停止や手を右から左に(またはその逆)素早く動かすSwipe操作などを検出してみたことについて書き残すことにします。

https://github.com/masanori840816/LeapMotionCtrl

ラッキングの開始・停止

まずは手のトラッキングの開始・停止を検出するところから。

MotionCtrl.cs

using Leap;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class MotionCtrl : MonoBehaviour {
    public List < GameObject > motionCtrlObjectList;

    private List < IMotionCallback > motionCtrlList;
    private int motionCtrlCount;

    private Controller leapCtrl;
    private Frame leapFrame;
    private List < Hand > handList;

    private bool isTracking;
    private bool isGrabbed;
    private bool lastTrackingStatus;
    private bool lastGrabbedStatus;
    private Vector3 convertedValue = new Vector3(0, 0, 0);

    private Vector3 handPosition;
    public Vector3 HandPosition
    {
        get { return handPosition; }
    }
    private void Start (){
        // Trackingと手の状態変化を返す対象のClass.
        motionCtrlList = motionCtrlObjectList.Select(ctrlObject => ctrlObject.GetComponent < IMotionCallback > ()).ToList();
        motionCtrlCount = motionCtrlList.Count - 1;

        // App起動時にLeapMotionに接続.
        leapCtrl = new Controller();
        // StartConnectionの実行は必要?
        leapCtrl.StartConnection();

        isTracking = false;
        isGrabbed = false;

        lastTrackingStatus = false;
        lastGrabbedStatus = false;
    }
    
    private void Update () {
        leapFrame = leapCtrl.Frame();
        handList = leapFrame.Hands;

        isTracking = (handList.Count > 0);

        if(isTracking != lastTrackingStatus)
        {
            // Trackingの開始・停止をCallbackで通知.
            if (isTracking)
            {
                for (var i = motionCtrlCount; i >= 0; i--)
                {
                    motionCtrlList[i].OnTrackingStarted();
                }
            }
            else {
                for (var i = motionCtrlCount; i >= 0; i--)
                {
                    motionCtrlList[i].OnTrackingStopped();
                }
                // 手の位置をリセット.
                handPosition.x = 0f;
                handPosition.y = 0f;
                handPosition.z = 0f;
            }
            lastTrackingStatus = isTracking;
        }

        if (! isTracking)
        {
            // Tracking中でなければ手の位置などは取得しない.
            return;
        }
        // Tracking中なら手(Hand0)の平の座標値を取得する.
        handPosition = ConvertToUnityVector3(handList[0].PalmPosition);
~省略~
    }
    private void OnApplicationQuit()
    {
        // Appの終了時は切断する.
        leapCtrl.StopConnection();
        leapCtrl.Dispose();
    }
    private Vector3 ConvertToUnityVector3(Vector originalValue)
    {
        // Leap.VectorからUnityのVector3に変換する.
        convertedValue.x = originalValue.x;
        convertedValue.y = originalValue.y;
        convertedValue.z = originalValue.z;
        return convertedValue;
    }
}
  • ラッキングなどの情報を得るためには、Leap.Controllerを使います。
  • アプリを閉じる前に、Leap.Controller.StopConnection()を実行して切断する必要があり、実行しないとアプリを閉じたタイミングでフリーズしてしまうことも。
  • 上記ではトラッキング中であれば0番目の手の座標値を取得しています。

ラッキングの開始・停止

Leap.Controller.Frame().Handsで取得した手の数が1以上、かつ前フレームと状態が変わった場合にコールバック関数を呼び出す、ということにしています。
が、もうちょっと簡単にできないかなぁとは思っています。

値の変化を検出するのなら、UniRxが使えるのでは?という気もしているのですが...。

コールバック関数

上記のMotionCtrl.csの情報を利用するClassでは、以下のInterfaceを継承して、トラッキングの開始・停止などの通知が受け取れるようにします。

IMotionCallback.cs

public interface IMotionCallback{
    void OnTrackingStarted();
    void OnTrackingStopped();
    void OnHandGrabbed();
    void OnHandReleased();
}

Grab(手を握る)の検出

手を握ったかどうかの検出は、Leap.Controller.Frame().Hands.Fingersが持つ「IsExtended」の値を見ることで判断できます。

MotionCtrl.cs

~省略~
    private void Update(){
    ~省略~
        // 手を握っているか(誤検知を考慮して開いている指の本数が1以下ならTrue).
        isGrabbed = (handList[0].Fingers.Where(finger => finger.IsExtended).Count() <= 1);
        if(isGrabbed != lastGrabbedStatus)
        {
            if (isGrabbed)
            {
                for (var i = motionCtrlCount; i >= 0; i--)
                {
                    motionCtrlList[i].OnHandGrabbed();
                }
            }
            else
            {
                for (var i = motionCtrlCount; i >= 0; i--)
                {
                    motionCtrlList[i].OnHandReleased();
                }
            }
            lastGrabbedStatus = isGrabbed;
        }
    }
~省略~

Swipeの検出

手を右から左、または上から下(+それぞれ逆方向)に素早く動かす、Swipe操作を検出します。

実はLeapMotionのVersion2.Xの時は、標準でジェスチャー検出が可能だったのですが、今回使用するVersion4.XではDeprecatedになっており、また今後実装される見込みも薄そうです。

ということで、自分でなんとかしてみることにします。

考え方

ある一定の速度で、ある一定の方向に手が動いているかを調べます。
Update関数で、前のFrameで取得した手の座標値と今の座標値とを比較して、その差分を取得します。

○                   ○                    ○                   ○
L前Frameの手の位置との差分 」L前Frameの手の位置との差分 」L前Frameの手の位置との差分 」

で、この値の合計値が一定以上ならSwipeとして処理を実行する、と。
ただし、斜めに動かした場合などは判別しづらいので、縦横逆方向に大きく動いている場合は無視することとします。

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class MotionSwipeCtrl : MonoBehaviour, IMotionCallback
{
    public GameObject motionCtrlObject;

    private readonly int MovedLengthCount = 3;
    // 
    private readonly float TargetDirectionPerFrameMoveLength = 25.0f;
    private readonly float OppositeDirectionPerFrameMoveLength = 5.0f;
    private readonly Vector2 DefaultMoveLength = new Vector2(0.0f, 0.0f);

    private MotionCtrl motionCtrl;
    private bool isCtrlDisabled;
    // X, Y方向のSwipeのみを取るためVector2を使う.
    private List movedLengthList;
    private float movedTotalLengthX;
    private float movedTotalLengthY;

    private Vector3 lastHandPosition;
    private Vector2 newMovedLength;

    private float validMinMoveLength;
    private float invalidMaxOppositeLength;

    private bool isGestureExecuted;

    public void OnTrackingStarted()
    {
        lastHandPosition = motionCtrl.HandPosition;
        isCtrlDisabled = false;
        isGestureExecuted = false;
    }
    public void OnTrackingStopped()
    {
        isCtrlDisabled = true;
        Reset();
    }
    public void OnHandGrabbed()
    {
        isCtrlDisabled = true;
    }
    public void OnHandReleased()
    {
        isCtrlDisabled = false;
    }
    private void Start(){
        motionCtrl = motionCtrlObject.GetComponent();
        movedLengthList = new List();
        validMinMoveLength = 0f;
        invalidMaxOppositeLength = 0f;
        // 初期化.
        for (var i = MovedLengthCount; i >= 0; i--)
        {
            movedLengthList.Add(DefaultMoveLength);

            validMinMoveLength += TargetDirectionPerFrameMoveLength;
            invalidMaxOppositeLength += OppositeDirectionPerFrameMoveLength;
        }
        isCtrlDisabled = true;
        isGestureExecuted = false;
    }
    private void Update () {
        if (isCtrlDisabled)
        {
            return;
        }
        // 前Frameからの手の座標値の差分を算出.
        newMovedLength.x = motionCtrl.HandPosition.x - lastHandPosition.x;
        newMovedLength.y = motionCtrl.HandPosition.y - lastHandPosition.y;

        if (! isGestureExecuted)
        {
            // 前数Frame分の移動距離を算出.
            movedTotalLengthX = Math.Abs(movedLengthList.Select(movedLength => movedLength.x).Sum() + newMovedLength.x);
            movedTotalLengthY = Math.Abs(movedLengthList.Select(movedLength => movedLength.y).Sum() + newMovedLength.y);

            if (movedTotalLengthX >= movedTotalLengthY)
            {
                if (movedTotalLengthX >= validMinMoveLength
                    && movedTotalLengthY <= invalidMaxOppositeLength)
                {
                    
                    // TODO: 横方向にSwipe操作した場合の処理.
                    
                    isGestureExecuted = true;
                    StopDetectingGesture();
                }
            }
            else
            {
                if (movedTotalLengthY >= validMinMoveLength
                    && movedTotalLengthX <= invalidMaxOppositeLength)
                {
                    
                    // TODO: 縦方向にSwipe操作した場合の処理.
                    
                    isGestureExecuted = true;
                    StopDetectingGesture();
                }
            }
        }
        
        // 値を更新して今Frameの座標値の差分を保存する.
        for (var i = MovedLengthCount; i >= 1; i--)
        {
            movedLengthList[i] = movedLengthList[i - 1];
        }
        movedLengthList[0] = newMovedLength;
        // 今Frameの座標値を保存する.
        lastHandPosition = motionCtrl.HandPosition;
    }
    private void Reset()
    {
        for (var i = MovedLengthCount; i >= 0; i--)
        {
            movedLengthList[i] = DefaultMoveLength;
        }
        isGestureExecuted = false;
    }
    private IEnumerator StopDetectingGesture()
    {
        // Swipe実行後は一定時間Swipe操作を無効にする.
        yield return new WaitForSeconds(0.5f);
        Reset();
    }
}
  • 手の座標値の差分を取得するFrame数やその合計値などは使用状況などによって調整してください。
  • 手を素早く動かし続けていると、Swipe処理が何度も実行されてしまうため、フラグで一定時間Swipe処理が実行されないようにしています。

おわりに

とりあえずそれっぽく動くようにはなりましたが、特にSwipeのしきい値などはまだ調整が必要そうです。

また、処理の内容の割にコードが長すぎる気がするので、ここももう少し工夫したいところです。

次回ももう少しLeapMotionであれこれやってみた話になる…予定です。

参考

Leap Motion

Linq

【Kotlin】【Windows】JavaFX + Apache POI + GsonでExcelからJsonファイルを作る2 (1の修正)

はじめに

前回で一応Excelからデータを取得してJsonファイルに書き出す、ということができるようになりました。
その時以下の内容で出力するようにしていました。

{
    data1:{
        "element1": "element1"
        ,"element2": "element2"
    }
    , data2{
        "element1": "element1"
        ,"element2": "element2"
    }
~省略~

しかし、Androidのアプリで同じくGsonを使ってこのJsonを読み込もうとしたときに、以下のような配列として出力されている方が良い、ということがわかってきました。

{
    data:[
        {
            "element1": "element1"
            ,"element2": "element2"
        }
        , {
            "element1": "element1"
            ,"element2": "element2"
        }
~省略~
    ]
}

ということで今回は、Excelから読み込んだデータを配列として出力する方法についてです。

データをセットするクラスを使用する

配列を作るには、専用のクラスを作成してその配列をJsonに変換 → 出力 という方法が考えられます。

ToiletInfo.kt

class ToiletInfo {
    var toiletName = ""
        get set
    var district = ""
        get set
    var municipality = ""
        get set
    var address = ""
        get set
    var latitude: Double = 0.0
        get set
    var longitude: Double = 0.0
        get set
    var availableTime = ""
        get set
    var hasMultiPurposeToilet: Boolean = false
        get set
}

このクラスを使って、Excelから読み込んだデータをセットした配列を作成します。

SpreadsheetAccesser.kt

class SpreadsheetAccesser {

    lateinit var ToiletInfoList: ArrayList< ToiletInfo >
        get

~省略~
    fun loadFile(targetFilePath: String, targetSheetName: String){
        val fileStream = FileInputStream(targetFilePath)
        val currentWorkbook = WorkbookFactory.create(fileStream)

        if(currentWorkbook == null){
            return
        }
        val targetSheet: Sheet? = currentWorkbook.getSheet(targetSheetName)
        if(targetSheet == null){
            return
        }
        val rowCount = targetSheet.physicalNumberOfRows - 1
        if(rowCount < 0){
            return
        }
        // 最初の行から列数を取得する.
        if(targetSheet.getRow(0).physicalNumberOfCells >= 9) {
            ToiletInfoList = ArrayList()

            // 最初の行は項目名なのでスキップ.
            for (i in 1..rowCount) {
                val toiletInfo = ToiletInfo()
                // 1. toiletName, 2. district, 3. municipality, 4. address,
                // 5. latitude, 6. longitude, 7.availableTime, 8.hasMultiPurposeToilet.
                toiletInfo.toiletName = targetSheet.getRow(i).getCell(1).stringCellValue
                toiletInfo.district = targetSheet.getRow(i).getCell(2).stringCellValue
                toiletInfo.municipality = targetSheet.getRow(i).getCell(3).stringCellValue
                toiletInfo.address = targetSheet.getRow(i).getCell(4).stringCellValue
                toiletInfo.latitude = targetSheet.getRow(i).getCell(5).numericCellValue
                toiletInfo.longitude = targetSheet.getRow(i).getCell(6).numericCellValue
                var availableTime: String? = targetSheet.getRow(i).getCell(7)?.stringCellValue
                availableTime = availableTime?: ""
                toiletInfo.availableTime = availableTime
                toiletInfo.hasMultiPurposeToilet = targetSheet.getRow(i).getCell(8).booleanCellValue

                ToiletInfoList.add(toiletInfo)
            }
        }
        currentWorkbook.close()
        fileStream.close()
    }
}

あとはJsonに変換し、出力します。

JsonFileCreater.kt

class JsonFileCreater {
    fun createFile(toiletInfoList: ArrayList< ToiletInfo >, fileTitle: String){
        val stringWriter = StringWriter()
        val jsonWriter = JsonWriter(BufferedWriter(stringWriter))
        // 出力したJsonファイルで適切にインデントが入る...はずだが今回は効いていないようです.
        jsonWriter.setIndent("  ")
        
        jsonWriter.beginObject()

        val gson = Gson()
        
        // 「jsonWriter.name("").value("")」のvalueにはArrayListを入れられないため、Jsonに変換した上でJsonデータとしてセット.
        jsonWriter.name("toiletInfo").jsonValue(gson.toJson(toiletInfoList))

        jsonWriter.endObject()
        jsonWriter.close()
        val createdJson = String(stringWriter.buffer)

        try{
            val splittedTitles = fileTitle.split('.')
            if(splittedTitles.size <= 0){
                return
            }
            val fileWriter = FileWriter(splittedTitles[0] + ".json")
            fileWriter.write(createdJson)
            fileWriter.close()
            
            // TODO: 出力が終わったことを知らせる.
            
        }catch(e: IOException){
            // TODO: 適切なエラー処理.
        }
    }
}

出力した結果は以下のようになります。

{

    "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
        },
~省略~

HashMapを使う(失敗)

上記の方法を取ると確かにやりたいことの実現はできたのですが、読み込むセルの列数やデータ型などが固定されてしまうため少々不便でもあります。

それを解決する方法を調べていたのですが、HashMap使えるんじゃね?と思い至り、試してみることにしました。

 SpreadsheetAccesser.kt

import javafx.collections.FXCollections
import javafx.collections.ObservableList
import org.apache.poi.ss.usermodel.Cell
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.WorkbookFactory
import java.io.FileInputStream
import java.util.*

class SpreadsheetAccesser {

    lateinit var LoadedSheetItemList: ArrayList < ArrayList < HashMap < String, Any > > >
        get

~省略~
    fun loadFile(targetFilePath: String, targetSheetName: String){
        val fileStream = FileInputStream(targetFilePath)
        val currentWorkbook = WorkbookFactory.create(fileStream)

        if(currentWorkbook == null){
            return
        }
        // シート名から対象のシートを取得する.
        val targetSheet: Sheet? = currentWorkbook.getSheet(targetSheetName)
        if(targetSheet == null){
            return
        }
        // セルに何らかの値が含まれる行数の取得.
        val rowCount = targetSheet.physicalNumberOfRows - 1
        if(rowCount < 0){
            return
        }
        // 最初の行から列数を取得する.
        val columnCount = targetSheet.getRow(0).physicalNumberOfCells - 1

        // 最初の行をタイトル行としてArrayListを作成する.
        val ColumnTitleList = ArrayList < String > ()
        for(cell in targetSheet.getRow(0)){
            ColumnTitleList.add(getCellValue(cell).toString())
        }
        // 実際の値が入ったセルの値をセットするArrayListの生成.
        LoadedSheetItemList = ArrayList < ArrayList < HashMap < String, Any > > > ()

        // タイトル行から取得した列数 ✕ (セルに値が含まれる行数 - 1)の値をセットする.
        for(i in 1..rowCount){
            val loadedRowItemList = ArrayList < HashMap < String, Any > > ()

            for(t in 1..columnCount){
                loadedRowItemList.add(hashMapOf < String, Any > (ColumnTitleList[t] to getCellValue(targetSheet.getRow(i).getCell(t))))
            }
            LoadedSheetItemList.add(loadedRowItemList)
        }
        currentWorkbook.close()
        fileStream.close()
    }
    fun getCellValue(targetCell: Cell?): Any{
        var result = ""
        if(targetCell == null){
            return result
        }
        // SpreadsheetにおけるCellの型に合わせて値を返す.
        when(targetCell.cellType){
            Cell.CELL_TYPE_BOOLEAN -> return targetCell.booleanCellValue
            Cell.CELL_TYPE_NUMERIC -> return targetCell.numericCellValue
            Cell.CELL_TYPE_STRING -> return targetCell.stringCellValue
            Cell.CELL_TYPE_FORMULA -> return targetCell.cellFormula
        }
        return result
    }
}

JsonFileCreater.kt

import com.google.gson.Gson
import com.google.gson.stream.JsonWriter
import java.io.BufferedWriter
import java.io.FileWriter
import java.io.IOException
import java.io.StringWriter
import java.util.*

class JsonFileCreater {
    fun createFile(toiletInfoList: ArrayList < ArrayList < HashMap < String, Any > > >, fileTitle: String){
        val stringWriter = StringWriter()
        val jsonWriter = JsonWriter(BufferedWriter(stringWriter))
        jsonWriter.setIndent("  ")
        jsonWriter.beginObject()

        val gson = Gson()
        jsonWriter.name("toiletInfo").jsonValue(gson.toJson(toiletInfoList))

        jsonWriter.endObject()
        jsonWriter.close()
        val createdJson = String(stringWriter.buffer)

        try{
            val splittedTitles = fileTitle.split('.')
            if(splittedTitles.size <= 0){
                return
            }
            val fileWriter = FileWriter(splittedTitles[0] + ".json")
            fileWriter.write(createdJson)
            fileWriter.close()
        }catch(e: IOException){

        }
    }
}

で、これを実行するとどうなるかというと、

{

    "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
        },
~省略~

う~ん、なんか違う(´;ω;`)

何かしら解決方法はありそうですが、今回調べただけではよくわかりませんでしたorz

これについては、今後解決方法が見つかったらこのブログで報告したいと思います。

参考

Gson

Kotlin