Draw.io でコスト0のデジタルツインを作る


<details><summary>記事の前提：デジタルツインって何？</summary><div>

現実世界とリンクして、現実世界を再現するバーチャル空間のことです。
デジタルの双子（デジタルツイン）と呼ばれています。

ざっくりとしたジャンルを表す言葉ですので、同じ言葉でもまるで異なる技術のことを指します。

- **シミュレーションツールとしてのデジタルツイン**  
  目的：現実に起こっていないことをバーチャル空間で再現する  
   例：NVIDIA Omniverse, 災害シミュレータ
- **可視化ツールとしてのデジタルツイン**  
  目的：言語化が難しいものに 3D でラベル付けする  
   例：AWS TwinMaker, Azure Digital Twins
- などなど。ほかにもいろいろ

この記事では「デジタルツイン＝現実とリンクした3Dの地図コンテンツ」の意味で扱います。
可視化ツールとして、「言語化の難しいものへのラベル付け」を目的とします。
</div></details>

# 記事の概要

- Wiki や 5ch みたいに、誰もが簡単に編集できる3D地図

の作り方を紹介する記事です。

![pagetop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/52360780-daac-112a-4f5a-9386faf18180.png)

## 記事が扱う開発環境とライブラリ

- Draw.io
- three.js
- Cloudflare Workers

## 技術的なトピックを 3 行で

- コストをかけずにデジタルツインを公開・運用する
- 編集可能な Google Drive 上のファイルを、Web サイトからAjax参照する
- Draw.io から 3D モデル（three.js） に変換するツールを筆者が作ったので紹介

## デモサイトで触れる？

以下のサイトで実際に動いているものに触ることができます。
Google Map と同じ操作方法で閲覧できます。

- 飛田の地図（飛田新地をDraw.ioのファイルで3D化したサイトです）  
  https://tobitamap.pages.dev

![compare.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/1f9749c9-caef-7d96-78ac-187a68c1b3b1.png)

- Githubに、編集用のページとプレビュー用のデモページのリンクを置いています。  
https://github.com/fuyoneko/dio2town-contents

# 何が嬉しいの？

## デジタルツインは何が嬉しいの？

デジタルツインのメリットに、『言語化できないもの』にラベル付けできることがあります。

![needle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/9e6e2e4b-d0e8-928b-b0b1-ce6cda3b2f20.png)

デジタルツインはデータを現実世界にひもづけて表現することで、名前がない、言葉で表現しにくい、名前を聞いてもピンとこないものに対して、ラベル付けやトピック付けをすることができます。

## それを Draw.io で導入できると何が嬉しいの？

Draw.io で更新できるようになることで、

- ツール不要で修正できるので、誰もがいつでも更新できる
- データストレージも開発環境も不要になるので、安くなる

ようになります。

## デジタルツインを Wiki 化できると何が嬉しいの？

オフィスが主体でデジタルツインをやるとしたら、

- 空調のデジタルツイン
- 会議中と人流のデジタルツイン

どうしても堅いプロジェクトになります。席替えやセンサーの入れ替えがあれば更新も手間がかかりますし、「毎回ダッシュボードを立ち上げるほど空調気にする？」があります。

Wiki なら、柔らかい情報をデジタルツインに書き込めます。

- 寒い座席、サーバの音がうるさい座席
- 推しのアニメの食玩が売っているコンビニの場所
- 会社の裏の自販機のラインナップ
- 明日休みますから飲み会の費用は〇〇さんに渡して、みたいなメモ書き

空調も人流に体感が入ることで双方向になりますし、使う人が見たい情報、見せたい情報、知りたい情報、比較的新鮮な情報が集まる場所になります。席替えをしてもすぐに更新できます。

# 構築してみる

構築の手順は以下の通りです。
- Draw.ioのファイルを共有する
  - Draw.io のファイルを作って、 Google Drive に保存する
  - ファイル設定の「保存時の圧縮」を無効にする
  - 誰でもブラウザ上で編集できるように公開する
  - Ajax で Web サイトからDraw.ioの取れるようにするようにする
- Draw.ioのファイルを3D化する
  - 読み込んだDraw.ioのファイルを、JavaScriptのライブラリで3D化する

以下のものが必要ですので、あらかじめ用意しておきます。

- Google のアカウント
- Cloudflare のアカウント

## 手順1：Draw.io のファイル共有する

**[1：Draw.io のファイルを作って、 Google Drive に保存する]**

