Mapbox、Node.js、GridDBによる世界の人口データの可視化

私たちは何を作るのか?

この記事では、世界の人口データを表示するウェブベースのマップを作成する方法を紹介します。データはworldometers から抽出し、React、Mapbox GL JS、Node.js、GridDBを使用してウェブベースの地図アプリケーション全体を構築しています。世界地図には、人口データの多い上位10カ国が表示されます。

プロジェクトを実行する

プロジェクトを起動するには、プロジェクトのリポジトリにアクセスします。リポジトリをクローンし、Node.jsのバージョンが18であることを確認し、必要な依存関係をすべてインストールし、プロジェクトを開始します。

git clone git@github.com:junwatu/griddb-codes.git

cd griddb-codes

core enable

corepack prepare pnpm@7.30.0 --activate

pnpm install

cd packages/data-server/

pnpm install

cd ../client

pnpm install

cd ../../

npm start

ブラウザで http://localhost:5173 にアクセスすると、ウェブマップが表示されます。

開発の流れ

このプロジェクトの開発フローは、いくつかのステップに分けることができます。

データ収集

まず、worldometersからリアルタイムのデータを取得する必要があります。Webサイトをスクラップしたり、APIを利用したりして、必要なデータを取得することができます。

データは一定時間ごとに抽出し、GridDBデータベースに格納します。保存されたデータは、さらなる分析、予測、レポート作成などに利用することができます。

バックエンド開発

サーバーサイドのアプリケーションサーバーのコーディングにはNode.jsを使用する予定です。

なぜNode.jsなのでしょうか。

JavaScriptはフルスタックアプリケーション開発の共通言語であり、当然の選択です。

私たちが作るアプリケーションサーバーはWebSocketベースで、Node.jsはデータストレージとしてGridDBとやりとりし、ワールドメーターからデータを取得し、クライアントUIからのリクエストとレスポンスを処理します。

フロントエンド開発

ユーザーインターフェースの作成にはReact.jsを使用し、世界人口データを視覚的に表示するために、強力な地図ライブラリであるMapbox GL JSを使用します。このライブラリにより、インタラクティブでカスタマイズ可能なマップを作成することができます。Mapbox GL JSをReactアプリケーションに統合して、人口データを表すラベルを持つ世界地図をレンダリングします。

前提条件

アプリケーションをコーディングする前に、開発用のソフトウェアとツールをセットアップする必要があります。ここでは、WSL 2 on Windows 11のOSにUbuntu 20.04を使用しています。

WSLはWindows 10のバージョン2004以上(Build 19041以上)のみで利用可能です。WSLの新規インストールはこちらのlinkに進んでください。

GridDB

GridDB™は、IoTやビッグデータに最適化された、拡張性の高いインメモリNoSQL時系列データベースです。注目すべきは、2種類のコンテナ・カテゴリーを備えていることです。

コレクションコンテナ

このタイプのコンテナは、従来のリレーショナル・データベースに似ています。データはキー値として保存され、コレクションコンテナは基本的なCRUD(作成、読み取り、更新、削除)操作をサポートします。

時系列コンテナ

このコンテナは、時系列データ(時間によって索引付けされた一連のデータポイント)を管理するために特別に設計されています。TimeSeries コンテナ内の各レコードは、一意なキーとなるタイムスタンプを持ち、このコンテナ内のデータは、要求があれば削除する以外は「追記のみ」です。

インストール

WSLにGridDBをインストールするためには、いくつか知っておくべきことがあります。

⚠️ GridDB の deb パッケージは systemd を使用しますが、WSL 2 Windows 11 上の Ubuntu 20.04 は SysVinit を使用するため、Ubuntu WSL では /etc/wsl.confというファイルを編集して、systemd を有効にする必要があります(このファイルが存在しない場合は作成します)。

Ubuntuインスタンスの内部で、/etc/wsl.conf1に以下の修正を加えます。

[boot]
systemd=true

