Node.jsでTwitter Streaming APIをGoogleマップにリアルタイム表示

2015/09/30 Nodejs

Node.js初心者なので、リアルタイムの何かをつくってみようと思いTwitterの Streaming API で試してみた。

デモを以下に設置した。

TweetMap
http://demo.kijtra.com/tweetmap/

画面イメージ。
TweetMap

環境と使用したものは以下。

  • CentOS6(さくらVPS 1G)
  • Node.js 0.10.36
    • express 4.13.3
    • socket.io 0.9.17
      最新(1.x系)だとなんかうまくいかなかったので0.9系を使った。
    • twitter 1.2.5
      TwitterのAPI用ライブラリ。似たやつで「node-twitter」というのがあるけど、それとは違うやつ。
    • twitter-text 1.13.2
      ツイートテキストをHTML表示用に変換してくれる公式ライブラリ。
    • point-in-polygon 1.0.0
      ある座標(Point)がポリゴンに含まれているか判定してくれるライブラリ。
    • forever 0.15.1
      Node.jsで作ったサーバーを裏側で起動しっぱなしにするやつ。
  • クライアントサイド(すべてCDNで対応)
    • Google Maps API 3
    • jQuery 1.10.2
    • Bootstrap 3.3.5
    • FontAwesome 4.4.0
      アイコンフォント。ごく一部に使用。
    • twemoji 1.3.2
      ツイート内の絵文字を画像で表示する公式ライブラリ。
    • jGrowl 1.4.1
      ツイートをグロール通知するライブラリ。

Node.jsでサーバー用スクリプトを書いて(サーバーサイド)、そのなかでHTMLファイルを表示する指示をする(クライアントサイド)という流れ。

サーバーサイドのスクリプト(ここでは「server.js」)は以下のような感じ。
日本全体のポリゴン作成はGoogle Maps and KML shapes generatorを使用。

var util = require('util');
var http = require('http');
var express = require('express');
var socketio = require('socket.io');
var twitter = require('twitter');
var twtext = require('twitter-text');
var polygon = require('point-in-polygon');

// 日本全体をおおまかに包含するポリゴンの座標
var poly_jp = [
  [23.94609601499837, 123.211669921875],
  [21.881889807629282, 128.287353515625],
  [26.22444694563432, 142.33062744140625],
  [46.56641407568593, 151.1663818359375],
  [45.78284835197676, 141.844482421875],
  [45.22848059584359, 138.680419921875],
  [34.88593094075317, 129.210205078125],
  [26.27371402440643, 123.299560546875],
  [24.587090339209634, 122.2833251953125],
  [23.94609601499837, 123.211669921875]
];


// サーバー起動
var app = express();
var server = http.createServer(app);

app.get('/', function (req, res) {
  res.sendFile(__dirname + '/index.html');
});

var io = socketio.listen(server, {log: false});
server.listen(3000);



// Twitter APIの設定
var twit = new twitter({
  consumer_key: 'Twitter API の Consumer Key',
  consumer_secret: 'Twitter API の Consumer Secret',
  access_token_key: 'Twitter API の Access Token',
  access_token_secret: 'Twitter API の Access Token Secret'
});

// Twitter Streaming APIでフィルタする範囲オプション
// (日本全体を含む矩形の南西の座標と北東の座標)
var option = {
    locations: '123.283201,24.117224,150.625329,46.242887'
};

// Streaming API開始
twit.stream('statuses/filter', option, function(stream) {
  stream.on('data', function (data) {
    // 座標つきツイート、かつポリゴン範囲内のもののみ抽出
    if (data.geo && polygon(data.geo.coordinates, poly_jp)) {
        // ツイートテキストをHTML表示用に変換
        var formatted = twtext.autoLink(twtext.htmlEscape(data.text));

        // HTML側で使用するため新たに「text_formatted」というキー名にセット
        data.text_formatted = formatted;

        // Socket.IOで送出
        io.sockets.emit('msg', data);
    }
  });
});

クライアントサイド(ここでは「index.html」)はこんな感じ。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>TweetMap</title>
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jquery-jgrowl/1.4.1/jquery.jgrowl.min.css">
  <style>
    html, body {
      height: 100%;
      margin: 0;
      padding: 0;
      overflow: hidden;
    }

    .jGrowl {
        color: #444;
    }

    .jGrowl-notification {
        background: #fff;
        box-shadow: 0 1px 3px rgba(0,0,0,.3);
    }

    .jGrowl-closer {
        color: #fff;
        background: #0275D8;
    }

    .tweet hr {
        margin: 7px 0;
        clear: both;
    }

    #map {
        width:100%;
        height: 100%;
        position:absolute;
        top:0;
        left:0;
    }
  </style>
</head>
<body>

