BIM/CIMデータのメタバース展示場を作ってみた　#1

# BIM/CIMデータのメタバース展示場を作ってみた　#1

みなさん、こんちにちは！  
この記事は[国交DPFアドベントカレンダー2023](https://qiita.com/advent-calendar/2023/mlit-dpf)に参加しています。本日は22日目です。  
今日は国土交通データプラットフォームから取得できるBIM/CIMデータ（IFC形式）を最近何かと流行っているメタバースに表示させて、仮想空間にBIM/CIMの展示場を作ってみようと思います。  
少し長くなりそうなので、2つの記事に分けて書きたいと思います。  

<br>

# 今回やりたいこと  
* 指定した期間の電子納品保管管理システムのBIM/CIMデータ(IFC形式)を取得
* 取得したBIM/CIMデータ（IFC形式）をOBJ形式に変換する（次回記事）
* メタバースに変換したBIM/CIMデータを投入して、展示場を作成する

<br>

まず、今日は事前準備として、国土交通データプラットフォームの利用者向けAPIを使って、電子納品保管管理システムのIFCデータをいくつか取得してみたいと思います。  
今回は、Javascriptを使って取得していきます。  
利用者向けAPIの利用には、実行環境の準備とAPIキーが必要になりますので以下の記事を参考に準備してみてください。  

<br>

### ・APIキーの取得方法については [こちら](https://qiita.com/dpf_hirometai/items/6a594e8dfe85da0bb499)  
### ・実行環境の準備については [こちら](https://qiita.com/dpf_hirometai/private/28ae44cd679bc69172d9)

<br>

## IFCをダウンロードするためのコードの解説  
まずはじめにIFCデータをダウンロードして、PCに保存していく手順を解説します。

<br>

```javascript
import https from "https";
import axios from "axios";
import fs from "fs";
import { mkdir } from "fs/promises";
import path from "path";

const END_POINT = "https://www.mlit-data.jp/api/v1/";
const API_KEY = "取得したAPIキー";

```  
まずは必要なライブラリのインポートとエンドポイント、APIキーの設定を行います。  

<br>

```javascript
async function GetData(date_start, date_end) {
  const pageSize = 1000;

  function MakeQueryString(pagefirst) {
    return `
    query{
      search(
        term: ""
        first: ${pagefirst}
        size: ${pageSize}
        attributeFilter: {
          AND: [
            { attributeName: "DPF:dataset_id", is: "cals_bimcim" },
            { attributeName: "DPF:fileformat", is: "IFC" },
            { attributeName: "DPF:completion_datetime", gte: "${date_start}", lt: "${date_end}" }
          ]
        }
        sortAttributeName: "DPF:completion_datetime"
        sortOrder: "asc"
      ) {
        totalNumber
        searchResults {
          id
          title
          files {
            id
            original_path
            sizeinbytes
          }
        }
      }
    }`;
  }

  const maximumDataNumber = 50;
  const result = [];
  let remainingResult = true; 
  let pagefirst = 0;
  while (remainingResult && result.length < maximumDataNumber) {
    const GraphQLQuery = MakeQueryString(pagefirst);
    const request = {
      url: END_POINT,
      method: "post",
      headers: {
        "Content-type": "application/json",
        apikey: API_KEY,
      },
      data: { query: GraphQLQuery },
    };
    await axios(request)
      .then((response) => {
        response.data.data.search.searchResults.forEach((data) => {
          result.push(data);
        });
        if (response.data.data.search.searchResults.length === pageSize)
          pagefirst += pageSize;
        else remainingResult = false;
      })
      .catch((error) => {
        console.error(error.message);
        if (error.response) console.debug("Error data:", error.response.data);
        remainingResult = false;
      });
  }
  return result;
}

```  
GetData関数を使って、取得するデータ範囲を指定します。  
今回対象は「IFC」データなので、クエリの部分では、attributeFilterを使って「cals_bimcim」と「IFC」を指定してあげます。
取得期間の開始日と終了日は最後にしてしますので、もう少し後で説明します。  
APIを呼び出して、結果の準備をここでしておきます！  

<br>

```javascript
async function CreateFolders(foldername) {
  try {
    if (!fs.existsSync(foldername)) {
      await mkdir(foldername, {
        recursive: true,
      });
    }
  } catch (error) {
    console.log(error);
  }
}

```  
今回ダウンロードするデータはお使いのPCローカルに保存しますので、CreateFolders関数を使って、フォルダを作成します。プログラムでは、指定のフォルダが「存在しなければ作成」ということにしています。  

<br>

```javascript
async function DownloadFile(URL, savepath) {
  const dirname = path.dirname(savepath);
  await CreateFolders(dirname);
  return new Promise((resolve, reject) => {
    try {
      const file = fs.createWriteStream(savepath);
      https.get(URL, function (response) {
        const { statusCode } = response;
        if (statusCode !== 200) {
          reject({ message: "Server response falls out of status=200" });
        } else {
          response.pipe(file);
          file.on("finish", () => {
            file.close();

            resolve(true);
          });
        }
      });
    } catch (error) {
      reject(error);
    }
  });
}

```  
DownloadFile関数を使用して、取得してきたダウンロード用URLからファイルをダウンロードします。ダウンロードしたファイルは指定されたパスに保存していきます。  

<br>

```javascript
async function GetFiles(year, month, savefolder) {
  const datalist = await GetData(year, month);
  let cnt = 0;

  for (let i = 0; i < datalist.length; i++) {
    const data = datalist[i];
    console.log("Checking file for:", data.title);
    if (data.files) {
      const filedata = data.files.find((file) => {
        return path.extname(file.original_path).toLowerCase() === ".ifc" &&
               file.sizeinbytes <= 10 * 1024 * 1024; // ファイルサイズが10MB以下のものに限定
      });

      // IFCファイルを見つけた場合にダウンロードを行う。
      if (filedata) {
        const GetFileDownloadURLQuery = `
          query {
            fileDownloadURLs(
              files:[{ id: "${filedata.id}", original_path: "${filedata.original_path}"}]
            ) {
              ID
              URL
            }
          }`;
        const request = {
          url: END_POINT,
          method: "post",
          headers: {
            "Content-type": "application/json",
            apikey: API_KEY,
          },
          data: { query: GetFileDownloadURLQuery },
        };

        // ダウンロード用URLを取得する。
        let URL = "";
        await axios(request)
          .then((response) => {
            if (
              response.data.data.fileDownloadURLs &&
              response.data.data.fileDownloadURLs.length === 1
            ) {
              URL = response.data.data.fileDownloadURLs[0].URL;
            }
          })
          .catch((error) => {
            console.error(error.message);
            if (error.response)
              console.debug(
                "Impossible to retreive file download URL.",
                error.response.data
              );
          });

        // ダウンロード用URLを取得した場合、実際にダウンロードする
        if (URL) {
          try {
            await DownloadFile(
              URL,
              path.join(savefolder, path.basename(filedata.original_path))
            );
            console.log("Download completed.");
            cnt++;
          } catch (error) {
            console.log("Error while downloading or saving file:", URL);
            console.log(error.message);
          }
        }
      } else {
        console.log("No suitable IFC file to download for:", data.title);
      }
    }
  }

  // ダウンロードしたファイル数のまとめを表示する。
  console.log(
    "Downloaded a total of",
    cnt,
    "files for",
    datalist.length,
    "construction work data."
  );
}

```  
指定した期間のデータを実際に取得しにいきます。  
この後、メタバースに載せるためにIFCデータをOBJ形式に変換するのですが、ファイルの容量が重いと変換に少し時間がかかってしまうので、今回は、サイズが10MB以下のファイルのみをダウンロードしようと思います。  
ちゃんと処理が実行されて完了されたかも確認したいので、コンソールへのログ出力は忘れません！  

<br>

```javascript
await GetFiles("開始日", "終了日", "保存先フォルダパス");
```  
最後にデータの取得期間として、開始日と終了日、保存先フォルダを指定してあげます。  
先ほどもお伝えしましたが、保存先フォルダはなければ、自動的に生成されますので準備は不要です！  

<br>

例えば、、
```javascript
await GetFiles("2022-03-01", "2022-03-15", "\IFC_2022-03");
```  
こんな感じに記載すれば、「2022-03-01」から「2022-03-15」までに登録されたIFCデータを「IFC_2022-03」フォルダに保存してください。  
といった具体になります。  

<br>

全体はこんな感じです。  

```javascript
import https from "https";
import axios from "axios";
import fs from "fs";
import { mkdir } from "fs/promises";
import path from "path";

const END_POINT = "https://www.mlit-data.jp/api/v1/";
const API_KEY = "取得したAPIキー";

async function GetData(date_start, date_end) {
  const pageSize = 1000;

  function MakeQueryString(pagefirst) {
    return `
    query{
      search(
        term: ""
        first: ${pagefirst}
        size: ${pageSize}
        attributeFilter: {
          AND: [
            { attributeName: "DPF:dataset_id", is: "cals_bimcim" },
            { attributeName: "DPF:fileformat", is: "IFC" },
            { attributeName: "DPF:completion_datetime", gte: "${date_start}", lt: "${date_end}" }
          ]
        }
        sortAttributeName: "DPF:completion_datetime"
        sortOrder: "asc"
      ) {
        totalNumber
        searchResults {
          id
          title
          files {
            id
            original_path
            sizeinbytes
          }
        }
      }
    }`;
  }

  
  const maximumDataNumber = 1000;
  const result = [];
  let remainingResult = true; 
  let pagefirst = 0;
  while (remainingResult && result.length < maximumDataNumber) {
    const GraphQLQuery = MakeQueryString(pagefirst);
    const request = {
      url: END_POINT,
      method: "post",
      headers: {
        "Content-type": "application/json",
        apikey: API_KEY,
      },
      data: { query: GraphQLQuery },
    };
    await axios(request)
      .then((response) => {
        response.data.data.search.searchResults.forEach((data) => {
          result.push(data);
        });
        if (response.data.data.search.searchResults.length === pageSize)
          pagefirst += pageSize;
        else remainingResult = false;
      })
      .catch((error) => {
        console.error(error.message);
        if (error.response) console.debug("Error data:", error.response.data);
        remainingResult = false;
      });
  }
  return result;
}

async function CreateFolders(foldername) {
  try {
    if (!fs.existsSync(foldername)) {
      await mkdir(foldername, {
        recursive: true,
      });
    }
  } catch (error) {
    console.log(error);
  }
}

async function DownloadFile(URL, savepath) {
  const dirname = path.dirname(savepath);
  await CreateFolders(dirname);
  return new Promise((resolve, reject) => {
    try {
      const file = fs.createWriteStream(savepath);
      https.get(URL, function (response) {
        const { statusCode } = response;
        if (statusCode !== 200) {
          reject({ message: "Server response falls out of status=200" });
        } else {
          response.pipe(file);
          file.on("finish", () => {
            file.close();

            resolve(true);
          });
        }
      });
    } catch (error) {
      reject(error);
    }
  });
}

async function GetFiles(year, month, savefolder) {
  const datalist = await GetData(year, month);
  let cnt = 0;

  for (let i = 0; i < datalist.length; i++) {
    const data = datalist[i];
    console.log("Checking file for:", data.title);
    if (data.files) {
      const filedata = data.files.find((file) => {
        return path.extname(file.original_path).toLowerCase() === ".ifc" &&
               file.sizeinbytes <= 10 * 1024 * 1024; // ファイルサイズが10MB以下
      });

      if (filedata) {
        const GetFileDownloadURLQuery = `
          query {
            fileDownloadURLs(
              files:[{ id: "${filedata.id}", original_path: "${filedata.original_path}"}]
            ) {
              ID
              URL
            }
          }`;
        const request = {
          url: END_POINT,
          method: "post",
          headers: {
            "Content-type": "application/json",
            apikey: API_KEY,
          },
          data: { query: GetFileDownloadURLQuery },
        };

        let URL = "";
        await axios(request)
          .then((response) => {
            if (
              response.data.data.fileDownloadURLs &&
              response.data.data.fileDownloadURLs.length === 1
            ) {
              URL = response.data.data.fileDownloadURLs[0].URL;
            }
          })
          .catch((error) => {
            console.error(error.message);
            if (error.response)
              console.debug(
                "Impossible to retreive file download URL.",
                error.response.data
              );
          });

        if (URL) {
          try {
            await DownloadFile(
              URL,
              path.join(savefolder, path.basename(filedata.original_path))
            );
            console.log("Download completed.");
            cnt++;
          } catch (error) {
            console.log("Error while downloading or saving file:", URL);
            console.log(error.message);
          }
        }
      } else {
        console.log("No suitable IFC file to download for:", data.title);
      }
    }
  }

  console.log(
    "Downloaded a total of",
    cnt,
    "files for",
    datalist.length,
    "construction work data."
  );
}

await GetFiles("開始日", "終了日", "保存先フォルダパス");

```  

## それでは実行してみましょう！  
Node.jsを使って実行してみます。  

<br>

Node.jsの実行環境が準備できたら、早速コマンドプロンプトを起動してみましょう。  
起動できたら、今回のサンプルコードを実行するディレクトリに移動していきましょう。  
今回はCドライブ直下に「project」というフォルダを作成して作成したスクリプトファイル「cals_ifc_download.js」を配置しました。  

<br>
移動できたら以下のコマンドを入力して・・・  
<br>

```cmd:cmd
C:\project>node cals_ifc_download.js
```  
<br>

<span style="font-size: 200%;">Enterキーをどうぞ！</span>  

<br>

```cmd:cmd
C:\project>node cals_download.js
Checking file for: 大沢野富山南道路（3号橋他）橋梁詳細設計業務
Download completed.
Checking file for: 令和3年度 来住高架橋外橋梁予備設計業務
Download completed.
Checking file for: 令和2年度飯田川橋詳細設計業務
Download completed.
///...
Downloaded a total of 12 files for 13 construction work data.
```  
こんな感じで表示されたら<span style="font-size: 200%;">成功です！</span>  
最終行には取得したファイルの総数もしっかり出ていますね！  
今回は2022年3月1日から2022年3月15日までの期間で取得してみました。  
総ファイル数は12ファイルダウンロードできたみたいです。  

<br>

保存先フォルダを確認してみます。  

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629590/f950aa7b-5470-1b4e-cf47-ca34116ae18d.png" width="500px">  

<br>

IFCデータ（10MB以下のもの！）がしっかりダウンロードされています！  
良かったです！  

# 本日はここまで 
本日はIFCデータのダウンロードまでを解説しました！  
次回は、今回ダウンロードしたIFCデータをメタバースに載せるための加工と実際に表示させるところまで説明したいと思います。  
それでは、引き続き国交DPFアドカレをお楽しみください！  
ありがとうございました！