Node.js + MongoDB 位置情報を保存し検索する(CoffeeScriptで)【19日目】
MongoDB には、地理空間のインデックスが用意されていて、簡単に位置情報を操作することが出来ます。
今回は、つぶやきを HTML5 の Geolocation API から取得した位置情報とともに MongoDB に保存しておき、
現在地近くのつぶやきだけを検索して表示するという簡単なサンプルを作ってみます。
ありがたいことに、うえじゅんさんの作った Node.js + MongoDB のサンプルがあるのでこれをベースに作っていきましょう。
https://dev.classmethod.jp/server-side/node-socket-io-mongodb/
位置情報を保存するフィールドを定義
つぶやきのモデルに位置情報の保存フィールドを定義します。
「message.coffee」
mongo = require 'mongoose' mongo.connect 'mongodb://localhost/cmapp' Schema = mongo.Schema Message = mongo.model 'messages', new Schema text: String location: [Number, Number] created_at: {type: Date, default: Date.now} updated_at: {type: Date, default: Date.now} module.exports = Message
location 部分を配列で定義します。
重要なのは順番で、保存や検索を行う際は、経度(longitude)、緯度(latitude)の順番で指定します。
地理空間インデックスの作成
二次元の地理空間のインデックス(geospatial index)を作成します。
MongoDBが起動している状態で以下を実行します。
$ mongo $ use cmapp $ db.messages.ensureIndex( { location : "2d" } )
つぶやき情報に位置情報を付加する
「chat.coffee」
$('#send').click (event) -> navigator.geolocation.getCurrentPosition (position) => longitude = position.coords.longitude latitude = position.coords.latitude msg = text: $("input#message").val() longitude: longitude latitude: latitude $("input#message").val "" socket.emit 'message:send', msg
HTML5 の Geolocation API から経度、緯度を取得して、その情報をつぶやきとともに emit します。
位置情報を保存する
保存時は、messageのlocationフィールドに 経度、緯度を渡して保存します。
「app.coffee(一部抜粋)」
socket.on 'message:send', (data) -> msg = new message() msg.text = data.text msg.location = [data.longitude, data.latitude] msg.save (err) -> throw err if err io.sockets.emit 'message:receive', { message: msg }
つぶやきを登録してみる
適当につぶやきます。
登録後、Collection を確認すると、位置情報が保存されていることが分かります。
$ db.messages.find() { "text" : "あきばだよ", "_id" : ObjectId("50cdf55fefc0380000000002"), "updated_at" : ISODate("2012-12-16T16:22:55.324Z"), "created_at" : ISODate("2012-12-16T16:22:55.324Z"), "location" : [ 139.6917375, 35.6162695 ], "__v" : 0 }
位置情報を指定して検索する
起動時に現在位置から半径1kmの範囲のつぶやきを取得するように修正します。
「chat.coffee(一部抜粋)」
navigator.geolocation.getCurrentPosition (position) -> position = latitude: position.coords.latitude longitude: position.coords.longitude socket.emit 'session:start', position
起動時に自身の位置情報をemitします。
「app.coffee(一部抜粋)」
io.sockets.on 'connection', (socket) -> socket.on 'session:start', (position) -> conditions = getConditions position message.find conditions, (err, messages) -> throw err if err socket.emit 'messeges:show', { messages: messages} socket.on 'message:send', (data) -> msg = new message() msg.text = data.text msg.location = [data.longitude, data.latitude] msg.save (err) -> throw err if err io.sockets.emit 'message:receive', { message: msg } getConditions = (position) -> conditions = location : $within : $centerSphere : [[position.longitude, position.latitude], 0.000157]
getConditions メソッド内で条件を取得しています。
centerSphere で 点(経度、緯度)と半径を指定し円を描き、within で その円内に存在するアイテムを選択することを指定します。
半径部分はラジアンを使って指定します。地球の半径で考えると1ラジアンは、6378.137km らしいので、1km ≒ 0.000157 としました。
画面を開くと、現在地近くのつぶやきのみ表示されているはずです。
まとめ
MongoDBの地理空間インデックスを使用することで、柔軟に位置情報を操作できることが分かりました。
位置情報と連動させて色々面白そうなものを作ってみたいです。
「app.coffee」
express = require 'express' http = require 'http' path = require 'path' io = require 'socket.io' message = require './models/message' app = express() server = http.createServer app io = io.listen server app.configure -> app.set 'port', process.env.PORT || 3000 app.set 'views', __dirname + '/views' app.set 'view engine', 'jade' app.use express.favicon() app.use express.logger 'dev' app.use express.bodyParser() app.use express.methodOverride() app.use app.router app.use express.static path.join __dirname, "public" app.configure 'development', -> app.use express.errorHandler() server.listen app.get 'port' io.sockets.on 'connection', (socket) -> socket.on 'session:start', (position) -> conditions = getConditions position message.find conditions, (err, messages) -> throw err if err socket.emit 'messeges:show', { messages: messages} socket.on 'message:send', (data) -> msg = new message() msg.text = data.text msg.location = [data.longitude, data.latitude] msg.save (err) -> throw err if err io.sockets.emit 'message:receive', { message: msg } getConditions = (position) -> conditions = location : $within : $centerSphere : [[position.longitude, position.latitude], 0.000157]
「chat.coffee」
socket = io.connect() navigator.geolocation.getCurrentPosition (position) -> position = latitude: position.coords.latitude longitude: position.coords.longitude socket.emit 'session:start', position socket.on 'messeges:show', (data) -> for message in data.messages $("div#chat-area").prepend "<div>" + message.text + "</div>" socket.on 'message:receive', (data) -> $("div#chat-area").prepend "<div>" + data.message.text + "</div>" $('#send').click (event) -> navigator.geolocation.getCurrentPosition (position) -> latitude = position.coords.latitude longitude = position.coords.longitude msg = text: $("input#message").val() latitude: latitude longitude: longitude $("input#message").val "" socket.emit 'message:send', msg