Windowsのターミナルを開き、以下のコマンドでwslを再起動します。

wsl --shutdown

その後、コマンドで再びwslを起動します。

wsl

GridDBをインストールするには、https://docs.griddb.net/ja/latest/gettingstarted/using-apt/#apt-getでインストールの手順に従います。

GridDBを起動し、サービスが稼働しているかどうかを確認します。以下のコマンドを使用します。

sudo systemctl status gridstore

そして、問題がなければ、このようなメッセージが表示されます。

● gridstore.service - GridDB database server.
     Loaded: loaded (/lib/systemd/system/gridstore.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2023-03-16 18:56:13 +07; 58min ago
    Process: 314 ExecStart=/usr/griddb/bin/gridstore start (code=exited, status=0/SUCCESS)
   Main PID: 393 (gsserver)
      Tasks: 34 (limit: 4605)
     Memory: 138.5M
        CPU: 20.129s
     CGroup: /system.slice/gridstore.service
             └─393 /usr/bin/gsserver --conf /var/lib/gridstore/conf

Mar 16 18:56:10 GenAI systemd[1]: Starting GridDB database server....
Mar 16 18:56:10 GenAI gridstore[314]: Starting gridstore service:
Mar 16 18:56:13 GenAI gridstore[392]: ..
Mar 16 18:56:13 GenAI gridstore[392]: Started node.
Mar 16 18:56:13 GenAI gridstore[314]: [ OK ]
Mar 16 18:56:13 GenAI systemd[1]: Started GridDB database server..

OSを再起動するたびにGridDBが起動するので、手動で起動する必要はありません。

Node.js

Node.js LTSをインストール2するには、以下のコマンドを実行します。

curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - &&\
sudo apt-get install -y nodejs

アプリケーションとGridDBの接続にはgriddb node-apiを使用しますが、その前にgriddb c clientをインストールしましょう。GridDB C Clientは、GridDBのC言語インタフェースを提供します。

wget https://github.com/griddb/c_client/releases/download/v5.0.0/griddb-c-client_5.0.0_amd64.deb
sudo dpkg -i griddb-c-client_5.0.0_amd64.deb

GridDB node-api

Node.jsからGridDBに接続するには、griddb-node-apiを使用する必要があります。このパッケージはnode-addon-apiを使ってビルドされており、2つのインストール方法があります。

1. ソースコードからコンパイルする。この方法は、特定のnode.jsのバージョンでgriddb-node-apiを使用する必要がある場合、正しい方法です。

git clone git@github.com:griddb/node-api.git
cd node-api
npm install

このようなエラーメッセージが表示された場合は、

gyp ERR! stack Error: not found: make

Ubuntuのbuild-essentialsパッケージをインストールしましょう。すべてが成功すれば、griddb.nodeというファイルが作成されるので、それをNODE_PATHに含める必要があります。

export NODE_PATH=$(pwd)

2. npm パッケージ griddb-node-api をプロジェクトに直接インストールします。

npm install griddb-node-api

今回のプロジェクトでは、よりシンプルなので2番目の方法で、Node 18 LTSを使用することにします。

VSCode

前述の通り、OSはUbuntu 20.04をWSL Windows 11上で使用しています。Windowsからコーディングする場合は、Visual Studio Codeのremote WSL pluginが充実しているため、これを使用しています。

アプリケーション自体をコーディングする前に長いセットアップが必要ですが、開発環境を透明化するために必要なことです。

プロジェクト構成ディレクトリ

npmの代わりにpnpmを使うのは、pnpmがストレージ効率がよく、ワークスペースに対応しているからです。サーバーとクライアントのコードを保持するmonorepoを作成します。

pnpmを有効にするには、Node.jsのcorepackを使用します。

corepack prepare pnpm@7.30.0 --activate

なぜmonorepoなのでしょうか。

将来の開発に有利だからです。もし、別の協力者を追加したい場合、誰もが同じコードベースで作業することになります。

プロジェクトを開始するには、monorepo用のディレクトリを作成します。

mkdir world-population
cd world-population
pnpm init

次に、プロジェクトのコードを格納する packages ディレクトリを作成します。サーバコードは data-server ディレクトリに、クライアントコードは client ディレクトリに格納します。

mkdir packages
mkdir packages/data-server
mkdir packages/client

pnpmpnpm-workspaces.yaml から設定を読み込んでプロジェクトのワークスペースを扱うので、それを作成して以下の内容を記入する必要があります。

# pnpm-workspaces.yaml
packages:
  # all packages in direct subdirs of packages/
  - "packages/*

このツリー構造は、クライアントとサーバーのコードベースが分かれているNode.jsプロジェクトに典型的に見られるかもしれません。monorepo内のパッケージとして整理され、pnpmパッケージマネージャで管理されます。

.
├── .gitignore
├── package.json
├── packages
│   ├── data-server
│   └── client
└── pnpm-workspaces.yaml

さっそくコーディングしてみましょう

まず、サーバープロジェクトを初期化し、主要なnpmパッケージをインストールします。

cd packages/data-server
pnpm init
cd ../../
pnpm --filter data-server install griddb-node-api express ws puppeteer

データ抽出

このプロジェクトでは、Worldometersのデータを使用しています。

Worldometersは、世界の人口、政府と経済、社会とメディア、環境、食料、水、エネルギー、健康など、さまざまなトピックについてリアルタイムで統計を提供するウェブサイトです。

ウェブサイトからデータを取得するためには、いくつかの方法があります:

  1. APIを利用する
  2. スクレイピング

残念ながら、WorldometersはAPIを提供していないため、最後の選択肢はウェブサイトをスクレイピングすることです。

注意点としては、Worldometersは動的なデータ、つまりリアルタイムのデータを提供していることです。CheerioのようなJavaScriptライブラリをデータ抽出に使用することはできません。このような動的なコンテンツを取得するには、Puppeteerを使用するのがベストな選択です。

Puppeteerは、ウェブページをレンダリングするためにヘッドレスブラウザインスタンスを起動するため、よりリソースを消費する可能性があります。しかし、動的コンテンツやユーザーインタラクションの処理など、より多くの機能を提供することができます。

2つのエンドポイントURLからデータが提供されます。ひとつは世界の総人口、もうひとつは国別の世界総人口です。

  • https://www.worldometers.info/world-population/
  • https://www.worldometers.info/world-population/population-by-country/
const worldPopDataSource = "https://www.worldometers.info/world-population/";
/**
 * fetch world population data
 */
const fetchWorldPopulationData = async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto(worldPopDataSource);

  const extractData = async () => {
    // Extract data from the page
    const data = await page.evaluate(() => {
      const populationElement = document.querySelector(".rts-counter");
      const population = populationElement.textContent.replace(/[\n\s]+/g, "");
      return { population };
    });

    return data;
  };

  const popData = await extractData();
  await browser.close();

  return popData;
};

