Androidのメモとか

ポキオの日記です。今日も遅延してない。

バチ抜けをNode-REDで地図に可視化する

つりはっく。

ポキオ Node-RED WorldMap バチ抜け

バチ抜けとは

釣り用語の一つです。

pokio-ringyo.hatenablog.com

pokio-ringyo.hatenablog.com

バチとはイソメやゴカイなどの環形動物のことを指し、バチ抜けとは春先の大潮の夜に海底に潜んでいるバチが一気に浮いてくる現象です。抜けたバチは種々の魚にとってはごちそうであって、バチを求めて魚が寄ってくるというわけです。バチ抜けは地域によって発生する時期がまちまちで、更に魚が寄ってくるかもわからない自然のイベントとなっているわけで、いつどのへんでバチ抜けが起こったのかを知りたいのは釣り人の性みたいなもの。

ポキオ輪業商会 横浜 メバリング シーバス プラッキング

今回は、Twitterで位置情報とともにつぶやかれている情報をもとに、バチ抜けなどの情報を地図上にマッピングしてみようと思います。

使うのはNode-RED

今回もNode-REDで実装していきます。フローはこんな感じ。

ポキオ Node-RED WorldMap バチ抜け

ポイントとなるノードはこちら。

Twitterノード

これでTwitter上の情報をほぼリアルタイムにスキャンして、指定したハッシュタグのツイートを引っ張ってきます。実際にツイートが見つかると、おそらくTwitter本家のTweet Objectの形式でデータが取得できます。

developer.twitter.com

ただ、仕様が複雑(笑)今回は、次のFunctionノードでテキトーに位置情報をパースします。

Functionノード

ここで、Twitterノードから取得したデータから位置情報をパースします。

if(msg.tweet && msg.tweet.place && msg.tweet.place.bounding_box && msg.tweet.place.bounding_box.coordinates){
    var longitude = 0;
    var latitude = 0;
    var array = msg.tweet.place.bounding_box.coordinates[0];
    var tweet = msg.payload.replace(/\r?\n/g,"");
    var link = 'https://twitter.com/' + msg.tweet.user.screen_name + '/status/' + msg.tweet.id_str;

    array.forEach(value => {
        longitude += value[0];
        latitude += value[1];
    });
    
    longitude /= array.length;
    latitude /= array.length;
    
    msg = {};
    msg.has_location = true;
    msg.payload = {
        "lat" : latitude,
        "lon" : longitude,
        "name" : tweet,
        "weblink" : link,
    };
}
return msg;

tweet.place.bounding_box.coordinates内の座標の平均値をとって、それを緯度経度として採用しています。後段の処理でCSVファイルに保存するため、Tweet本文は改行を削除してから保存しています。

CSVノード・Fileノード

前段の処理で、位置情報がパースできたTweetだけをCSVとして一旦保存しておきます。

WorldMapノード

今回の要。

flows.nodered.org

node-red-contrib-web-worldmapをインストールして、地図を表示できるようになります。フロー中にこのWorldMapノードがあると、特定のURLにアクセスすると地図が表示されるようになります。このノードに対して緯度経度などをくべると、地図上にピンが表示されるようになります。今回は、WorldMapにアクセスがあったときに発火するイベントを契機に、CSVを読み込み、そこに書かれた位置情報をすべてWorldMapにマッピングします。

これをつかって地図表示させてみました

こんな感じ。

ポキオ Node-RED WorldMap バチ抜け

まだまだTwitterの監視をし始めたばかりなのでピンはすくなめですが、ちゃんと可視化できています。

ちょっとずつピンが増えてくると楽しいですねぇ。

フローはこちら

