グラフィカルな簡易監視画面の作成

# はじめに
WebアプリケーションサーバやDBの状態を監視するには[Grafana](https://grafana.com/ja/)の様なダッシュボードが使用されるケースが多いと思います。これで多くの場合問題ありませんが、工場設備やエレベータ、発送電、上下水等の監視にはもう少しグラフィカルな図付きの監視画面が使用されています。一般用途向けでも、ソーラーパネルの発電量を表示するシステムは、単に数値だけではなく図付きのポップな画面を良く見ます。今回はこの様なグラフィカルな監視画面をWebで作成してみました。

実現にはOSSである[WinterCardinal GraphicEditor](https://github.com/winter-cardinal/winter-cardinal-graphic-editor)を利用しています。また、作成したサンプルは[GitHub](https://github.com/winter-cardinal/winter-cardinal-starter)にプッシュしてあります。

本記事では、作成したサンプルをベースに、WinterCardinal GraphicEditorの使い方について紹介します。

# WinterCardinal GraphicEditorとは？

今回使用しているWinterCardinal GraphicEditorはブラウザ上で動作するグラフィックエディター兼ビューワーです。内部的にはWebGL、[PixiJS](https://pixijs.com/)、[WinterCardinal UI](https://github.com/winter-cardinal/winter-cardinal-ui)が使用されており、[InkScape](https://inkscape.org/ja/)や[Microsoft Visio](https://www.microsoft.com/ja-jp/microsoft-365/visio/flowchart-software)の様にグラフィックを作成するためのものです。ただしInkScapeやVisioが静的な絵を作成するものであるのに対し、GraphicEditorは電力量等の値に応じて動的に変化する絵を作成する事を目的としています。例えば電力量がある一定値を超えたら、文字を赤く大きくして警告すると言った表現が出来ます。この点ではPowerPointのアニメーションに似ています。PowerPointのアニメーションはクリックやページ遷移契機で動作するのに対し、GraphicEditorではセンサー値を契機として動作します。

InkScapeやVisioでも動きに関する情報を埋め込み、SVGとしてエクスポート、専用ビューワを開発する事で似たようなことは可能です。過去にはその様な事を行ってきましたが、どうしても無理や煩雑さが生じていました。またSVGゆえにグラフィックが大きくなるとロードに時間を要する問題もありました。GraphicEditorはそれ専用に設計されており、またSVGと比較し非常に軽量なファイルフォーマットとなっています。

またGraphicEditorはWeb上で動作するため、同じくWeb上で動作する監視システムであれば、GraphicEditorをシステムに組み込む事が可能です。これによって設備に変化が生じた時にもグラフィックをその場で変更して動かすと言う事が容易に出来るようになります。

# 作成したサンプルの概要

GitHubに置いたサンプルをREADMEに従ってビルド・起動すると、下記のような画面が表示されます。

![sign-in.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/4e9b5b62-5990-7e9d-002e-58467e8f5bb5.png)

ログイン後、左サイドバーのDiagramを選択すると下のような画面が表示されます。センサー値に応じて絵がアニメーションしますので、これを見ながら監視を行うという想定のサンプルになっています。

![diagram-with-side-menu.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/ee70d8eb-eb78-f187-2c16-747b3bb5234c.gif)

また、左サイドバーのGraphic Editorを選択し、`plant`と言う名前のファイルを読み込むと上のDiagram画面の元となったグラフィックが表示されます。

![graphic-editor-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/427ef5c4-40d5-78be-d080-0d21d92105dc.png)

このGraphic Editorで表示したいグラフィックを作成し、Diagram画面に切り替えて表示することによって、画面の作成・変更・確認をWebブラウザ上でグラフィカルかつシームレスに行えます。

上記サンプルでは[WinterCardinal GraphicEditor](https://github.com/winter-cardinal/winter-cardinal-graphic-editor)をホストするためのAPサーバとして[Spring Boot](https://spring.io/projects/spring-boot)と[WinterCardinal](https://github.com/winter-cardinal/winter-cardinal)、[H2 DB](https://www.h2database.com/html/main.html)を使用しています。アイコンは[Material Icons](https://fonts.google.com/icons)から拝借しています。

画面がブラウザ上に表示されるまでの流れは下記の通りです。

画面を最初に表示する時はブラウザ側から`plant`のグラフィックデータ要求を出し、H2 DBに入っているグラフィックデータを返して表示します。グラフィックの表示はGraphicEditorが行ってくれます。

![overall-step1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/2b83dc79-75ec-1e59-fc95-8e2aecf2daf9.png)

その後センサー値を収集し、GraphicEditorがグラフィックを更新する（＝数値の更新とタンク容量アニメーション）と言う流れになります。センサー値のプッシュにWinterCardinal（＝WebSocket）を使用していますが、更新頻度が低ければHTTPでポーリングしても良いかと思います。

![overall-step2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/e15cb991-a4c2-ceaa-044a-6ae9939a44c0.png)

グラフィックの作成・表示はGraphic Editorを使ってブラウザ上から行えますが、Graphic Editor自体のホスティングや、送られてくるセンサ値との連携などにはコーディング作業が必要となります。詳細はGitHubのサンプルコードをご覧いただければと思いますが、概要を次に説明します。

# Graphic Editor画面を実装する

まずグラフィックを作成・編集するためのGraphic Editor画面を実装します。MVCコントローラを新設し`/graphic-editor`にアクセスされたら`src\main\resources\templates\graphic-editor\main.html`を表示するように設定します。

```java:src\main\java\app\mvc\MvcGraphicEditorController.java
@Controller
public class MvcGraphicEditorController {
	@GetMapping("/graphic-editor")
	public String main() {
		return "graphic-editor/main";
	}
}
```

合わせてサイドメニューからGraphic Editorを選択すると`/graphic-editor`に遷移する様に設定します。

```diff_html:src\main\resources\templates\top\menu.html
-<button class="wcs-menu-item alert">
+<button class="wcs-menu-item alert" url="./graphic-editor">
	<th:block th:replace="~{image/editor}"></th:block>
	<span th:text="#{top.menu.graphic-editor}">Graphic Editor</span>
</button>
```

`src\main\resources\templates\graphic-editor\main.html`側ではWinterCardinalやPixiJSと言った必要なJavaScriptを読み込みます。

```diff_html:src\main\resources\templates\graphic-editor\main.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:with="title=#{graphic-editor.label}">
<head>
	<th:block th:insert="~{common/header}"></th:block>
	<th:block th:insert="~{common/banner::head (${title})}"></th:block>
</head>
<body>
	<th:block th:insert="~{common/banner::body (${title})}"></th:block>
	<th:block th:insert="~{common/csrf}"></th:block>
+	<script th:src="@{/webjars/wcardinal/wcardinal.worker.min.js}"></script>
	<script th:src="@{/graphic-controller}"></script>
+	<script th:src="@{/webjars/pixi.js/pixi.min.js}"></script>
+	<script th:src="@{/webjars/wcardinal-ui/wcardinal-ui.min.js}"></script>
+	<script th:src="@{|/webjars/wcardinal-ui/wcardinal-ui-theme-white-${#strings.toLowerCase(#locale.language)}-${#strings.toLowerCase(#locale.country)}.min.js|}"></script>
+	<script th:src="@{/webjars/wcardinal-geditor/wcardinal-geditor.min.js}"></script>
+	<script th:src="@{|/webjars/wcardinal-geditor/wcardinal-geditor-theme-default-${#strings.toLowerCase(#locale.language)}-${#strings.toLowerCase(#locale.country)}.min.js|}"></script>
+	<script th:src="@{|/asset/message-script/${product.version}/message-script-${#strings.toLowerCase(#locale.language)}-${#strings.toLowerCase(#locale.country)}.min.js|}"></script>
+	<script th:src="@{/webjars/wcardinal-starter/wcs-graphic-editor.min.js}"></script>
	<script>
	(function(){
		"use strict";
		new wcs.Main({
			controller: graphicController,
			csrf: window.csrf
		});
	}());
	</script>
</body>
</html>
```

最後に読み込んでいる`webjars/wcardinal-starter/wcs-graphic-editor.min.js`は`src\main\typescript\graphic-editor\main.ts`をRollupでバンドルしたものです。`src\main\typescript\graphic-editor\main.ts`内の`Main`クラスが`window.wcs.Main`にエクスポートされる様に設定してあります。ですのであとはこの`Main`クラスを実装すれば良いと言う事になります。

## `src\main\typescript\graphic-editor\main.ts`の実装

グラフィックエディターを画面に出すにはWinterCardinal GraphicEditorが提供する`FGraphicEditor`と言うクラスを使用します。例えば以下の様にしますとエディタが`body`直下に生成されます。

```typescript:src\main\typescript\graphic-editor\main.ts
export class Main extends FGraphicEditor {
	constructor(options: MainOptions) {
		super({
			controller: options.controller,
			header: newHeader("graphic-editor.label")
		});
	}
}
```

:::note
任意の位置にエディターを生成するには以下の様に`root`を指定します。

```typescript
import { FGraphicEditor } from "@wcardinal/wcardinal-geditor";

export class Main extends FGraphicEditor {
	constructor(options: MainOptions) {
		super({
			application: {
				root: "#my-root" // DOM要素のセレクタまたはDOMそのもの
			},
			controller: options.controller,
			header: newHeader("graphic-editor.label")
		});
	}
}
```
:::

ここで`options.controller`はHTML側で渡した`graphicController`となります。`graphicController`はWinterCardinal GraphicEditorの`FGraphicEditorControllerOptions`インターフェイスを実装したクラスで、グラフィックの検索やDBへの保存を担当するクラスです。[GraphicEditorのサンプル](https://github.com/winter-cardinal/winter-cardinal-graphic-editor/blob/main/src/main/sample/resource/graphic-controller.js)に実装例があります。

今回作成した簡易監視画面ではJava側の[app.wcc.graphic.GraphicController](https://github.com/winter-cardinal/winter-cardinal-starter/blob/main/src/main/java/app/wcc/graphic/GraphicController.java)をHTMLから`<script th:src="@{/graphic-controller}"></script>`で読み込んで、それをそのまま渡すと言う実装になっています。

```diff_html:src/main/resources/templates/graphic-editor/main.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:with="title=#{graphic-editor.label}">
<head>
	<th:block th:insert="~{common/header}"></th:block>
	<th:block th:insert="~{common/banner::head (${title})}"></th:block>
</head>
<body>
	<th:block th:insert="~{common/banner::body (${title})}"></th:block>
	<th:block th:insert="~{common/csrf}"></th:block>
	<script th:src="@{/webjars/wcardinal/wcardinal.worker.min.js}"></script>
+	<script th:src="@{/graphic-controller}"></script>
	<script th:src="@{/webjars/pixi.js/pixi.min.js}"></script>
	<script th:src="@{/webjars/wcardinal-ui/wcardinal-ui.min.js}"></script>
	<script th:src="@{|/webjars/wcardinal-ui/wcardinal-ui-theme-white-${#strings.toLowerCase(#locale.language)}-${#strings.toLowerCase(#locale.country)}.min.js|}"></script>
	<script th:src="@{/webjars/wcardinal-geditor/wcardinal-geditor.min.js}"></script>
	<script th:src="@{|/webjars/wcardinal-geditor/wcardinal-geditor-theme-default-${#strings.toLowerCase(#locale.language)}-${#strings.toLowerCase(#locale.country)}.min.js|}"></script>
	<script th:src="@{|/asset/message-script/${product.version}/message-script-${#strings.toLowerCase(#locale.language)}-${#strings.toLowerCase(#locale.country)}.min.js|}"></script>
	<script th:src="@{/webjars/wcardinal-starter/wcs-graphic-editor.min.js}"></script>
	<script>
	(function(){
		"use strict";
		new wcs.Main({
+			controller: graphicController,
			csrf: window.csrf
		});
	}());
	</script>
</body>
</html>
```

:::note
Java側の`app.wcc.graphic.GraphicController`をJavaScript側で読み込む機能はWinterCardinalによるものです。`<script th:src="@{/graphic-controller}"></script>`でクラスを読み込むと、`window.graphicController`にクラスのインスタンスが生成されます。

`GraphicController`に定義されたメソッドやフィールドの内、`@Callable`や`@Component`を付与されたものがJavaScriptから見える様になります。
:::

## `app.wcc.graphic.GraphicController`の実装

`GraphicController`はグラフィックの検索やDBへの保存を担いますが、今回の簡易監視画面ではグラフィックデータをDBからSELECT、UPSERT、DELETEする単純なものとなっています。

```java:src\main\java\app\wcc\graphic\GraphicController.java
@Controller
public class GraphicController {
	@Autowired
	protected GraphicComponent graphic;
}
```

```java:src\main\java\app\wcc\graphic\GraphicComponent.java
@Component
public class GraphicComponent {
	@Autowired
	GraphicService service;

	@Autowired
	GraphicPieceComponent piece;

	/** 検索文字を名称に含むグラフィック一覧を返す */
	@Callable
	Iterable<GraphicFound> search(String word) {
		return service.search(word);
	}

	/** 指定のIDのグラフィックデータを返す */
	@Callable
	Graphic get(final Long id) {
		return service.find(id).orElse(null);
	}

	/** 指定の名称のグラフィックデータ返す */
	@Callable
	Graphic getByName(final String name) {
		return service.find(name).orElse(null);
	}

	/** グラフィックを保存するメソッド */
	@Callable
	Long save(final Graphic graphic) {
		return service.save(graphic).getId();
	}

	/** 指定のIDのグラフィックを削除する */
	@Callable
	void delete(Long id) {
		service.delete(id);
	}
}
```

:::note
`GraphicComponent#piece`はグラフィックピースと呼ばれる別のグラフィックを扱うためのものです。グラフィックピースはVisioで言うステンシルの様なもので、グラフィックの中に埋め込むことが出来るグラフィックです。Graphic Editor画面では左側のパズルのアイコンからグラフィックピースを埋め込む事が出来ます。グラフィックピースの自体の作成はGraphic Piece Editor画面から行える様にしています。

![graphic-piece-button.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/0eb56dbe-e540-4b93-c029-639a7c12ea7d.png)
:::

# Diagram画面を実装する

次にグラフィックを表示するDiagram画面を作成します。そのためまずMVCコントローラを新設し、`/diagram`にアクセスすると`src\main\resources\templates\diagram\main.html`を返却する様に設定します。

```java:src\main\java\app\mvc\MvcDiagramController.java
@Controller
public class MvcDiagramController {
	@GetMapping("/diagram")
	public String main() {
		return "diagram/main";
	}
}
```

合わせてサイドメニューからDiagramを選択すると`/diagram`に遷移する様に設定します。

```diff_html:src\main\resources\templates\top\menu.html
-<button class="wcs-menu-item diagram">
+<button class="wcs-menu-item diagram" url="./diagram">
	<th:block th:replace="~{image/diagram}"></th:block>
	<span th:text="#{top.menu.diagram}">Diagram</span>
</button>
```

`src\main\resources\templates\diagram\main.html`側ではGraphic Editor画面と同様にWinterCardinalやPixiJSと言った必要なJavaScriptを読み込んでいます。

```diff_html:src\main\resources\templates\diagram\main.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:with="title=#{diagram.label}">
<head>
	<th:block th:insert="~{common/header}"></th:block>
	<th:block th:insert="~{common/banner::head (${title})}"></th:block>
</head>
<body>
	<th:block th:insert="~{common/banner::body (${title})}"></th:block>
	<th:block th:insert="~{common/csrf}"></th:block>
+	<script th:src="@{/webjars/wcardinal/wcardinal.worker.min.js}"></script>
	<script th:src="@{/diagram-controller}"></script>
+	<script th:src="@{/webjars/pixi.js/pixi.min.js}"></script>
+	<script th:src="@{/webjars/wcardinal-ui/wcardinal-ui.min.js}"></script>
+	<script th:src="@{|/webjars/wcardinal-ui/wcardinal-ui-theme-white-${#strings.toLowerCase(#locale.language)}-${#strings.toLowerCase(#locale.country)}.min.js|}"></script>
+	<script th:src="@{|/asset/message-script/${product.version}/message-script-${#strings.toLowerCase(#locale.language)}-${#strings.toLowerCase(#locale.country)}.min.js|}"></script>
+	<script th:src="@{/webjars/wcardinal-starter/wcs-diagram.min.js}"></script>
	<script>
	(function(){
		"use strict";
		new wcs.Main({
			controller: diagramController,
			csrf: window.csrf
		});
	}());
	</script>
</body>
</html>
```

最後に読み込んでいる`webjars/wcardinal-starter/wcs-diagram.min.js`は`src\main\typescript\diagram\main.ts`をRollupでバンドルしたものです。`src\main\typescript\diagram\main.ts`内の`Main`クラスが`window.wcs.Main`にエクスポートされるように設定してあります。ですのであとはこの`Main`クラスを実装していけばよいと言う事になります。

## `src\main\typescript\diagram\main.ts`の実装

WinterCardinal GraphicEditorはグラフィックを表示するためのクラス`FGraphicViewer`も提供しています。`FGraphicViewer`が生成する画面には、グラフィック選択ボタンがあり、そのボタンを押すとグラフィック選択＆表示できると言う画面構成になっています。しかしながら今回はDiagram画面表示後即座に`plant`グラフィックを表示したかったため`FGraphicViewer`を使用しないで実装しました。WinterCardinal UIが提供する`DDiagram`を使用すれば比較的簡単に実装する事が可能です。

```typescript
export class Main {
	protected _application: DApplication;
	protected _controller: DiagramController;
	protected _diagram?: DDiagram;

	constructor(options: MainOptions) {
		const application = new DApplication();
		this._application = application;

		// センサー値を受信した時の処理
		const controller = options.controller;
		this._controller = controller;
		controller.instant.on("update", (e: unknown, values: Record<string, InstantValue>) => {
			this.onInstantUpdate(values);
		});

		// グラフィックを表示するdiagramをステージに登録
		const diagram = this.diagram;
		application.stage.addChild(diagram);
	}

	// センサー値を受信した時に呼び出されるメソッド
	protected onInstantUpdate(values: Record<string, InstantValue>): void {
		const data = this.diagram.data;
		for (const sensorName in values) {
			const value = values[sensorName];
			data.set(sensorName, value.value, value.time);
		}
		this._application.update();
	}

	// グラフィックを描画するDDiagram部品
	protected get diagram(): DDiagram {
		return (this._diagram ??= this.newDiagram());
	}

	protected newDiagram(): DDiagram {
		const result = new DDiagram({ ... });
		this._controller.graphic.getByName("plant").then((graphic) => {
			result.set(DDiagrams.toSerialized(graphic));
		});
		return result;
	}
}
```

`DDiagram`クラスの`#set(...)`メソッドにグラフィックデータを渡すことでグラフィックが表示されるようになります。`newDiagram()`メソッドの中で`result.set(...)`している所がそれになります。

:::note
今回は使用しませんでしたが、仕様が合えば`FGraphicViewer`なら上記の様な実装も省略できます。`FGraphicViewer`を使用した実装例は[GraphicEditorのサンプル](https://github.com/winter-cardinal/winter-cardinal-graphic-editor#documentation)から見る事ができます。
:::

:::note
`DDiagram`も様々なカスタマイズが可能です。[WinterCardinal UIのサンプル](https://winter-cardinal.github.io/winter-cardinal-ui/sample/white/)からDiagramを参照してください。
:::

センサー値はサーバからプッシュされて送られてきますが、プッシュされた時に`options.controller.instant`において`update`イベントが発生する様にしています。このイベントの第２引数がセンサー値のマップですのでこれを`this.diagram.data.set(...)`に渡してグラフィックを更新しています。

`options.controller`はHTML側で渡した`diagramController`ですが、これはGraphic Editor画面の場合と同様に、Java側の`app.wcc.diagram.DiagramController`を`<script th:src="@{/webjars/wcardinal-starter/wcs-diagram.min.js}"></script>`で読み込んだものです。

```diff_html:src\main\resources\templates\diagram\main.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:with="title=#{diagram.label}">
<head>
	<th:block th:insert="~{common/header}"></th:block>
	<th:block th:insert="~{common/banner::head (${title})}"></th:block>
</head>
<body>
	<th:block th:insert="~{common/banner::body (${title})}"></th:block>
	<th:block th:insert="~{common/csrf}"></th:block>
	<script th:src="@{/webjars/wcardinal/wcardinal.worker.min.js}"></script>
+	<script th:src="@{/diagram-controller}"></script>
	<script th:src="@{/webjars/pixi.js/pixi.min.js}"></script>
	<script th:src="@{/webjars/wcardinal-ui/wcardinal-ui.min.js}"></script>
	<script th:src="@{|/webjars/wcardinal-ui/wcardinal-ui-theme-white-${#strings.toLowerCase(#locale.language)}-${#strings.toLowerCase(#locale.country)}.min.js|}"></script>
	<script th:src="@{|/asset/message-script/${product.version}/message-script-${#strings.toLowerCase(#locale.language)}-${#strings.toLowerCase(#locale.country)}.min.js|}"></script>
	<script th:src="@{/webjars/wcardinal-starter/wcs-diagram.min.js}"></script>
	<script>
	(function(){
		"use strict";
		new wcs.Main({
+			controller: diagramController,
			csrf: window.csrf
		});
	}());
	</script>
</body>
</html>
```

## `app.wcc.diagram.DiagramController`の実装

`DiagramController`の実装は以下の様になっています。`GraphicComponent`はGraphic Editor画面を作成する時に作ったクラスです。新設したのはセンサー値を収集する`InstantComponent`です。

```java
@Controller
public class DiagramController {
	@Autowired
	protected InstantComponent instant;

	@Autowired
	protected GraphicComponent graphic;
}
```

`InstantComponent`は以下の様に定義しています。`sensors`に格納されたセンサーIDのリストに基づき`triggerDirect("update", values)`でJavaScript側に`update`イベントを１秒周期で発火させると言うコードになっています。実際の監視システムでは物理デバイスから値を収集する事になりますが、今回はダミーの値を流しています。

```java
@Component
@RequiredArgsConstructor
public class InstantComponent extends AbstractController {
	// このフィールドは型がSClassのためJavaScript側から変更できます
	// 値が変更されると@OnChange("sensors")が付与されているonSensorsChangeが呼び出されます
	@Autowired
	protected SClass<List<String>> sensors;

	// sensorsが変更された時に呼び出されるメソッド
	@OnChange("sensors")
	protected void onSensorsChange(final List<String> sensors) {
		final var values = new HashMap<String, InstantValue>();
		for (int i = 0; i < sensors.size(); ++i) {
			final var sensor = sensors.get(i);
			values.put(sensor, new InstantValue(0, 0, i));
		}
		this.cancelAll(); // 定期処理を全て停止します
		this.interval("send", 0, 1000, values); // １秒周期でsendメソッドを実行する
	}

	@OnTime
	protected void send(final Map<String, InstantValue> values) {
		final var now = System.currentTimeMillis();
		for (final var value : values.values()) {
			value.update(now);
		}
		this.triggerDirect("update", values); // JavaScript側にupdateイベントを発生させます
	}
}
```

:::note
`triggerDirect`は`AbstractController`が提供しているメソッドで、WinterCardinalの機能です。使用方法はWinterCardinalの[Cheatsheet](https://winter-cardinal.github.io/winter-cardinal/cheatsheet/all-in-one.html#event)か[Javadoc](https://winter-cardinal.github.io/winter-cardinal/api/java/org/wcardinal/controller/AbstractController.html#triggerDirect(java.lang.String,java.lang.Object...))が参考になるかと思います。
:::

# DBの初期化

DBの初期化処理は`src\main\resources\data.sql`で行っています。Spring BootがこのSQLをサーバ起動時に実行してくれます。ただしSpring Boot 2.5から`data.sql`の実行タイミングが変更されているため`spring.jpa.defer-datasource-initialization=true`を指定しています。

```sql:src\main\resources\data.sql
insert into account (id, name, password) values
	(nextval('account_seq'), 'Account1', '{bcrypt}$2a$10$vmG149.oKSDVFxishLcodelvvAoL.u1yw1Q5tlGObqfhbXQZoxHMe');

insert into sensor (id, name, color) values
	(nextval('sensor_seq'), 'Sensor1', 0xc00000),
	(nextval('sensor_seq'), 'Sensor2', 0xff0000),
	(nextval('sensor_seq'), 'Sensor3', 0xffc000),
	(nextval('sensor_seq'), 'Sensor4', 0xffff00),
	(nextval('sensor_seq'), 'Sensor5', 0x92d050),
	(nextval('sensor_seq'), 'Sensor6', 0x00b050),
	(nextval('sensor_seq'), 'Sensor7', 0x00b0f0),
	(nextval('sensor_seq'), 'Sensor8', 0x0070c0),
	(nextval('sensor_seq'), 'Sensor9', 0x002060),
	(nextval('sensor_seq'), 'SensorA', 0x7030a0);

insert into graphic (version, id, name, label, category, summary, description, data) values
	(1, 1, 'plant', 'Plant', null, '', '', '{"width":1000,"height":500,"background":{"color":16777215,"alpha":1},"tile":{"mapping":{"enable":false,"from":{"lon":-180,"lat":85.05112877980659},"to":{"lon":180,"lat":-85.05112877980659}}},"resources":["","[1,12900086,1]","[0,5464442,1,2,0,15,0]","auto","[1,1]","[0,0]","[0,16777215,1,0.5]","[10,10]","[0,5197647,1,3,14,0,4,5,0,6,5,0,7,0,0]","[0,0,1]","[0,0,0,9,0,0,0,1,0]","[10]","data.value","[2,0,0,1,0.5,1,12]","[0,5197647,1,2,0,11,0]","[]","[1,16777215,0.5]","[1,5464442,1,2,0,15,0]","[1,16777215,1]","[0,15,15,18]","[19,19]","[[-40,-95,40,-95,-40,95,40,95],[],1,20]","[0,16777215,0.5]","[0,5197647,1,2,0,15,0]","Sensor1","[24,5464442,1,3,14,0,4,5,0,6,5,0,7,0,0]","[1,0,0,9,0,0,0,1,0]","[26]","[9,0,2,0,12900086,1,12]","[2,15,15,18]","[29,19]","[[-50,-35,-50,35,50,35],[],2048,30]","[2,0,0,9,0,0,0,1,0]","[32]","[[-70,15,-70,-15,-40,-15,70,-15],[],0,30]","Sensor2","[35,5464442,1,3,14,0,4,5,0,6,5,0,7,0,0]","[1,5197647,1,2,0,15,0]","[29,29]","[[-25,0,25,0],[],0,38]","[3,0,0,9,0,0,0,1,0]","[40]","[9,0,2,1,12900086,1,12]","[[-65,40,-65,-40,65,-40],[],2048,30]","Sensor3","[44,5464442,1,3,14,0,4,5,0,6,5,0,7,0,0]","[[-39.5,0,39.5,0],[],0,20]","[[0,-20,0,20],[],0,30]","[[0,-45,0,45],[],0,38]","[[0,20,0,-20],[],0,30]","[4,0,0,9,0,0,0,1,0]","[50]","[[50,-30,50,30,-50,30],[],2048,30]","Sensor4","[53,5464442,1,3,14,0,4,5,0,6,5,0,7,0,0]","[[30,15,30,-15,-30,-15],[],2048,30]","[[-35,10,-35,60,15,60,15,-60,35,-60],[],0,30]","Sensor5","[57,5464442,1,3,14,0,4,5,0,6,5,0,7,0,0]","Sensor6","[59,5464442,1,3,14,0,4,5,0,6,5,0,7,0,0]","Sensor7","[61,5464442,1,3,14,0,4,5,0,6,5,0,7,0,0]","%d%%","[63,5464442,1,3,14,0,4,5,0,6,5,0,7,0,0]","Math.round(value * 100)","[0,0,65,9,0,0,0,1,0]","[66]","[6,0,1,12]","[5,0,65,9,0,0,0,1,0]","[69]","[6,0,0,9,0,0,0,1,0]","[71]","[6,0,65,9,0,0,0,1,0]","[73]","[3,0,65,9,0,0,0,1,0]","[75]","[2,0,65,9,0,0,0,1,0]","[77]","[1,0,65,9,0,0,0,1,0]","[79]","[4,0,65,9,0,0,0,1,0]","[81]","Base Layer","[0,16777215,1]"],"data":["Sensor7","Sensor1","Sensor2","Sensor3","Sensor4","Sensor5","Sensor6"],"pieces":[],"layers":[[83,3,0,0,500,500,84,1]],"items":[[1,0,895.3,205,79,188.20601184465414,0,0,1,2,-1,8,11,1,12,-1,0,[13],-1,-1,[],0,0,2,-1,-1,27,0,0,0],[24,0,895.2,298.4,79,79,-3.141592653589793,0,1,14,-1,8,15,0.25,15,-1,0,[],-1,-1,[],0,0,2,-1,-1,36,0,0,0],[2,0,320.67595027202145,205,83,270,0,0,16,17,-1,8,15,1,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,2,0,0,0],[5,0,321.00350966810487,206.26566856686998,80,190,0,0,16,17,-1,8,15,0.25,15,21,0,[],-1,-1,[],0,0,0,-1,-1,3,0,0,0],[1,0,173.2999087667538,38.765510492931185,150,40,0,0,22,23,-1,25,15,0.25,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,4,0,0,0],[5,0,221.12709768190766,105,100,70,0,0,16,17,-1,8,27,0.25,15,31,0,[28],-1,-1,[],0,0,0,-1,-1,5,0,0,0],[5,0,141.00350966810484,195,140,30,0,0,16,17,-1,8,33,0.25,15,34,0,[28],-1,-1,[],0,0,0,-1,-1,6,0,0,0],[1,0,71.00350966810485,230,150,40,0,0,22,23,-1,36,15,0.25,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,7,0,0,0],[5,0,246.00350966810484,180,50,0.001,0,0,16,37,-1,8,33,0.25,15,39,0,[28],-1,-1,[],0,0,0,-1,-1,8,0,0,0],[5,0,206.00350966810484,310,130,80,0,0,16,37,-1,8,41,0.25,15,43,0,[42],-1,-1,[],0,0,0,-1,-1,9,0,0,0],[1,0,141.00350966810484,370,150,40,0,0,22,23,-1,45,15,0.25,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,10,0,0,0],[2,0,505.56237190444716,158.77839459189352,82,270,0,0,16,17,-1,8,15,1,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,11,0,0,0],[5,0,505.10443771695526,65.45895714570631,79,0.001,0,0,16,17,-1,8,15,0.25,15,46,0,[],-1,-1,[],0,0,0,-1,-1,12,0,0,0],[5,0,506.0929247688943,254.5415428542937,79,0.001,0,0,16,17,-1,8,15,0.25,15,46,0,[],-1,-1,[],0,0,0,-1,-1,13,0,0,0],[5,0,321.00350966810487,360,0.001,40,0,0,16,17,-1,8,15,0.25,15,47,0,[],-1,-1,[],0,0,0,-1,-1,14,0,0,0],[5,0,321.00350966810487,435,0.001,90,0,0,16,17,-1,8,15,0.25,15,48,0,[],-1,-1,[],0,0,0,-1,-1,15,0,0,0],[3,0,614.2253687511643,270.86321251756783,80,40,0,0,16,17,-1,8,15,0.25,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,16,0,0,0],[0,0,614.2253687511643,240.86321251756783,80,80,0,0,18,17,-1,8,15,0.25,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,17,0,0,0],[2,0,556.911855533147,381,82,221,1.5707963267948966,0,16,17,-1,8,15,1,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,18,0,0,0],[5,0,505.0483404887911,313.2358646565492,0.001,40,0,0,16,17,-1,8,15,0.25,15,47,0,[],-1,-1,[],0,0,0,-1,-1,19,0,0,0],[5,0,612.8308671645913,320.9136787482432,0.001,40,0,0,16,17,-1,8,15,0.25,15,49,0,[],-1,-1,[],0,0,0,-1,-1,20,0,0,0],[5,0,604.4654957643252,98.76257940993423,100,60,0,0,16,17,-1,8,51,0.25,15,52,0,[28],-1,-1,[],0,0,0,-1,-1,21,0,0,0],[1,0,653.5071037852289,41.25179705856203,150,40,0,0,22,23,-1,54,15,0.25,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,22,0,0,0],[5,0,584.4654957643255,185.98913888463437,60,30,0,0,16,17,-1,8,15,0.25,15,55,0,[],-1,-1,[],0,0,0,-1,-1,23,0,0,0],[5,0,754.669098890451,360,0.001,40,0,0,16,17,-1,8,15,0.25,15,47,0,[],-1,-1,[],0,0,0,-1,-1,25,0,0,0],[5,0,754.669098890451,435,0.001,90,0,0,16,17,-1,8,15,0.25,15,48,0,[],-1,-1,[],0,0,0,-1,-1,26,0,0,0],[2,0,895.2828559916301,205,83,270,0,0,16,17,-1,8,15,1,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,37,0,0,0],[5,0,895.6104153877135,360,0.001,40,0,0,16,17,-1,8,15,0.25,15,47,0,[],-1,-1,[],0,0,0,-1,-1,28,0,0,0],[5,0,895.6104153877135,435,0.001,90,0,0,16,17,-1,8,15,0.25,15,48,0,[],-1,-1,[],0,0,0,-1,-1,29,0,0,0],[5,0,812.7482675081753,321.9531452959578,70,120,0,0,16,17,-1,8,15,0.25,15,56,0,[],-1,-1,[],0,0,0,-1,-1,30,0,0,0],[1,0,556.0035096681048,364.18250230763476,150,40,0,0,22,2,-1,58,15,0.25,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,31,0,0,0],[1,0,753.2592292869073,40,150,40,0,0,22,23,-1,60,15,0.25,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,32,0,0,0],[1,0,895.5149489057096,40,150,40,0,0,22,23,-1,62,15,0.25,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,1,0,0,0],[1,0,895,210,150,40,0,0,22,23,-1,64,67,0.25,15,-1,0,[68],-1,-1,[],0,0,0,-1,-1,34,0,0,0],[1,0,555,396.7671588564074,150,40,0,0,22,23,-1,64,70,0.25,15,-1,0,[68],-1,-1,[],0,0,0,-1,-1,35,0,0,0],[1,0,754.3308738467676,205.2353290191372,79,188.20601184465414,0,0,1,2,-1,8,72,1,12,-1,0,[13],-1,-1,[],0,0,2,-1,-1,38,0,0,0],[24,0,754.2308738467677,298.63532901913703,79,79,-3.141592653589793,0,1,14,-1,8,15,0.25,15,-1,0,[],-1,-1,[],0,0,2,-1,-1,39,0,0,0],[2,0,754.3415394943676,205,83,270,0,0,16,17,-1,8,15,1,15,-1,0,[],-1,-1,[],0,0,0,-1,-1,24,0,0,0],[1,0,755,210,150,40,0,0,22,23,-1,64,74,0.25,15,-1,0,[68],-1,-1,[],0,0,0,-1,-1,33,0,0,0],[1,0,140,390,150,40,0,0,22,23,-1,64,76,0.25,15,-1,0,[68],-1,-1,[],0,0,0,-1,-1,40,0,0,0],[1,0,70,250,150,40,0,0,22,23,-1,64,78,0.25,15,-1,0,[68],-1,-1,[],0,0,0,-1,-1,41,0,0,0],[1,0,170,59.535870583066384,150,40,0,0,22,23,-1,64,80,0.25,15,-1,0,[68],-1,-1,[],0,0,0,-1,-1,42,0,0,0],[1,0,655,60,150,40,0,0,22,23,-1,64,82,0.25,15,-1,0,[68],-1,-1,[],0,0,0,-1,-1,43,0,0,0]],"snap":[1,[1,1,[]],[1,1,10]]}');
```

# 画面の表示とカスタマイズ

サイドメニューからDiagramを選択し、Diagram画面が表示されるようになります。数値やタンク容量が`InstantComponent`の生成するセンサー値に応じてアニメーションするのを確認できます。

![diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/89adaab4-4208-fb47-9eca-f98a91ad8eb5.png)

試しにタンクを１つ消し、色味をグリーンに変更してみます。まず削除するタンクを選択します。左マウスボタンでクリックしながらドラッグすると範囲選択する事が出来ます。

![modified-a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/860036bd-b188-26d6-fe14-8b23fe23e133.png)

![modified-b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/bbc3931a-6a78-edd9-8c8b-df16b459f1ec.png)

選択されたらDeleteキーで削除します。

![modified-c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/72ec3061-c733-5a50-4f70-49726dd9d481.png)

色を変更したい場所を選択し、塗り色（Fill）を変更します。

![modified-d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/aa66857a-de07-3f3a-0343-3fe22f07fc1d.png)
![modified-e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/abb44f13-6863-f1dc-6764-5d7f0905c124.png)
![modified-f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/6253d6ec-7ac8-13ca-c8fd-74aae9ceb57f.png)

一部の線にアクションと呼ばれる動きの設定がされていますので、そのアクションの色も合わせて変更します。

![modified-g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/8d269359-ec0f-2964-c225-726efa6228f4.png)

この状態で保存し、Diagram画面を表示させると反映されていることを確認できます。

![modified-h.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888283/d25a059f-3444-b59e-ef29-3ea1478c03f2.png)

# まとめ

今回はグラフィカルな簡易監視画面をWinterCardinal GraphicEditorで作成してみました。GraphicEditorがWebGLで出来ていることから、SVGと比較して比較的軽量に動作するかと思います。カスタマイズ例としてタンク削除と色変更を行いましたが、設備を増やしたり、クリックで画面遷移できるようにしたり、センサー割り当てを変更したりと言った事も可能です。またより応用的なカスタマイズとして、アプリケーション独自のシェイプやアクションを導入する事も可能になっています。[サンプルページ](https://winter-cardinal.github.io/winter-cardinal-graphic-editor/sample/default/)に実装例もありますので参考にして頂ければ幸いです。