リアルタイム位置情報共有アプリWhere am I in Sapporo
複数のユーザーがブラウザから自分の位置を入力し、他のユーザーとの間にリアルタイムで共有することができるアプリを作ってみました。選択できる位置は札幌市内の主要観光スポットを事例としています。
ユーザーごとにIDが付与され、ネコの写真もアイコンとしてランダムで付与されます。
ソースコードは以下リポジトリで公開しています。
https://github.com/xinmiao1995/share_now_location
利用するツール
- WebAPI配信
- FastAPI
- WebSocket
- ngrok
- マップビュワー
- MapLibre GL JS
- OpenStreetMapベースマップの表示
- マーカーの表示
- 地図ズーム
- Turf.js
- 地図ズーム領域の取得
- MapLibre GL JS
- アイコン画像フリー素材
リアルタイム通信を実現するために
FastAPIについて
FastAPIはPython(バージョン3.6以降対応)製の軽量Webフレームワークです。非同期処理が対応可能との特徴もありますので、今回は主にこちらを利用しています。また、PythonのパッケージではWebSocketも内容されているので、fastapiをインストールするだけで、WebSocketの機能も使えます。
- インストール
pip install fastapi
- Pythonコードの書き方
from fastapi import FastAPI, WebSocket
WebSocketについて
WebSocketについては常時接続が可能なプロトコルです。接続トラフィックの低減と非同期処理によって、安定した双方向通信が実現できます。それによって、今回の目標である複数クライアント間のリアルタイム通信が可能になリます。
出典:https://atmarkit.itmedia.co.jp/ait/articles/1111/11/news135.html
システムフロー
今回のアプリケーションでは、このように複数のクライエントとサーバー間の通信ができることを目指しています。
ソースコード
-
サーバーサイド
サーバーサイドではクライエントの情報を保存したり、加工したり、クライエントに情報を送付したりしています。本来はデータベースを利用するべきですが、今回は簡易的にPythonのリストでユーザー情報を保管します。サーバーを再起動すればリセットされますので要注意です。 -
WebSocket接続の成立と切断
async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) def disconnect(self, websocket: WebSocket): self.active_connections.remove(websocket)
-
クライエントにメッセージの送付
async def broadcast(self, client_id: int, coordinate: str): self.client_ids.append(client_id) self.markers[client_id] = { "coordinate": coordinate, "marker_size": self.client_ids.index(client_id) + 40, } for connection in self.active_connections: await connection.send_json({ "message": f"user {client_id}'s location: {coordinate}", "markers": self.markers })
@app.websocket("/ws/{client_id}") async def websocket_endpoint(websocket: WebSocket, client_id: int): await manager.connect(websocket) try: while True: text = await websocket.receive_text() await manager.broadcast(client_id, text) except WebSocketDisconnect: manager.disconnect(websocket) del manager.markers[client_id]
こちらではjsonを送っていますが、WebSocketではsend,receiveできる型はそれぞれtext,bytes,jsonがあります。async、awaitは非同期処理を意味しています。
-
フロントエンド
クライエントでは、ユーザーIDの生成や、サーバーから受け取った情報をマップ上の常時をしています。- 定義したURLにアクセスしたらWebSocketインスタンスが宣言される
let ws = new WebSocket(`wss://${hostName}/ws/${client_id}`);
- ユーザーが選択した位置をサーバーに送る
function sendMessage(event) { ws.send(loc_coordinat[document.getElementById("myDropdown").value]) event.preventDefault() }
- サーバーからメッセージが届いたらマップ上にマーカーを表示させる
ws.onmessage = function (event) { Object.keys(JSON.parse(event.data).markers).forEach((key, index) => { let coordinate = JSON.parse(event.data).markers[key].coordinate; let marker_size = JSON.parse(event.data).markers[key].marker_size; ... // add marker to map new maplibregl.Marker(el) .setLngLat(coordinate.split(',')) .addTo(map); points.push(coordinate.split(',')); // Zoom to points const pointsCollection = turf.featureCollection(points.map(point => turf.point(point))); const bbox = turf.bbox(pointsCollection); map.fitBounds([ [bbox[0], bbox[1]], [bbox[2], bbox[3]] ]); }); };
-
起動するには
# ローカルホストを起動する(ローカルマシンから接続可能)
uvicorn main:app --reload
# ngrokで公開(任意のIPから接続可能)
ngrok http 8000
参考資料
動かして学ぶ!Python FastAPI開発入門
FastAPI公式ドキュメント
双方向通信を実現! WebSocketを使いこなそう
MapLibre公式ドキュメント