[
    {
        "id": "add2a0f2.b28d2",
        "type": "twitter in",
        "z": "52e43921.021f08",
        "twitter": "",
        "tags": "バチ抜け,シーバス,#バチ抜け,#シーバス",
        "user": "false",
        "name": "バチ抜けツイート",
        "inputs": 0,
        "x": 130,
        "y": 300,
        "wires": [
            [
                "643d563.f2d9fa8",
                "1e92e179.29a17f"
            ]
        ]
    },
    {
        "id": "efe29703.79f498",
        "type": "comment",
        "z": "52e43921.021f08",
        "name": "ツイート監視",
        "info": "",
        "x": 110,
        "y": 260,
        "wires": []
    },
    {
        "id": "643d563.f2d9fa8",
        "type": "function",
        "z": "52e43921.021f08",
        "name": "位置情報パース",
        "func": "if(msg.tweet && msg.tweet.place && msg.tweet.place.bounding_box && msg.tweet.place.bounding_box.coordinates){\n    var longitude = 0;\n    var latitude = 0;\n    var array = msg.tweet.place.bounding_box.coordinates[0];\n    var tweet = msg.payload.replace(/\\r?\\n/g,\"\");\n    var link = 'https://twitter.com/' + msg.tweet.user.screen_name + '/status/' + msg.tweet.id_str;\n\n    array.forEach(value => {\n        longitude += value[0];\n        latitude += value[1];\n    });\n    \n    longitude /= array.length;\n    latitude /= array.length;\n    \n    msg = {};\n    msg.has_location = true;\n    msg.payload = {\n        \"lat\" : latitude,\n        \"lon\" : longitude,\n        \"name\" : tweet,\n        \"weblink\" : link,\n    };\n}\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 340,
        "y": 300,
        "wires": [
            [
                "8649b0d9.a5f98"
            ]
        ]
    },
    {
        "id": "dddeedd9.5a74b",
        "type": "worldmap",
        "z": "52e43921.021f08",
        "name": "",
        "lat": "",
        "lon": "",
        "zoom": "",
        "layer": "",
        "cluster": "",
        "maxage": "",
        "usermenu": "show",
        "layers": "show",
        "panit": "false",
        "panlock": "false",
        "zoomlock": "false",
        "hiderightclick": "false",
        "coords": "none",
        "showgrid": "false",
        "allowFileDrop": "false",
        "path": "/worldmap",
        "x": 1060,
        "y": 460,
        "wires": []
    },
    {
        "id": "8649b0d9.a5f98",
        "type": "switch",
        "z": "52e43921.021f08",
        "name": "位置情報があるときだけ動作",
        "property": "has_location",
        "propertyType": "msg",
        "rules": [
            {
                "t": "true"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 1,
        "x": 580,
        "y": 300,
        "wires": [
            [
                "6d46063b.e4bce8"
            ]
        ]
    },
    {
        "id": "433fb432.0e49fc",
        "type": "function",
        "z": "52e43921.021f08",
        "name": "ズーム",
        "func": "msg.payload = {\n    command : {\n        lat : msg.payload.lat,\n        lon : msg.payload.lon,\n        zoom : 6,\n    }\n}\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 930,
        "y": 480,
        "wires": [
            [
                "dddeedd9.5a74b"
            ]
        ]
    },
    {
        "id": "75d4e561.cf315c",
        "type": "worldmap in",
        "z": "52e43921.021f08",
        "name": "",
        "path": "/worldmap",
        "events": "all",
        "x": 100,
        "y": 460,
        "wires": [
            [
                "493cf6af.c9d1d8"
            ]
        ]
    },
    {
        "id": "493cf6af.c9d1d8",
        "type": "switch",
        "z": "52e43921.021f08",
        "name": "",
        "property": "payload.action",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "connected",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 1,
        "x": 250,
        "y": 460,
        "wires": [
            [
                "6381668a.b712c8"
            ]
        ]
    },
    {
        "id": "6d46063b.e4bce8",
        "type": "csv",
        "z": "52e43921.021f08",
        "name": "CSVに変換",
        "sep": ",",
        "hdrin": "",
        "hdrout": "",
        "multi": "one",
        "ret": "\\n",
        "temp": "lat,lon,name,weblink",
        "skip": "0",
        "strings": false,
        "x": 810,
        "y": 300,
        "wires": [
            [
                "96084250.d8c06",
                "23921cd8.841d44"
            ]
        ]
    },
    {
        "id": "96084250.d8c06",
        "type": "file",
        "z": "52e43921.021f08",
        "name": "history.csvに保存",
        "filename": "history.csv",
        "appendNewline": true,
        "createDir": false,
        "overwriteFile": "false",
        "encoding": "none",
        "x": 1010,
        "y": 300,
        "wires": [
            []
        ]
    },
    {
        "id": "23921cd8.841d44",
        "type": "debug",
        "z": "52e43921.021f08",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "x": 970,
        "y": 340,
        "wires": []
    },
    {
        "id": "3b137fe3.26fa",
        "type": "csv",
        "z": "52e43921.021f08",
        "name": "",
        "sep": ",",
        "hdrin": "",
        "hdrout": "",
        "multi": "one",
        "ret": "\\n",
        "temp": "lat,lon,name,weblink",
        "skip": "0",
        "strings": true,
        "x": 630,
        "y": 460,
        "wires": [
            [
                "5786ae6a.d8e1"
            ]
        ]
    },
    {
        "id": "6381668a.b712c8",
        "type": "file in",
        "z": "52e43921.021f08",
        "name": "history.csvを読み込み",
        "filename": "history.csv",
        "format": "utf8",
        "chunk": false,
        "sendError": false,
        "encoding": "none",
        "x": 440,
        "y": 460,
        "wires": [
            [
                "3b137fe3.26fa"
            ]
        ]
    },
    {
        "id": "5786ae6a.d8e1",
        "type": "function",
        "z": "52e43921.021f08",
        "name": "weblink作成",
        "func": "const weblink = {\n    \"name\" : \"Twitter\",\n    \"url\" : msg.payload.weblink,\n    \"target\":\"_new\"\n}\n\nmsg.payload.weblink = weblink;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 790,
        "y": 460,
        "wires": [
            [
                "dddeedd9.5a74b",
                "433fb432.0e49fc"
            ]
        ]
    },
    {
        "id": "cddaa0e7.caea7",
        "type": "comment",
        "z": "52e43921.021f08",
        "name": "Mapにアクセスがあったとき",
        "info": "",
        "x": 160,
        "y": 420,
        "wires": []
    },
    {
        "id": "1e92e179.29a17f",
        "type": "debug",
        "z": "52e43921.021f08",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "x": 310,
        "y": 340,
        "wires": []
    }
]
「Androidのメモとか」は、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、Amazonアソシエイト・プログラムの参加者です。

このブログは個人的なメモ書きであったり、考えを書く場所であります。執筆者の所属する団体や企業のコメントや意向とは無関係であります。また、このブログは必ずしも正しいことが書かれているとは限らず、誤字脱字や意図せず誤った情報を載せる場合がありえます。それが原因で読者が不利益を被ったとしても、執筆者はいかなる責任も負いません。ありがとうございます。