Kekeの日記

エンジニア、読書なんでも

Cloud FirestoreとRxJSを使ってLeafletでリアクティブな座席表をつくる

f:id:bobchan1915:20181019215000p:plain

本記事

本記事ではCloud FirestoreとLeafletを掛け合わせてリアルタイムな座席表を作ります。

リアクティブな座席表とは、単に「誰の席か」だけではなくて「どこに誰が座っているか」を可視化できるものです。

ちょうどドキュメント型データベースを使ったことがなかったので本記事で学べなれたらいいなと思います。

注意点としては

  • Pug
  • Scss

を使用している点です。

基本的なこと

Leaflet描画エリアを指定する

まず、以下のようにdiv要素を指定します。

この領域をLeaflet描画エリアと定義します。

body
    #map(style="width: 80%; height: 600px;")

boundsを指定する

次に以下のようにmapを初期化してboundsを指定します。

var map = L.map('map', {crs: L.CRS.Simple});
var bounds = [[0,0], [480,640]];

bounds実際のMap描画エリアです。

先ほどのdiv要素との対応関係は以下のようになります。

f:id:bobchan1915:20181019151848p:plain

ここの灰色の部分がLeafletの描画エリアなので、スクロールやズームをしてもはみ出すことはありませんので、divのスタイルをいじるだけで簡単に大きさを調整できることがわかります。

f:id:bobchan1915:20181019152122p:plain

もちろんdiv要素がboundより小さいとクロップされてしまいます。

f:id:bobchan1915:20181019152216p:plain

また、どうやらboundsの指定するものは画像の解像度よりは小さくはできないようになっています。

座標系を確認する

以下のようにboundsを設定したとします。

var bounds = [[0,0], [400,640]]; 

そして、以下のように円形を座標を指定して追加したとします。

L.circle([100, 320], {color: 'green',radius: 35}).addTo(map)

すると以下のように表示され、座標系が図中のように計算されていることがわかります。

f:id:bobchan1915:20181019152905p:plain

座標系は

  • boundsの左下が原点
  • 指定方法は上下がxで左右がy座標と思えばいい

です。

実践

席を画像の通り配置する

以下のようにして大量に配置していきます。

f:id:bobchan1915:20181019163337p:plain

コードは以下のようになっています。

        L.circle([195, 240], {color: 'green',radius: 15}).addTo(map)
        L.circle([195, 286], {color: 'green',radius: 15}).addTo(map)

        L.circle([252, 240], {color: 'green',radius: 15}).addTo(map)
        L.circle([252, 286], {color: 'green',radius: 15}).addTo(map)

        L.circle([254, 330], {color: 'green',radius: 15}).addTo(map)
        L.circle([254, 376], {color: 'green',radius: 15}).addTo(map)

        L.circle([195, 333], {color: 'green',radius: 15}).addTo(map)
        L.circle([195, 378], {color: 'green',radius: 15}).addTo(map)

        L.circle([203, 431], {color: 'green',radius: 15}).addTo(map)
        L.circle([164, 431], {color: 'green',radius: 15}).addTo(map)

        L.circle([247, 432], {color: 'green',radius: 15}).addTo(map)
        L.circle([286, 432], {color: 'green',radius: 15}).addTo(map)

        L.circle([122, 408], {color: 'green',radius: 15}).addTo(map)
        L.circle([122, 454], {color: 'green',radius: 15}).addTo(map)

        L.circle([228, 470], {color: 'green',radius: 15}).addTo(map)
        L.circle([228, 514], {color: 'green',radius: 15}).addTo(map)

        L.circle([310, 366], {color: 'green',radius: 15}).addTo(map)
        L.circle([310, 412], {color: 'green',radius: 15}).addTo(map)

        L.circle([310, 286], {color: 'green',radius: 15}).addTo(map)
        L.circle([310, 330], {color: 'green',radius: 15}).addTo(map)

        L.circle([340, 247], {color: 'green',radius: 15}).addTo(map)
        L.circle([303, 247], {color: 'green',radius: 15}).addTo(map)

これで座席表ができました。

NodeのExpressサーバーに乗せる

以下のようなExpressサーバーを構築します。

const express = require('express');
const app = express();
var path    = require("path");

app.get('/', (req, res) => res.send('Hello World!'));

app.get('/floormap', (req, res) => {
    res.sendFile(path.join(__dirname+'/index.html'));
});

app.listen(3000, () => console.log('Example express app listening on port 3000!'));

そして起動します。

Cloud Firestoreを初期化する

Firebase ConsoleでCloud Firestoreデータベースを初期化すると以下のように指定します。

const firebase = require("firebase");
require("firebase/firestore");

var config = {
            apiKey: "hoge",
            authDomain: "hoge.firebaseapp.com",
            databaseURL: "https://hoge.firebaseio.com",
            projectId: "hoge",
            storageBucket: "hoge.appspot.com",
            messagingSenderId: "hoge"
 };
 firebase.initializeApp(config);