Draw.io （https://app.diagrams.net/）をブラウザで開きます。
ファイルの保存先を尋ねられるので、「Google ドライブ」を選択します。

![diagrams-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/126353d3-ba90-d34c-a5cb-1f81c139f49d.png)

Draw.io（diagrams.net）からGoogle ドライブへのアクセス権限を求められた場合は、3つともにチェックをつけて、Draw.ioからのアクセスを許可します。

![diagrams-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/f722b595-361c-e383-68db-8e8cd0c84ece.png)

**[2：ファイル設定の「保存時の圧縮」を無効にする]**

初期設定では「属性」→「圧縮」が有効になっているのですが、これを無効にします。
「圧縮」のチェックを外すと保存時の圧縮が無効になります。

![uncheck-element.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/b3f3ca9a-9bd0-4105-7513-35ac6346a8ff.png)

**[3：誰でもブラウザ上で編集できるようにする]**

右上にある「共有」ボタンをクリックします。

![share.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/5fc69f84-b7de-9744-e86d-818efdde6c7b.png)

ダイアログが出たら、「リンクを知っている全員」に対して「編集者」の権限を設定します。
「リンクをコピー」をクリックして、URLをコピーします。

![share-dialog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/7096d77a-2dfd-4121-df47-1dc4e83920bc.png)

コピーしたURLは`(https)://drive.google.com/file/d/${ファイルID}/view?usp=sharing`のようなフォーマットになっているのですが、末尾が`/view`になっています。これを`/edit`に置き換えた`(https)://drive.google.com/file/d/${ファイルID}/edit?usp=sharing`が編集用のURLになります。

編集用のURLを開くと、誰でもブラウザ上でDraw.ioファイルを編集することができますし、変更した内容はGoogle Driveに即時で反映されます。

> この${ファイルID}は後で繰り返し出てくるため、コピーして手元のテキストエディタに置いておきます。

**[4：Ajax で Web サイトから取れるようにする]**

編集用のURLはあくまでプレビュー画面のURLですので、AjaxでWebアプリから参照するには直接のURLが必要です。直接参照するURL は`https://drive.google.com/uc?export=download&id=${ファイルID}`になります。

ただ、これをAjaxでウェブサイトから取得しようとするとエラーが出てしまいます。ドメインが違うためです。GoogleはこのURLから別のドメインにリダイレクトをかけているため、CloudfrontやAPI Gatewayの下にGoogle Driveを置くだけでは回避できません。

昔は CORS Anywhere（https://github.com/Rob--W/cors-anywhere ）で簡単に解決できたのですが、悪用が多かったらしく、今ではサーバ側から厳しい制限がかけられています。Herokuも有料化したので、別の方法で対応します。

CORS Anywhereと同じものをCloudflare Workersの無料プランで自作します。
Cloudflare Workers（https://www.cloudflare.com/ja-jp/ ）の「サービスを作成」で「HTTPハンドラ」のサービスを1つ作ります。

![workers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/7376e3da-d345-8d5f-efd9-b588d616f9e6.png)

作ったサービスを選んだら、右上の「クイック編集」のボタンを押します。

![quick-edit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/ca6f79d0-50ad-d04c-1d9e-440a071d2ff7.png)

ソースコードをブラウザ上で編集できます。

![worker-contents.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/7e60672f-8761-7910-a081-c0404c866fb2.png)

ソースコードに以下のソースをコピペして、デプロイします。

```javascript:CORS anywhareの簡単版
addEventListener("fetch", (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);
  const id = url.pathname.replace("/", "");
  const corsHeaders = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET,OPTIONS",
  };
  const url_response = await fetch(
    "https://drive.google.com/uc?export=download&id=" + id
  );
  const body = await url_response.text();
  return new Response(body, {
    headers: corsHeaders,
  });
}
```

エディタの右側に検証ツールがあるので、URLの後ろに${ファイルID}をつけてリクエストします。

リクエスト先のURLはこんな感じになるはずです。
`https://${workerの関数名}.${プロジェクト名}.workers.dev/${ファイルID}`

リクエストに対して、xml形式のDraw.ioデータが返ってきたらOKです

> Cloudflare Workers の無料プランには、
>
> - 実行時間（CPU runtime）が 10ms 以内であること
> - 1 日あたり 10 万リクエスト以内であること
>
> の制限があります。
> ピンとこない表現ですが、CPU runtime はプロセスの待ち時間（例：Google Driveに対してfetchしている間の通信時間など）を除いた時間になります。上記のソースなら 1ms 程度です。
> ですので、このために有料プランを導入する必要はありません。