fetchWorldPopulationData関数が返すデータは、単なるJavaScriptオブジェクトです。

{
  type: 'worldPopulationData',
  worldPopulation: {
    population: '8,022,758,957',
    timestamp: 1679222700112
  }
}

国別の世界人口をデータ抽出するコードは、リポジトリプロジェクトにあります。

出力は配列で、キーは次のとおりです:country coordinate population をキーとする配列が出力されます。

データストア

世界の人口データを保存するために、時系列コンテナを使用することにします。他のデータベースと同様に、まずデータベースに接続する必要があります。GridDBでは、クラスタに接続する必要があります。

クラスタの構成は、以下のファイルから確認できます。

/var/lib/gridstore/conf/gs_cluster.json

そして、そこにいくつかのポートがあるので、コードに入れたポートがトランザクションキーの値であることを確認します。

const initStore = async () => {
  const factory = griddb.StoreFactory.getInstance();
  try {
    // Connect to GridDB Cluster
    const store = await factory.getStore({
      host: "127.0.0.1",
      port: 10001,
      clusterName: "myCluster",
      username: "admin",
      password: "admin",
    });
    return store;
  } catch (e) {
    throw e;
  }
};

その後、データを格納するためのコンテナを作成する必要があります。initContainer()関数は、コンテナのカラム名やデータ型など、コンテナのスキーマに関する情報を提供します。