<div id="map"></div>

<script src="/socket.io/socket.io.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twemoji/1.3.2/twemoji.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-jgrowl/1.4.1/jquery.jgrowl.min.js"></script>
<script>
// Google Maps APIのコールバック関数
function initMap() {
  var currentWindow = null,
  markers = [],
  infowindows = [],
  markerCount = 0,
  markerMax = 1000,// マーカー最大数
  infowindowMax = 3;// フキダシ最大数

  var map = new google.maps.Map(document.getElementById('map'), {
    center: {lat: 38.2586, lng: 137.6850},
    zoom: 7,
    zoomControlOptions: {
        position: google.maps.ControlPosition.LEFT_BOTTOM
    },
    streetViewControlOptions: {
        position: google.maps.ControlPosition.LEFT_BOTTOM
    }
  });

  // 日本を含む矩形をfitBoundsすることで、画面サイズに依らず日本の中心を表示
  map.fitBounds(new google.maps.LatLngBounds(
    new google.maps.LatLng(27.128750, 128.267736),
    new google.maps.LatLng(44.489597, 144.263829)
  ));

  // Socket.IOと接続
  var socket = io.connect();

  // Streaming APIからデータが来た際の処理
  socket.on('msg', function(data) {
    if (currentWindow) {
      currentWindow.close();
    }

    // ツイートテキスト(絵文字を変換)
    var tweet = twemoji.parse(data.text_formatted, {size:16});

    // グロールに表示するHTMLの組み立て
    var text = '<div class="tweet">';
    text += '<a href="https://twitter.com/' + data.user.screen_name + '/statuses/' + data.id_str + '" target="_blank">';
    text += '<img src="' + data.user.profile_image_url_https + '" width="16">';
    text += ' <strong>' + data.user.screen_name + '</strong></a>';
    text += '<hr>';
    text += tweet;
    // 位置情報の名称があれば表示
    if (data.place && data.place.name) {
        text += '<hr><i class="fa fa-fw fa-map-marker text-danger"></i> <a href="https://twitter.com/search?q=place%' + data.place.id + '" target="_blank">' + data.place.name + '</a>';
    }
    text += '</div>';

    // グロールを表示
    $.jGrowl(text, {
        life: 10000
    });

    // 地図マーカーのフキダシ
    var infowindow = new google.maps.InfoWindow({
      content: '<div>' + tweet + '</div>',
      maxWidth: 200,
      disableAutoPan: true
    });

    infowindows.push(infowindow);

    // 地図マーカー
    var marker = new google.maps.Marker({
      map: map,
      position: new google.maps.LatLng(data.geo.coordinates[0], data.geo.coordinates[1]),
      animation: google.maps.Animation.DROP,
      title: '@' + data.user.screen_name
    });

    marker.infowindow = infowindow;

    // マーカーをクリックしたらフキダシ表示
    marker.addListener('click', function() {
      this.infowindow.open(map, this);
    });

    // デフォルトでフキダシを開く
    infowindow.open(map, marker);

    markers.push(marker);

    // ウインドウが最大数になったら前のものを閉じる
    if (markerCount >= infowindowMax) {
      var cls = markerCount - infowindowMax;
      infowindows[cls].close();
    }

    // マーカー数を記録
    markerCount++;

    // マーカーが最大数になったら徐々に透明になっていく(10段階)
    if (markerCount > markerMax) {
      var fade = (markerCount + 1) - markerMax;
      markers[fade].setOpacity(0.9);

      fade--;
      if (markers[fade]) {
        markers[fade].setOpacity(0.8);
        fade--;
        if (markers[fade]) {
          markers[fade].setOpacity(0.7);
          fade--;
          if (markers[fade]) {
            markers[fade].setOpacity(0.6);
            if (markers[fade]) {
              markers[fade].setOpacity(0.5);
              fade--;
              if (markers[fade]) {
                markers[fade].setOpacity(0.4);
                fade--;
                if (markers[fade]) {
                  markers[fade].setOpacity(0.3);
                  fade--;
                  if (markers[fade]) {
                    markers[fade].setOpacity(0.2);
                    fade--;
                    if (markers[fade]) {
                      markers[fade].setOpacity(0.1);
                      fade--;
                      if (markers[fade]) {
                        markers[fade].setMap(null);
                        markers[fade] = null;
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  });
}
</script>
<script src="//maps.googleapis.com/maps/api/js?v=3.exp&sensor=false&callback=initMap"></script>
</body>
</html>

上記2ファイルを同ディレクトリに置いたら、そこで以下を実行してサーバーを起動。

node server.js

コマンドには何も表示されないけど実行されているので、 http://localhost:3000 にアクセスしてみると動いている。
サーバーを止める場合はCtrlC

comments powered by Disqus