## 手順2：Draw.io で 3D に必要なデータを設定する

Draw.io は、座標や形、ラベルの文字列のほかに、自由なプロパティを持つことができます
※ショートカットはctrl+Mになります

![property-drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/15bfe657-ba79-b6e2-cd8f-e3a9644ab888.png)

Rectangle で好きな形を作成したら、プロパティに「type: x-building」を設定します
このデータをあとのライブラリで読み込むと、建物として立体化されます

![property-building.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194295/46904804-507c-42b3-d771-f4d263c715e1.png)

ライブラリが読み込めるプロパティの例です

| プロパティの値     | できるもの               |
| :------------- | :----------------------- |
| x-building   | 建物                     |
| x-street     | 道                       |
| street-label | 道の上に表示するテキスト |

## 手順3：取得した Draw.io のファイルを、3D化して表示する

以下のサンプルプロジェクトを参考にしてください。

【githubのプロジェクト】
https://github.com/fuyoneko/dio2town-contents

【index.htmlのソース】
https://github.com/fuyoneko/dio2town-contents/blob/master/docs/tobitamap/index.html

Draw.io を 3D に変換する Javascript のソースは以下の通りです。

ライブラリの読み込みをヘッダに書き込みます。

```html:HTMLへの導入
<script
  src="https://cdn.jsdelivr.net/gh/fuyoneko/dio2town@v0.0.1/_bundles/dio-2-town-map.js"
  crossorigin="anonymous"
></script>
```

地図の表示場所にDIVタグを置きます。

```html:HTMLのbodyには以下のタグを追加します
<!-- このDIVタグが3D地図に置き換わります。大きさは自由です -->
<div id="main" style="height: 100vh; width: 100vw"></div>
```

スクリプトで読み込みをします。

```html:Draw.ioファイルの読み込み
<script>
  window.onload = function () {
    // カメラ、レンダラを作成して、mainタグの場所にマッピングする
    const core = new Dio2TownMap.Core(document.getElementById("main"));
    // DrawIoからデータを取得する
    new Dio2TownMap.DioParser()
      .parse({
        dioFile:
          "${先の手順で作ったCloudflare WorkersへのURL}", // Draw.ioファイルを取得する
        polygonFile: "./polygon.json", // 空のJSONファイルへのパス
      })
      .then((dataList) => {
        // Dioデータから3D画面を作成する
        new Dio2TownMap.MainField().basement(
          core.scene, // シーンオブジェクトを連携する
          dataList, // Dioデータを連携する
          (started, dataCount) => {} // オブジェクト化の完了通知
        );

        // 光源を初期化
        new Dio2TownMap.Light(core.scene);
      });
  };
</script>
```

既存のホームページに埋め込んでもいいですし、S3 や Cloudflare pages で公開してもいいですし、React や Vue に埋め込むのもよいと思います。

サンプルソースにはオブジェクトへのクリック操作を拾うソースも書いています。MainField がオブジェクトを持っているので、JavaScript で参照して操作することもできます。

# これまでの展開と今後の展開

ここからは技術的な話を離れて雑記になります。

「Draw.io を 3D にするツール」は、飛田の地図のコア部分を切り出したものですので、「このツールで現状どこまでできるの？」は、飛田の地図を見ていただければと思います。
https://tobitamap.pages.dev

実装済みや一部実装した機能：

- 建物をタップすると詳細を表示する
- カメラ位置が下がると現実に近い表現（GLBデータの表示）になる
- ルートに沿ってカメラが移動して、道案内をする
- QRコードでカメラの状態を共有する

これらについてはドキュメントを整備します。

実装予定の機能：

- 現実の雨に連動して、デジタルツイン空間内でも雨が降る
- 「building」以外のプロパティ（「room」やデータ表示のオブジェクト）を整備する

これらについては引き続き対応をと考えています

## まとめとか

コストゼロ、編集もブラウザだけで完結するデジタルツインの紹介でした。

画面をぐりぐりと操作してみて、

- 3D の地図を、予算が用意できない商店街や学園祭でやったら？
- QRコードのカメラ情報共有を、GPSの精度が下がる地下街やドーム球場でやったら？
- iPhoneで3Dスキャンしたモデルをデジタルツイン上に表示したら？

「きっと楽しくなるだろう」「きっと便利になるだろう」と思ってもらえるのなら、色々な会社が出しているデジタルツインのサービスも含めて検討して、ぜひ導入してみてはと思います。