Fusetools - Fuse.Maps パッケージ利用時のTips

この記事では、Fusetoolsというプロトタイピングツールの
公式パッケージの1つであるFuse.Mapsと、その利用時のTipsについて紹介します。
Fusetoolsの基本的な知識がある方を前提としています。

# この記事を通して実現できるもの
- Fusetoolsアプリ上での地図表示。
    - 地図上でのマーカー表示を含む。
- 地図上へのUIパーツの配置。(UIパーツ例: 検索窓, メニュー)
    - Uberや食べログなど、地図表示を用いるアプリでほぼ避けられない表示形式。

# Fuse.Maps パッケージとは
Fusetoolsが公式で提供しているパッケージの1つで、
アプリ上での地図表示を可能とする[MapView](https://www.fusetools.com/docs/fuse/controls/mapview)クラスを内包。
地図上でのピンの表示も当然可能で、例えば[Google Places API](https://developers.google.com/places/web-service/?hl=ja)と連動させ
Google Map同様の地図をNativeアプリ上で描画することが可能。
地図の回転や、ズーム、当然、現在位置表示にも対応。

今回は、アプリ内での地図表示のための一連の処理を説明しますが、
外部の地図アプリを開くための [FuseJS/Maps](https://www.fusetools.com/docs/fuse/reactive/fusejs/maps) というモジュールも用意されています。

# 環境
- Fusetools ver1.4.0 (2017/11/30時点で最新)

# 使い方
## プロジェクトファイルの編集
Fusetoolsのプロジェクトルートに存在する、"プロジェクト名.unoproj"ファイルにて
以下の通り、利用パッケージとしてFuse.Mapsを宣言。
現在地情報が必要な場合は、Fuse.GeoLocationも宣言します。

加えて、Androidでの地図表示向けに、GoogleのApi Keyが必要となる。 
以下の具合で、Android.Geo.ApiKey にてApiKeyを設定する。

``` プロジェクト名.unoproj
{
  ...,
  "Packages": [
    ...
    "Fuse.GeoLocation",
    "Fuse.Maps",
    ...
  ],
  ...,
  "Android": {
   "Geo": {
        "ApiKey": "ご自身のGoogle API Key"
    }
  }
}
```

## UXファイル, JSファイルの編集
UXファイル(Fusetools独自のXML記法のHTMLのようなもの)にて、
以下のようにNativeViewHostタグを親とし、その下に各要素を配置する。

```地図を表示したいページ(の抜粋).ux
<NativeViewHost>
    <DockPanel>
        <Panel Dock="Top">
            <!-- 例えば、ここで定義した検索窓は、地図上に表示される -->
        </Panel>
        <Panel Layer="Background">
            <MapView ShowCompass="true" ShowMyLocation="true" ShowMyLocationButton="true" Latitude="{latitude}" Longitude="{longitude}" Zoom="14" LocationTapped="{onMapTapped}">
                <Each Items="{markerList}">
                    <MapMarker Latitude="{latitude}" Longitude="{longitude}" Label="{name}" Tapped="{onMarkerTapped}" />
                </Each>
            </MapView>
        </Panel>
    </DockPanel>
</NativeViewHost>
```

以下の通り、対応するjsファイル内では、位置情報の取得処理や


```地図を表示したいページ.js
var Observable = require("FuseJS/Observable");

// 位置情報の取得
var GeoLocation = require("FuseJS/GeoLocation");
var currentLocation = GeoLocation.location;
var latitude = Observable(currentLocation.latitude);
var longitude = Observable(currentLocation.longitude);

// Google Place API関連
var googlePlaceApiUrl = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json?';
var googleApiKey = 'ご自身のGoogle API Key'
var radiusMeter = 500; // place API の探索半径(メートル)

// 地図上に表示するマーカーオブジェクトを格納するリスト
var markerList = Observable();
// 選択されたマーカーを保持するオブジェクト
var selectedMarker = Observable();

function GooglePlaceMarker(google_palce_result) {
    // 一部の代表的なパラメータをセット
    this.place_id = google_palce_result['place_id'];
    this.name = google_palce_result['name'];
    this.price_level = (google_palce_result['price_level']) ? google_palce_result['price_level'] : '-';
    this.rating = (google_palce_result['rating']) ? google_palce_result['rating'] : '-';
    this.latitude = google_palce_result['geometry']['location']['lat'];
    this.longitude = google_palce_result['geometry']['location']['lng'];
    this.open_now = (google_palce_result['opening_hours']) ? google_palce_result['opening_hours']['open_now'] : null;
    this.permanently_closed = google_palce_result['permanently_closed'];
    this.vicinity = (google_palce_result['vicinity']) ? google_palce_result['vicinity'] : '';
    this.icon = google_palce_result['icon'];
    // マーカーがタップされた際のイベントハンドラー
    this.onMarkerTapped = function(self) {
    	console.log(JSON.stringify(self.data));
    	selectedMarker.value = self.data;
    };
};

function searchGooglePlace() {
    var tmpMarkerList = [];
    var url = googlePlaceApiUrl + 'location=' + latitude.value + ',' + longitude.value
            + '&radius=' + radiusMeter + '&types=food&key=' + googleApiKey;
    fetch(url)
    .then(function(response) {
        return new Promise(function(resolve, reject) {
            if (response.ok) {
                resolve(response.json());
            } else {
                reject(response);
            }
        });
    }).then(function(json) {
        json.results.forEach(function(result) {
            tmpMarkerList.push(new GooglePlaceMarker(result));
        });
        // Observableのリストの総入れ替えは、replaceAllでの実行が確実。
        markerList.replaceAll(tmpMarkerList);
    }).catch(function(e) {
        console.log(e.status, e.statusText, e.toString());
    });
}

function onMapTapped() {
    // マーカーの選択解除
    selectedMarker.clear();
}

(function() {
    // 地図上への初期表示
    searchGooglePlace();
})();

module.exports = {
    latitude: latitude,
    longitude: longitude,
    markerList: markerList,
    selectedMarker: selectedMarker,
    onSearchClicked: searchGooglePlace,
    onMapTapped: onMapTapped,
};
```

# どこに落とし穴があったか
普通に開発者が地図とUIパーツを、Fusetoolsのタグ名を頼りに組もうとすると、
まず、以下のように、MapViewだけを納めたNativeViewHostを作り、
その外に、地図上に表示したい検索窓やメニュー等のパーツを実装すると思います。
しかし、これでは想定通りに動きません。
というのも、NativeViewHostは、UI上最前面に描画されるレイヤーとなるらしく、
この外でUIパーツをいくら定義しようと、ユーザが触れる/見れるところには出てこないのです。
なので、少し気持ち悪いですが、上述のサンプルコード(uxファイル)のように、
**NativeViewHost内** にて、必要となるパーツを配置する必要が出てきます。

```最初にやってしまいがちな地図とUIパーツの埋め込み方.ux
<Panel Layer="Overlay">
    <!-- 地図の上に表示させたいUIパーツ -->
</Panel>
<NativeViewHost>
    <MapView ShowCompass="true" ShowMyLocation="true" ShowMyLocationButton="true" Latitude="{latitude}" Longitude="{longitude}" Zoom="14" LocationTapped="{onMapTapped}">
        <Each Items="{markerList}">
            <MapMarker Latitude="{latitude}" Longitude="{longitude}" Label="{name}" Tapped="{onMarkerTapped}" />
        </Each>
    </MapView>
</NativeViewHost>
```

# おまけ
## Fusetoolsを選ぶべき理由
- プロトタイプと本制作の垣根がもはやなくなる。
    - 簡単に、でも、こだわれば独自で綺麗なiOS/Androidアプリを一挙に制作可能。
- 基本的な、Native機能にはアクセスできる。
    - WebRTC等は未対応だった気がします。
- デザイナーとエンジニアの連携にも思想としてこだわっている(らしい)。
    - 最近、UIビルダーも導入され、エンジニア以外にも敷居が低下。
- 何よりも動作が軽快。
    - Androidでも、iOS並みの挙動を期待できる。(これは結構大きい)
    - 低レイヤーから独自実装しているので、高い描画パフォーマンスを出せているらしい。
- XMLベースの独自記法であるものの、慣れれば直感的でわかりやすい。
    - 豊富な動作サンプル。
    - プラットフォーム別(Android / iOS)の記述を必要としない(今のところ経験していない)。
- コミュニティが活発。
    - 公式フォーラムに質問を投げかけると、1~2日でFusetoolsのエンジニアから直々に返答がある。(英語)
    - 改善要望も、同フォーラムにて容易に提案可能なため、役職問わず意見が回収・検討されやすい。
- アップデート頻度の高さ(いい意味で)。
- 個人で利用するには安価。
    - 料金プランは近い将来に、変更の可能性があるらしいものの。

## トレンド
久々のFusetoolsに関する記事ということで、
[Fusetoolsのトレンド(vs Prott, Origami Studio)](https://trends.google.co.jp/trends/explore?date=today%205-y&q=Fusetools,Origami%20Studio,Prott)を調べてみました。
上昇傾向ではあるものの、伸び悩んではいるようですね。頑張って欲しい。
ノルウェーと韓国での利用が盛んで、他の地域では拡大途上のようです。

## 参考資料
- https://www.fusetools.com/docs/fuse/controls/mapview
- https://www.fusetools.com/docs/fuse/controls/nativeviewhost
- https://www.fusetools.com/community/forums/howto_discussions/nativeviewhost_is_rendered_in_front_of_whole_app

次回はおそらく、Fusetools上での
画像の切り出し範囲選択(クライアントからの画像投稿用)の実装サンプルを紹介します。
それでは。