Angular Materialを使ってMATLAB Production ServerのWebフロントサンプルをちょっとファンシーにしてみた

MATLABで作った解析アルゴリズムをシステム化する際に、コンパイルしてMATLAB Production Serverに展開すればRESTful APIでやり取りできるので、JavaScriptからもコールできる。Webフロントエンドを試しに作ってみたのでその備忘録。

# サンプル
[ドキュメントのExample](https://jp.mathworks.com/help/mps/restfuljson/example-web-based-bond-pricing-tool-using-javascript.html)に債券計算を題材にしたMATLABコードとHTML/JavaScriptのサンプルがあるので、手始めにまずはここから始めてみる。

## HTML/JavaScript側
bptool.htmlとcalculatePrice.jsをコピペしてほぼ完了だが、bptool.htmlの`<script src="calculatePrice.js" type="text/javascript"></script>`のコメントアウトを外しておく。

## MATLAB側
サンプルのpricecalc.mをそのまま使う。

```matlab:pricecalc.m
function price = pricecalc(face_value, coupon_payment,...
                           interest_rate, num_payments)
    M = face_value;
    C = coupon_payment;
    N = num_payments;
    i = interest_rate;
    
    price = C * ( (1 - (1 + i)^-N) / i ) + M * (1 + i)^-N;
```

MATLABの「アプリ」→「Production Serverコンパイラ」をクリックしてコンパイル画面を開く。

- タイプ：デフォルトの「配布可能なアーカイブ」のまま。
- エクスポートする関数：pricecalc.mを追加
- アーカイブ情報：「BondTools」に変更

![201866_204610.png](https://qiita-image-store.s3.amazonaws.com/0/254990/625e93a8-82bd-af8c-f664-f54b0e1304fa.png)
これで「パッケージ化」をクリックするとMATLAB Production Server用のパッケージ(.ctfファイル)が作成される。が、今回はWebフロントエンドを作成するのが目的なので、パッケージ化をせずに「クライアントのテスト」をクリックする。これによってMATLAB上でMATLAB Production Serverのもどきが立ち上がる。

「テスト」タブに移るので以下のとおり設定。

- ポート：デフォルトの9910のまま
- CORSを有効にする：チェックを入れる
 
「起動」ボタンをクリックすると、http://localhost:9910/BondTools でサービスがListenするようになる。


![201866_211949.png](https://qiita-image-store.s3.amazonaws.com/0/254990/cc1265bc-833f-c5b6-bc2d-1bd1857a8317.png)

## 動作確認
Webサーバーも立てずに、先程のHTML/JavaScriptをPCのデスクトップに置き、ブラウザでbptool.htmlを開く。

スライダーを左右に動かしてパラメータ(Face Value、Coupon Paymant、Number of payments、Interest Rate)に応じて債券価格が計算される。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/254990/0930d231-da2c-b8e6-160f-2d36bd8cb03a.png)

MATLABで実行させているMATLAB Production Serverもどきへの入出力もうまくできている。
![201866_214852.png](https://qiita-image-store.s3.amazonaws.com/0/254990/ee6ff86f-ce21-eb54-2ed1-d8a5c4ce1566.png)

ただ、NativeのHTML/JavaScriptで画面を作っているので、画面がしょぼい。もっとファンシーにしたい。

後半に続く。

# もっとファンシーなWebフロントにしたい
どうせならフレームワークを使ってMaterial Designの画面を作ってみようと思ったので、備忘録後半。
Angularのインストールは[AngularのQuick Start Guide](https://angular.io/guide/quickstart)や他の記事にお任しして、ngコマンドでプロジェクトを作成するところから。

## Angularプロジェクトの作成
プロジェクト名はmpsBondCalcという名前にして、ng newでプロジェクトの作成。

```ng new mpsBondCalc
```
これで一通りのAngularのテンプレートが作成される。

## Angular-Materialのインストール
プロジェクトのルートフォルダーにチェンジディレクトリして、Angular-Material一式をインストール。
```
cd mpsBondCalc
npm install --save @angular/material @angular/cdk
npm install --save @angular/animations
npm install --save hammerjs
```
[AngularのGetting Started](https://material.angular.io/guide/getting-started)の Step 5: Gesture Supportにあるとおり、スライダーを使うにはHammerJSが必須となるので、main.tsに```import 'hammerjs';```を追加する。

モジュールにAngular-Materialで今回使用するインプットフォーム、スライダーなどをインポートする(下記のMat*)。

```typescript:app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatCardModule, MatInputModule, MatSliderModule, MatDividerModule } from '@angular/material';    
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpModule } from '@angular/http';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    MatCardModule,
    MatInputModule,
    MatSliderModule,
    MatDividerModule,
    BrowserAnimationsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

メインとなるapp.component.htmlとapp.component.tsは、サンプルのbptool.htmlとcalculatePrice.jsを移植しつつ、DOM操作などAngular用に適宜書き直す。区切り線もbptool.htmlでは```<hr>```を使っていたが、ここではAngular-Materialのdivider(mat-divider)を使ってみる。

```html:app.component.html
<mat-card class="result">
    <mat-card-content>
        <h1><a>Bond Pricing Tool</a></h1>
        <h2></h2>
        This example shows an application that calculates a bond price from a simple formula.<p>
            You run this example by entering the following known values into a simple graphical interface:<p>
        <ul>
            <li>Face Value (or value of bond at maturity) - M</li>
            <li>Coupon payment - C</li>
            <li>Number of payments - N</li>
            <li>Interest rate - i</li>
        </ul>
        The application calculates price (P) based on the following equation:<p>
            P = C * ( (1 - (1 + i)^-N) / i ) + M * (1 + i)^-N<p>
        <mat-divider [inset]="true"></mat-divider>
        <h3>M: Face Value </h3>
        <form class="example-form form-inline" novalidate>
            <mat-form-field class="example-full-width">
                <input matInput type="number" name="facevalueValue" [(ngModel)]="facevalueValue"  min="0" max="10000" (change)="calculatePrice()">
            </mat-form-field>
            <mat-slider
                name="couponPaymentSlider"
                class="example-margin"
                [max]=10000
                [min]=0
                [(ngModel)]="facevalueValue"
                (change)="calculatePrice()">
            </mat-slider>
        </form>
        
        <h3>C: Coupon Payment </h3>
        <form class="example-form" novalidate>
            <mat-form-field class="example-full-width">
                <input matInput type="number" name="couponPaymentValue" [(ngModel)]="couponPaymentValue"  min="0" max="1000" (change)="calculatePrice()">
            </mat-form-field>
            <mat-slider
                name="couponPaymentSlider"
                class="example-margin"
                [max]=1000
                [min]=0
                [step]=0.01
                [(ngModel)]="couponPaymentValue"
                (change)="calculatePrice()">
            </mat-slider>            
        </form>
        
        <h3>N: Number of payments  </h3>
        <form class="example-form" novalidate>
            <mat-form-field class="example-full-width">
                <input matInput type="number" name="numPaymentsValue" [(ngModel)]="numPaymentsValue"  min="0" max="1000" (change)="calculatePrice()">
            </mat-form-field>
            <mat-slider
                name="numPaymentsSlider"
                class="example-margin"
                [max]=1000
                [min]=0
                [(ngModel)]="numPaymentsValue"
                (change)="calculatePrice()">
            </mat-slider>            
        </form>
        
        
        <h3>i: Interest rate </h3>      
        <form class="example-form" novalidate>
            <mat-form-field class="example-full-width ">
                <input matInput type="number" name="interestRateInput" [(ngModel)]="interestRateValue"  min="0" max="1" step="0.01" (change)="calculatePrice()">
            </mat-form-field>   
            <mat-slider
                name="interestRateSlider"
                id="interest_rate_slider"
                class="example-margin"
                [max]=1
                [min]=0
                [step]=0.01
                [(ngModel)]="interestRateValue"
                (change)="calculatePrice()">
            </mat-slider>            
        </form>

        <h2>BOND PRICE</h2>
        <p id="price_of_bond_value" style="font-weight: bold" #price_of_bond_value>
        <p id="error" style="color:red" #error>

        <mat-divider [inset]="true"></mat-divider>
        <h3>Request to MPS Server</h3>
        <p id="request" #request>

        <h3>Response from MPS Server</h3>
        <p id="response" #response>
        <mat-divider [inset]="true"></mat-divider>
    </mat-card-content>
</mat-card>
```

```typescript:app.component.ts
import {Component, ViewChild, ElementRef} from '@angular/core';
import {Http, Headers} from "@angular/http";
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})

export class AppComponent {
    @ViewChild('request') requestElement: ElementRef;
    @ViewChild('response') responseElement: ElementRef;
    @ViewChild('error') errorElement: ElementRef;
    @ViewChild('price_of_bond_value') priceOfBondValueElement: ElementRef;

    // Initial values
    facevalueValue = 0;
    couponPaymentValue = 0;
    numPaymentsValue = 0;
    interestRateValue = 0;

    httpOptions = {
        headers: new Headers({
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        })
    };

    constructor(private http: Http) {}

    // Calculate
    calculatePrice() {
        let url = "http://localhost:9910/BondTools/pricecalc";

        //Use MPS RESTful API to specify params using JSON
        let params = {
            "nargout": 1,
            "rhs": [this.facevalueValue, this.couponPaymentValue, this.interestRateValue, this.numPaymentsValue]
        };

        let requestElement = this.requestElement.nativeElement;
        requestElement.innerHTML = "URL: " + url + "<br>" + "Method: POST <br>" + "Data:" + JSON.stringify(params);


        const response = this.http.post(url, params, {headers: this.httpOptions.headers}).toPromise()
            .then(response => {
                //Use MPS RESTful API to check HTTP Status
                if (response.status === 200) {
                    // Deserialization: Converting text back into JSON object
                    // Response from server is deserialized 
                    let result = response.json();
                    //Use MPS RESTful API to retrieve response in "lhs"
                    if ('lhs' in result) {
                        let errorElement = this.errorElement.nativeElement;
                        errorElement.innerHTML = "";
                        let priceOfBondValueElement = this.priceOfBondValueElement.nativeElement;
                        priceOfBondValueElement.innerHTML = "Bond Price: " + result.lhs[0].mwdata;
                    } else {
                        let errorElement = this.errorElement.nativeElement;
                        errorElement.innerHTML = "Error: " + result.error.message;
                    }
                } else {
                    let errorElement = this.errorElement.nativeElement;
                    errorElement.innerHTML = "Error:" + response.statusText;
                }

                let responseElement = this.responseElement.nativeElement;
                responseElement.innerHTML = "Status: " + response.status + "<br>"
                    + "Status message: " + response.statusText + "<br>" +
                    "Response text: " + JSON.stringify(response.json());
        });
    }
}
```

書き直しは必要最小限にしたいと思いつつ、入力フォームやスライダーのところでngModleを使うとかなり実装が楽なので、かなり書き直してしまった。

## 実行結果
それではAngular-MaterialによるWebフロントでの実行結果を確認。
```ng serve --open
```

![201866_213411.png](https://qiita-image-store.s3.amazonaws.com/0/254990/6a0477fe-3327-03f9-bb69-92c3a98aaaec.png)

よしよし、入力フォームとスライダーがモダンになっていい感じ。
Webサーバーにデプロイする時は```ng build --prod```でTypeScriptをビルドして、できたHTML/JavaScriptをWebサーバーに置き、MATLAB Production ServerもMATLAB上での「もどき」ではなく、ちゃんとパッケージ化してCTFファイルをMATLAB Production Serverに持っていって実行させれば良いのだが、Webフロントの開発を行う時はデバッグしやすい「もどき」が便利。