function initContainer() {
  const conInfo = new griddb.ContainerInfo({
    name: containerName,
    columnInfoList: [
      ["timestamp", griddb.Type.TIMESTAMP],
      ["value", griddb.Type.DOUBLE],
    ],
    type: griddb.ContainerType.TIME_SERIES,
  });

  return conInfo;
}

なお、initContainer()関数はコンテナを作成しません。コンテナを作成するには、GridDB APIが提供する putContainer() 関数を使用します。

async function createContainer(store, conInfo) {
  try {
    const timeSeriesDB = await store.putContainer(conInfo);
    return timeSeriesDB;
  } catch (err) {
    console.error(err);
    throw err;
  }
}

データの保存方法

データの保存はとても簡単です。まず、コンテナ接続を取得し、次に put() を使ってデータをGridDBコンテナに保存します。

db.put(data);

dbcreateConnection() 関数から返される接続参照です。

このプロジェクトで注意すべき重要な点は、データを定期的に保存することです。これを行う最も簡単な方法は、JavaScriptのネイティブ関数である setInterval() を使用することです。

function updateClientsWithWorldPopulationDataPeriodically(clients) {
  updateClientsWithWorldPopulationData(clients);
  setTimeout(
    () => updateClientsWithWorldPopulationDataPeriodically(clients),
    worldDataUpdateTime
  );
}

// Call the function to start the periodic updates
updateClientsWithWorldPopulationDataPeriodically(wss.clients);

データ抽出のデフォルト時間は、変数 worldDataUpdateTime で設定されており、5秒です。つまり、5秒ごとにデータを更新していることになります。

GridDBからデータを読み込む

GridDBはSQLをサポートしているので、生のSQLを使用してデータを取得することができます。query()fetch()を使うことで、SQLクエリに基づいたデータを簡単に取得することができます。

async function queryAll(db) {
  const query = db.query(
    `SELECT * FROM ${containerName} ORDER BY timestamp DESC LIMIT 1`
  );

  try {
    const rowset = await query.fetch();
    const results = [];

    while (rowset.hasNext()) {
      const row = rowset.next();
      const rowData = { timestamp: `${row[0]}`, population: row[1] };
      results.push(rowData);
    }

    return results;
  } catch (err) {
    console.log(err);
    throw err;
  }
}

プロジェクトの目的のためには、最新のデータを最新のタイムスタンプで表示する必要があります。これを実現するためにSQLクエリを使うことができます。

sql
SELECT * FROM [containerName] ORDER BY timestamp DESC LIMIT 1

ウェブクライアントにデータを配信する

ReactとMapboxで構築されたプロジェクトのWebクライアントにデータを配信するためにWebSocketを使用します。

なぜ WebSocket を使うのか?

WebSocketは、クライアントとサーバー間のリアルタイムな双方向通信を可能にするため、集中的なデータ利用を必要とするアプリケーションに最適な選択です。WebSocketは効率的なデータ転送を可能にし、待ち時間を最小限に抑えることができます。これは、継続的な更新と迅速な応答が重要なデータ集約型アプリケーションでは不可欠です。

このスニペットコードでは、世界人口のデータをGridDBに保存して照会し、WebSocket wsを介してWebクライアントに送信している様子を示しています。

const worldPopData = [worldPopulation.timestamp, worldPopulation.population];
await GridDB.insert(worldPopData, timeSeriesDb);

const result = await GridDB.queryAll(timeSeriesDb);

const jsonArray = result.map((item) => {
  return {
    timestamp: item.timestamp.toString(),
    population: item.population,
  };
});

