ちょっと話題の記事

Node.js + MongoDB 位置情報を保存し検索する(CoffeeScriptで)【19日目】

2012.12.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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 }

つぶやきを登録してみる

適当につぶやきます。

tweet

登録後、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 としました。


画面を開くと、現在地近くのつぶやきのみ表示されているはずです。

tl

まとめ

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