var firestore = firebase.firestore();

これでFirestoreを使うことはできます。

Firestoreのデータベース設計をする

以下のように設計します。

  • Firestore Root
    • Stores(Storeについてのコレクション)
      • Store(店についてのコレクション)
        • Floors(Floorについてコレクション)
          • Floor(Floorドキュメント)
            • Seats(Seatについてのコレクション)
              - Seat(Seatドキュメント)
              

Floorsコレクションにしたのは、いくつか階をもっているものだったりするからです。

そして以下のようにSeatドキュメントはフィールドで構成されています。

フィールド 内容
cordinates 座標
isOcupied 席に座られているか

そして以下のように取得します。 ここでは、Cloud Firestoreの初期設定については説明を割愛させていただきます。

Firestoreからリアルタイムで取得して描画する

最初はこのように取得します。本記事では店番号11階についての可視化をします。

firestore.collection("stores/store-1/floors/floor-1/seats").get()
            .then(docs=>{
                docs.forEach(doc=>{
                    seats.next(doc)
                })
            })

また、リアルタイムで取得したものについては以下のようにしています。

firestore.collection("stores/store-1/floors/floor-1/seats").onSnapshot()
            .then(docs=>{
                docs.forEach(doc=>{
                    seats.next(doc)
                })
            })

そして、これらに対してすべての円を描いて、クリックイベントを購読します。

fromEvent(circle, 'click')
                .subscribe(()=>{
                    circle.setStyle({color: 'red'})
                    firestore.doc(`stores/store-1/floors/floor-1/seats/${doc.id}`)
                        .set({
                            cordinates: [circle.getLatLng().lat, circle.getLatLng().lng],
                            isOccupied: true
                        }).then(()=>{
                            console.log("changed")
                    })
                })

isOccupiedfalseならば、席が空いているということで色をgreenにしてtrueならばredにするようにしています。 ここでは、デモ用のために

fromEvent(circle, 'click')
                .subscribe(()=>{
                    console.log("changing color ...")
                    circle.setStyle({color: 'red'})
                    firestore.doc(`stores/store-1/floors/floor-1/seats/${doc.id}`)
                        .set({
                            cordinates: [circle.getLatLng().lat, circle.getLatLng().lng],
                            isOccupied: !data.isOccupied
                        }).then(()=>{
                            console.log("changed color")
                    })
                })

のようにします。

するとこのようになります。

f:id:bobchan1915:20181019232442g:plain

席を追加できるようにする

以下のようにdiv要素を追加します。

<button class="addButton">Add seat</button>

そして、以下のようにcircleを追加します。

fromEvent(addButton, 'click')
            .subscribe(()=>{
                let cordinates = [60, 60]
                let circle = L.circle(cordinates, {color: 'gray'}).addTo(leafletMap)
                fromEvent(circle, 'mousedown')
                    .subscribe(()=>{
                        fromEvent(leafletMap, 'mousemove')
                            .subscribe((e)=> circle.setLatLng(e.latlng))
                        firestore.doc(`stores/store-1/floors/floor-1/seats/seat-${circles.length}`)
                            .set({
                                cordinates: [circle.getLatLng().lat, circle.getLatLng().lng],
                                isOccupied: false
                            }).then(()=>{
                            })
                    })

すると追加ができるようになりました。

f:id:bobchan1915:20181020035928g:plain

席のメタデータを付与する

席は取得できたのですが席自体に

  • いつ座ったのか
  • 誰が座ったのか

などの席のメタデータを付与したい時があります。

そのようなときに新しく

Usersコレクションをルートから作成し、Seatのフィールドに

  • userID: Usersコレクション内のUserドキュメントID

を追加して指定しようと思います。今回はすべて同じ人であるとして実装します。

ここでUserドキュメントは決め打ちで

  • Username
  • Profile画像URL

にします。Profile画像は最近知ったこのキャラにします。

http://st.cdjapan.co.jp/pictures/l/04/35/NEOGDS-264259.jpg

このuserIDを取得してPopupに出してみようと思います。

例えば、以下のようにPopupを実装することができます。

circle.bindTooltip("HTML or String", {permanent: true, direction: 'center'}).openTooltip();

ここにHTMLを代入すると返してくれます。

以下のようなヘルパー関数を定義してHTMLを生成します。

function getTool(seat, user) {
     ...       
}

また、これまでcircleを作成していたところにpopUpを挟むような実装をします。

そして、mouseoverしたときに表示するようにします。

すると、以下のようにモックを作ることができます。

f:id:bobchan1915:20181020062131p:plain

あとは何かしらなAPIでCloud Firestore内を書き換えたり、直接Webアプリで書き換えてもいいです。

まとめ

RxSwiftばかり使っていたのでフロントエンドでRxJSを使えてよかった。

まだまだ、htmlとcssで凝れる部分はいくらでもあるので、どんどんやっていきたいと思いました。