// client is WebSocket client
clients.forEach((client) => {
  if (client.readyState === WebSocket.OPEN) {
    try {
      client.send(JSON.stringify(jsonArray));
    } catch (error) {
      console.error("Error sending data to client:", error);
    }
  }
});

今回は、WebSocketにws、HTTPサーバーにexpress.jsを使用しています。

ウェブクライアント React + Mapbox GL JS

現在、Reactベースのアプリケーションを作成するためのツールは非常に多く存在します。Viteは、次世代フロントエンドツールです。強化されたスピードを提供し、ESMをサポートし、Reactをサポートします。傑出した選択肢になります。

Viteを使って新しいフロントエンドプロジェクトを作成します。

pnpm create vite
cd client
pnpm install

コマンドはいくつかの質問をするので、JavaScriptとReactを選択することを確認してください。Webクライアントのソースコードは、packages/clientというフォルダの中に生きています。

クライアント側のプロジェクト構成です。

.
├── index.html
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── public
│   └── vite.svg
├── src
│   ├── App.css
│   ├── App.jsx
│   ├── assets
│   │   └── react.svg
│   ├── index.css
│   └── main.jsx
└── vite.config.js

Viteで作成したファイルやディレクトリは、App.jsxindex.cssファイル以外はあまり変更することはありません。

App.jsxはMapbox用のreactコンポーネントとUIをコーディングする場所です。コンポーネントは2つだけです。一つは世界の総人口を表示するための sidebar で、もう一つは世界地図を表示して各国の総人口を表示するための map-container です。

<div className="App">
  <div className="sidebar">World Population: {worldPopulation}</div>
  <div ref="{mapContainer}" className="map-container" />
</div>

Mapbox GL JS

Mapbox GL JS は、ウェブ上のベクターマップ用の JavaScript ライブラリです。そのパフォーマンス、リアルタイムのスタイリング、インタラクティブな機能は、高速で没入感のあるウェブマップを構築するすべての人のためのバーを設定します。

Mapbox GL JS をクライアントサイドアプリケーションに追加します。

pnpm install mapbox-gl

最近のブラウザはすでにWebSocketをネイティブにサポートしているので、WebSocketサーバーに接続するための追加パッケージは必要ありません。

const ws = new WebSocket("ws://localhost:3000");
setSocket(ws);

また、世界人口のデータを取得するには、messageイベントをリッスンするだけです。

ws.addEventListener("message", (event) => {
  const data = JSON.parse(event.data);

  if (data[0].country) {
    console.log("country", data);
    updateLabels(data);
  } else {
    setWorldPopulation(data[0].population);
  }
});

updateLabels(data) 関数は、世界人口上位10位までのすべての国のラベルを更新します。

React

Reactを使えば、簡単にコンポーネントを作ることができます。今回のプロジェクトでは、サイドバーの作成に使用します。

このUIは、世界の総人口を表示します。React の useState を使用すると、WebSocket からのデータ更新のたびにトリガーがかかり、サイドバーに新しいデータをレンダリングします。

const [worldPopulation, setWorldPopulation] = useState(0);

//...//

ws.addEventListener("message", (event) => {
  const data = JSON.parse(event.data);

  if (data[0].country) {
    console.log("country", data);
    updateLabels(data);
  } else {
    setWorldPopulation(data[0].population);
  }
});

概念的には、このプロジェクトは簡単そうに見えます。しかし、Mapbox、Node.js、GridDBを使用して世界の人口データを視覚化するWebアプリケーションを、リアルタイムのライブデータ統合とライブUI更新で実装することは、より複雑な課題を提示します。データの実世界的な性質とアプリケーションの動的な側面が何重にも複雑に絡み合い、効果的に実行するのは自明な課題ではありません。


  1. https://ubuntu.com/blog/ubuntu-wsl-enable-systemd 

  2. https://github.com/nodesource/distribution 

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *