ラーニングパス「Azure Digital Twins と Unity を使用して Mixed Reality デジタル ツインを構築する」にAzure Mapsをアドオンしてみよう

#　Microsoft Build 2021 MVPパーソナルスポンサー企画でサンプルを作りました。

先日から続いているMicrosoft Build 2021で紹介されたDigital Twinsを実現するためのAzure関連サービス。今回はAzure Mapsの話です。今回も題材はラーニングパス「[Azure Digital Twins と Unity を使用して Mixed Reality デジタル ツインを構築する](https://docs.microsoft.com/ja-jp/learn/paths/build-mixed-reality-azure-digital-twins-unity/)」です。
Build 2021イベントの中で紹介されていたはDigital Twinsに関するいくつかのサービスをMixed Realityを含めて体験できる学習コンテンツです。
ラーニングパスで利用されているサービス部分の解説は以下のQiita記事にも整理していますので興味がある方はぜひご覧ください。

* Digital Twins, Mixed Realityのラーニングパス解説（Azure IoT Hub）
[Microsoft Learn 「Azure Digital Twins と Unity を使用して Mixed Reality デジタルツインを構築する」解説(Azure IoT Hub編) - Qiita](https://qiita.com/miyaura/items/dc78983d7094038e9f29)

*  Digital Twins, Mixed Realityのラーニングパス解説（Azure Digital Twins）
[Microsoft Learn 「Azure Digital Twins と Unity を使用して Mixed Reality デジタルツインを構築する」解説(Azure Digital Twins編) - Qiita](https://qiita.com/miyaura/items/14812caf737afd48a460)

Microsoft Build 2021で紹介されたDigital Twinsに全体については以下に整理しています。

* Microsoft Build 2021で紹介されたDigital Twins, Mixed Reality, Metaverseの全体概要  
[Microsoft Build 2021で話があったMixed Realityに絡むDigital Tiwnsについての技術要約 - Qiita](https://qiita.com/miyaura/items/dcd76fc796d8974fd099)

今回はこのラーニングパスをアドオンする形で、Build 2021で紹介されたMetaverse Technology stackのうちの1つ「Azure Maps(Indoor)」を利用するサンプルを構築しました。コードや簡単な説明は以下のGitHubのReadMe.mdにも記載していますが実際にどの様にデータ連携をしているのかをAzure Mapsのサービスと合わせて紹介したいと思います。


## Azure Mapsについて

Azure MapsはいわゆるBing Map等の地図サービスを拡張した地理空間合サービスAPIという位置づけで提供されています。

https://azure.microsoft.com/ja-jp/services/azure-maps/#azuremaps-features

Digital Twinsの中でAzure Mapsは「Monitor」としての役割で定義されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/0c30bb8c-62bc-5600-b104-1649828d9cd9.png)

Azure Mapsは地図空間サービスとして非常に多くの機能を持っておりなかなか興味深いサービスです。

| サービス | 概要 |
|:-|:-|
|検索|地図サービスでよく使う検索機能とほぼ同じものが提供されています。マップ上の位置をレンダリングしたり、住所をジオコーディング (リバース ジオコーディング) して対応する緯度/経度の座標を取得が可能です。|
|トラフィック|渋滞を避けるための複数の代替経路等、移動時間に関する分析情報を提供することが可能です。|
|マップ|Web SDK と Android SDK を使用して Web アプリとモバイル アプリを容易に構築できます。今回のサンプルはWeb SDKを活用して可視化しています。|
|ルーティング|これも最近の地図サービスにあるルート検索機能です。移動手段や経路探索ルール等を指定して経路計算を最適化する事ができます。|
|タイム ゾーン|世界中のあらゆる場所の時間をユーザーが簡単に表示できる機能です。|
|標高|地表の任意の場所の標高データを調べることができます。Azure Maps API を介して情報を取得可能です。|
|空間演算|ジオフェンシング、最接近点、大圏距離、バッファーなどの一般的な地理空間数学計算のライブラリを使用して、位置情報のインテリジェンスを強化します。|
|位置情報(プレビュー)|IP アドレスから国を検索することができ、ユーザーの位置情報に基づいてコンテンツとサービスをカスタマイズして提供することが可能になります。|
|Data Service(プレビュー)|空間演算や画像合成で使用する地理空間データをアップロードすることでキャッシュデータをあらかじめ構築、遅延を短縮し、生産性を向上ことが可能になります。|
|モビリティ(プレビュー)|停留所、経路情報、推定される移動時間など、周辺の公共交通機関サービスに関するリアルタイムの位置情報を提供します。|
|Weather Service|現在の気象条件、天気予報、ルートに沿った天気を取得すし、アプリケーション内で天気に基づいた決定ができるようになります。|
|Creator|Azure Maps やその他の Azure サービスと組み合わせて使用できるように、マッピングデータを構築することができます。今回のサンプルはこのCreatorの機能を利用してフロアマップをAzure Maps内に取込み可視化を行っています。|

今回はこの中でIndoor Mapを構築しています。この機能はAzure MapsのCreatorの機能で実現します。Indoor Mapはその名の通りでフロアマップなど屋内マップを管理することができます。

### Creatorサービスについて

Cretorサービスはマッピング用のデータをアドオンするための機能です。Azure Mapsに1つCreatorサービスを紐づけることができます。Creatorは一種のストレージとして機能しているため、作成するマッピングデータに応じて容量を定義します。（1unit= 100 MiB）
オリジナルのマッピングデータはCreatorサービスに登録し、その情報をAzure Maps経由で利用することでオリジナルのマッピングデータでも経路探索などが利用可能になります。

Creatorにオリジナルのマッピングデータを作成するには、Drawing パッケージを利用します。これはDWGファイル（平面CAD）と、構造物を定義したJsonファイルをZipで圧縮したものです。
このパッケージデータをアップロードしてDataSetに変換することでオリジナルのマッピングデータが生成されます。以下の図はデータの関係を模式化したものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/1778be47-771e-e1d8-b59a-75e1898b8ef7.png)


DataSetにはDWGパッケージで定義されたマッピングデータの構造物の情報が登録されておりAPIによるクエリ処理で情報を取得することが可能です。
今回のサンプルでは各風車情報を電源設備としてT98~T107、発電管理棟をContorol Room、送電ラインをL98~L107といった形で構造物として定義しています。これらの情報はStateSet等と合わせて利用し各構造物の状態を管理することが可能です。
TileSetはDwgファイルから生成された地図情報として利用可能なベクター情報です。Azure Mapsサービスを経由して可視化する情報として利用されます。
StateSetはDataSet上の各構造物に対しての状態定義を管理するためのデータです。例えば、風車の異常状態を地図上にマッピングする際に地図上の風車の構造物の色を変更する等の定義を行います。StateSetはDataSetに対して複数定義することが可能です。イメージとしては地図サービスによくある、「道路を可視化」「渋滞情報を可視化」といったように表示したい状態情報によって地図に反映する状態を切り替える機能がありますがあれと同じ事を実現することができます。





## 提供したサンプルコードについて

提供したサンプルコードはラーニングパス「[Azure Digital Twins と Unity を使用して Mixed Reality デジタル ツインを構築する](https://docs.microsoft.com/ja-jp/learn/paths/build-mixed-reality-azure-digital-twins-unity/)」を拡張して利用します。ラーニングパスでは風力発電装置に異常が発生した場合にHoloLens上でAlert表示になります。以下の様にタービンの上にマークがつきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/1c0e1a17-559d-eddf-7549-4b6beb7987df.png)

今回サンプルとしてAzure Mapsを追加する際にはこの風力発電プラントを屋内マップとして地図を作成しました。
この地図情報は状態を持っており、装置に異常が発生した場合にマップ上のタービンが赤くなるというものです。

![AzureMaps2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/07dec38e-2d43-2540-a491-4707f3b74133.gif)

この赤くなる部分はAzure Digital Twinsの情報更新に連動してAzure Mapsで状態を変更するように実装しています。
サンプルなどは以下に公開しているので、興味があればぜひラーニングパスの実施後にアドオン実装をしてみてください。

https://github.com/TakahiroMiyaura/AzureMapsIndoorDataSamples

### アーキテクチャ

ラーニングパスに今回のサンプルコードを加えた結果のアーキテクチャは以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/b2f2c03f-51bd-6e16-6824-ea674483ecd7.png)

ラーニングパスから追加されているのは赤枠の部分でAzure Digital TwinsでAlert情報が変更された場合にAzure Mapsの該当するタービンの状態を変更する処理をAzure Functionsで構成しています。Azure Mapsで更新された情報を別のPCのブラウザ上からAzure Maps Web SDKを使って表示しています。

## サンプルコード解説

次に今回作成したサンプルコードの構成について紹介します。
サンプルコードは大きくは3つに分かれています。

* [ARMテンプレート](https://github.com/TakahiroMiyaura/AzureMapsIndoorDataSamples/tree/main/ARM-Template)  
ラーニングパスにAzure Mapsを利用できるようにアドオンするためのサービスを自動展開するArmテンプレートです。
* [Dwgパッケージファイルアップロードアプリ](https://github.com/TakahiroMiyaura/AzureMapsIndoorDataSamples/tree/main/IndoorDataUploader)  
CreatorにDWGパッケージファイルをアップロードするコンソールアプリケーションです。Indoor Mapの機能はまだプレビューということもありCreatorに登録する手順がAPIを複数回叩いて登録する形になります。今回はこの設定を簡易化するためにDWGパッケージをアップロードしDataSetを生成、風車の異常状態に応じたStateSetを登録する処理をコンソールアプリとして構築しました。実際のCreatorへの登録手順は[公式ドキュメント - チュートリアル:Creator (プレビュー) を使用して屋内マップを作成する](https://docs.microsoft.com/ja-jp/azure/azure-maps/tutorial-creator-indoor-maps)の通りです。コンソールアプリはこの手順の上から順に処理を実行するようになっています。
* [Azure Digital TwinsからAzure Mapsの情報更新用関数](https://github.com/TakahiroMiyaura/AzureMapsIndoorDataSamples/tree/main/ARM-Template/functions)  
実際にAzure Digital Twinsに通知された風車の異常状態をトリガーとしてAzure MapsのAPIを経由して該当する風車の構造物の状態を変更する処理を実行します。

なお、[公式ドキュメント - チュートリアル:Creator (プレビュー) を使用して屋内マップを作成する](https://docs.microsoft.com/ja-jp/azure/azure-maps/tutorial-creator-indoor-maps)の日本語ドキュメントは各手順で実行するAPIのURLが間違っています。チュートリアルの手順には「atlas~」と記述があるのですが、実際には「[geometory].atlas.~」となります。[geometory]部分はデプロイしたAzure Mapsのリージョンに相当します。現状は「us」を設定することになります。

### DWGパッケージファイルとStateSetについて

今回ラーニングパスに合わせて風車の発電プラントを模した屋内マップを構築しています。このためにDWGパッケージファイルと風車の異常状態をマップ上に可視化できるようにStateSetも用意しているのでこのフォーマット等の情報を紹介します。

#### DWGパッケージファイル

[サンプルで用意したDWGパッケージファイル](https://github.com/TakahiroMiyaura/AzureMapsIndoorDataSamples/blob/main/IndoorDataUploader/Resources/Plant.zip)

DWGパッケージファイルはZipファイルで圧縮されたファイルで中身は1つ以上のDWGファイルとJson形式のManifestファイルが含まれています。ビルなど複数階層の構造物の場合はDWGが階層分必要になります。
DWGファイルはAutoCADの標準形式の2CADデータです。平面図によってフロアマップなどを作成していきます。作成する際にCADデータはある程度ルールに従って作成する必要があります。詳細は[公式ドキュメント - Drawing パッケージの要件](https://docs.microsoft.com/ja-jp/azure/azure-maps/drawing-requirements)を参照してください。
manifest.jsonにはDWGと構造物の情報をマッピングのための定義が入っています。
今回の風車については以下のような定義にしました。定義の情報についても[公式ドキュメント - Drawing パッケージの要件](https://docs.microsoft.com/ja-jp/azure/azure-maps/drawing-requirements)に記載されています。
今回は風力発電プラントを想定し、建物は1フロア地上という想定です。構造物は制御室(1)、風車(10)、送電ライン(10)をそれぞれ定義しています。ラーニングパスではT102が異常状態となるようにつくれられているので異常情報が通知されると、T102に対してStateSetで定義された条件で地図上の状態が変化します。

```json:manigfest.json
{
    "version": "1.1",
    "directoryInfo": {
        "name": "Contoso Plant 1",
        "streetAddress": "Contoso Way",
        "unit": "1",
        "locality": "Eastside",
        "postalCode": "00000",
        "adminDivisions": [
            "Contoso City",
            "Contoso State",
            "United States"
        ],
        "hoursOfOperation": "Mo-Fr 08:00-17:00 open",
        "phone": "1 (425) 555-1234",
        "website": "www.contoso.com",
        "nonPublic": false,
        "anchorLatitude": 47.636152,
        "anchorLongitude": -122.132600,
        "anchorHeightAboveSeaLevel": 1000,
        "defaultLevelVerticalExtent": 1
    },
    "buildingLevels": {
        "levels": [
            {
                "levelName": "Ground",
                "ordinal": 0,
                "verticalExtent": 5,
                "filename": "./Plant.dwg"
            }
        ]
    },
    "georeference": {
        "lat": 47.636152,
        "lon": -122.132600,
        "angle": 0
    },
    "dwgLayers": {
        "exterior": [
            "Exterior"
        ],
        "unit": [
            "Units"
        ],
        "wall": [
            "Walls"
        ],
        "door": [
            "Doors"
        ],
        "unitLabel": [
            "UnitLabels"
        ],
        "zone": [
            "Zones"
        ],
        "zoneLabel": [
            "ZoneLabels"
        ]
    },
    "unitProperties": [
        {
            "unitName": "T98",
            "categoryName": "room.supply",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "T99",
            "categoryName": "room.supply",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "T100",
            "categoryName": "room.supply",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "T101",
            "categoryName": "room.supply",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "T102",
            "categoryName": "room.supply",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "T103",
            "categoryName": "room.supply",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "T104",
            "categoryName": "room.supply",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "T105",
            "categoryName": "room.supply",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "T106",
            "categoryName": "room.supply",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "T107",
            "categoryName": "room.supply",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "L98",
            "categoryName": "room.utility.cable",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "L99",
            "categoryName": "room.utility.cable",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "L100",
            "categoryName": "room.utility.cable",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "L101",
            "categoryName": "room.utility.cable",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "L102",
            "categoryName": "room.utility.cable",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "L103",
            "categoryName": "room.utility.cable",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "L104",
            "categoryName": "room.utility.cable",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "L105",
            "categoryName": "room.utility.cable",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "L106",
            "categoryName": "room.utility.cable",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "L107",
            "categoryName": "room.utility.cable",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        },
        {
            "unitName": "Control Room",
            "categoryName": "room.office",
            "navigableBy": ["pedestrian", "wheelchair", "machine"],
        }
    ],
    "zoneProperties":
    [        
    ]
}
```

#### StateSet

StateSetは構造物に対して地図上での状態を変更するための変更セットを管理するものです。

[サンプルで提供したStateSet.json](https://github.com/TakahiroMiyaura/AzureMapsIndoorDataSamples/blob/main/IndoorDataUploader/Resources/StateSet.json)

作成したStateSet.jsonには風車の異常状態時に構造物の色を正常時：緑、異常時：赤となるように定義しています。

```json:stateSet.json(抜粋)
{
	"styles": [
		{
			"keyname": "alert",
			"type": "boolean",
			"rules": [
				{
					"true": "#FF0000",
					"false": "#00FF00"
				}
			]
		},
```

DWGパッケージからDataSet、TileSetを作成し、StateSet.jsonを紐づけることで、これらの情報をリンクさせることが可能になります。
例えば、風車の異常状態の際に色を変える場合はDataSetに登録されている構造物の中から、異常の発生した風車のID（T102）に対してalertキーの値をfalseで登録します。
Azure Mapsで情報を可視化する場合には地図情報としてのTileSetと重ね合わせるStateSetのIDを割り当てることで、TileSet上の風車T102の色がStateSetのAlert情報に合わせて変更されるようになります。
他にも今回のサンプルで風車の情報として温度や回転数、発電量などの情報に対応する状態も定義しています。

##　まとめ

今回はBuild 2021で紹介されていたDigital TwinsのサービスのうちAzure Mapsの簡単な動作を確認するためのサンプルを構築しました。Azureを活用したDigital TwinsとMixed Realityが織りなす複合現実を活用するための技術スタックに関する検証として何かの参考になれば幸いです。