本記事
本記事では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
要素との対応関係は以下のようになります。
ここの灰色の部分がLeafletの描画エリアなので、スクロールやズームをしてもはみ出すことはありませんので、div
のスタイルをいじるだけで簡単に大きさを調整できることがわかります。
もちろんdiv
要素がbound
より小さいとクロップされてしまいます。
また、どうやらbounds
の指定するものは画像の解像度よりは小さくはできないようになっています。
座標系を確認する
以下のようにbounds
を設定したとします。
var bounds = [[0,0], [400,640]];
そして、以下のように円形を座標を指定して追加したとします。
L.circle([100, 320], {color: 'green',radius: 35}).addTo(map)
すると以下のように表示され、座標系が図中のように計算されていることがわかります。
座標系は
bounds
の左下が原点- 指定方法は上下が
x
で左右がy
座標と思えばいい
です。
実践
席を画像の通り配置する
以下のようにして大量に配置していきます。
コードは以下のようになっています。
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ドキュメント)
- Seats(Seatについてのコレクション)
- Floor(Floorドキュメント)
- Floors(Floorについてコレクション)
- Store(店についてのコレクション)
- Stores(Storeについてのコレクション)
Floors
コレクションにしたのは、いくつか階をもっているものだったりするからです。
そして以下のようにSeatドキュメントはフィールドで構成されています。
フィールド | 内容 |
---|---|
cordinates |
座標 |
isOcupied |
席に座られているか |
そして以下のように取得します。 ここでは、Cloud Firestoreの初期設定については説明を割愛させていただきます。
Firestoreからリアルタイムで取得して描画する
最初はこのように取得します。本記事では店番号1
の1階
についての可視化をします。
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") }) })
isOccupied
がfalse
ならば、席が空いているということで色を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") }) })
のようにします。
するとこのようになります。
席を追加できるようにする
以下のように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(()=>{ }) })
すると追加ができるようになりました。
席のメタデータを付与する
席は取得できたのですが席自体に
- いつ座ったのか
- 誰が座ったのか
などの席のメタデータを付与したい時があります。
そのようなときに新しく
Usersコレクションをルートから作成し、Seatのフィールドに
- userID: Usersコレクション内のUserドキュメントID
を追加して指定しようと思います。今回はすべて同じ人であるとして実装します。
ここでUserドキュメントは決め打ちで
- Username
- Profile画像URL
にします。Profile画像は最近知ったこのキャラにします。
このuserIDを取得してPopupに出してみようと思います。
例えば、以下のようにPopupを実装することができます。
circle.bindTooltip("HTML or String", {permanent: true, direction: 'center'}).openTooltip();
ここにHTMLを代入すると返してくれます。
以下のようなヘルパー関数を定義してHTMLを生成します。
function getTool(seat, user) { ... }
また、これまでcircle
を作成していたところにpopUp
を挟むような実装をします。
そして、mouseover
したときに表示するようにします。
すると、以下のようにモックを作ることができます。
あとは何かしらなAPIでCloud Firestore内を書き換えたり、直接Webアプリで書き換えてもいいです。
まとめ
RxSwiftばかり使っていたのでフロントエンドでRxJSを使えてよかった。
まだまだ、htmlとcssで凝れる部分はいくらでもあるので、どんどんやっていきたいと思いました。