C# + WPF で Google Map JavaScript API v3のGeoJSONを使ってみた

先日 DevFest Japan 2014 Summer - Google I/O 2014 報告会 信州会場 に参加した際、 @ さんによるGoogleMap + GeoJSONのプレゼンがありました。

どんなものかと気になったため、公式サイトを眺めてみたところ、

ということが分かりました。

 
JavaScriptというとWebブラウザを使えばいろいろと試せそうでしたが、せっかくなので最近やっているC#WPFで扱うことができないかを調べてみました。

すると、WebBrowserコントロールを使えばHTMLやJavaScriptをウィンドウの中に表示させられることが分かりました。

そこで、WPFのWebBrowserコントロールの勉強がてら、C# + WPFGoogle Map JavaScript API v3のGeoJSONを試してみることにしました。

なお、WPFGoogle Mapを表示する方法については、以下がとても参考になりました。ありがとうございました。
WPF で Google Map | アカベコマイリ

 

環境

 
2015/12/10 追記 ここから

公式Blogに以下の記事が掲載されましたので、Microsoft.TeamFoundation.MVVMの使用前に記事を確認してみてください。
Microsoft.TeamFoundation.MVVM 名前空間の利用について - Visual Studio サポート チーム blog - Site Home - MSDN Blogs

2015/12/10 追記 ここまで

 

ViewModel

ViewModelのベースクラス

ViewModelは、以前と同じく Microsoft.TeamFoundation.MVVM.ViewModelBase を継承したものを使うので、Microsoft.TeamFoundation.Controlsの参照を追加しておきます。

今回は手抜きをして以前作った VMBase.cs をそのまま持ってきました。そのため、今回特に使わないINotifyDataErrorInfoインタフェースも実装されています。
CSharp-Sample/MVVMApp/MVVMApp/VMBase.cs - thinkAmi/CSharp-Sample - GitHub

 

ViewModel

この時点のViewModelの実装は、

  • WebBrowserコントロールにバインドするためのプロパティUriを用意
  • コンストラクタで、WebBrowserに表示するローカルのhtmlファイルのパスをUrlに渡す

だけになります。

class MainWindowViewModel : VMBase
{
    public MainWindowViewModel()
    {
        Uri = String.Format( "file://{0}GeoJsonLoad.html", AppDomain.CurrentDomain.BaseDirectory );
    }

    private string _uri;
    public string Uri
    {
        get { return _uri; }
        set 
        {
            _uri = value;

            RaisePropertyChanged();
        }
    }
}

 

WebBrowserUtilityクラス

WebBrowserのSourceプロパティへデータバインディングするために、依存プロパティ用にWebBrowserUtilityクラスを作成します。

内容は参考にしたものと同じになります。

 

View (MainWindow.xaml)

以前と同様に

  • xmlnsにプロジェクトの名前空間を追加
  • Window.DataContextに、ViewModelクラスを指定

します。

 
また、WebBrowser要素には、先ほど作成したWebBrowserUtilityクラスにあるプロパティ(ここではSource)を指定してデータバインディングします。

<Grid>
    <WebBrowser local:WebBrowserUtility.Source="{Binding Uri}" />
</Grid>

 

WebBrowserコントロールに表示するHTMLの作成

HTMLの中でJavaScriptを記述し、GeoJsonのデータを取り込みます。

今回は以下の3つの方法を試してみました。

  • 外部のGeoJsonデータを読み込み、loadGeoJson()を使って表示
  • JavaScript内のGeoJsonデータを読み込み、addGeoJson()を使って表示
  • C#でGeoJsonデータを作成し、addGeoJson()を使って表示

なお、差が分かるように、それぞれのHTMLではzoomレベルを変えるなどしています。

 

各HTMLの共通の設定

ヘッダに、以下を追加します。

  • 「セキュリティ保護のため~」のエラーを回避するため、<!-- saved from url=(0017)http://localhost/ -->を追加
  • GoogleMapをWebBrowserコントロール全体に表示するために、スタイルを指定
<html lang="ja" xmlns="http://www.w3.org/1999/xhtml">
<!-- saved from url=(0017)http://localhost/ -->
...
<head>
    <style type="text/css">
        html, body  { height: 100%; margin:0px; }
        #map { height: 100%; overflow:auto; }
    </style>
    ...
</head>

 
また、HTML内のJavaScriptで以下のようなエラーが発生することがあります。

  • loadGeoJson()で外部のGeoJSONデータを読み込むと、「アクセスが拒否されました」エラー
  • JSON.parse()を使うと、「'JSON'は定義されていません (JSON is undefined)」エラー

 
原因は、通常WebBrowserコントロールはIE7相当であり、IE7にはJSONオブジェクトなどが存在しないため、発生しています。

 
手元はIE11なので、WebBrowserコントロールをIE11相当で動かすことができますが、方法としては、

  • headに <meta http-equiv="X-UA-Compatible" content="IE=11" /> を追加
  • レジストリを修正
    • <アプリ名>,exe をエントリに追加
    • VisualStudioのデバッグ実行で確認する場合、<アプリ名>.vshost.exe のエントリ追加も必要

のどちらかが必要になります。

 
なお、前者のX-UA-Compatibleを使う方法はIE11以降では非推奨になっていることに注意します。
ドキュメント モードの非推奨 - MSDN

 
あとは、忘れずに各HTMLをプロジェクトに含めた上で、プロパティを以下のようになっているか確認します。

プロパティ名 設定内容
ビルドアクション コンテンツ
出力ディレクトリにコピー 常にコピーする

 

外部のGeoJsonデータを読み込み、loadGeoJson()を使って表示する方法(loadGeoJson.html)

この場合は、Google Maps JavaScript API v3 のサンプル通りに実装します。

map.data.loadGeoJson('https://storage.googleapis.com/maps-devrel/google.json');

 

JavaScript内でGeoJsonデータを作成し、addGeoJson()を使って表示

addGeoJson()を使う場合は、addGeoJsonにGeoJsonデータのオブジェクトを渡せば表示することができます。

//  https://storage.googleapis.com/maps-devrel/google.json にあるGeoJsonデータから、改行・スペースを削除したもの
var j = '..' // 長いので省略
map.data.addGeoJson(JSON.parse(j));

 

C#でGeoJsonデータを作成し、addGeoJson()を使って表示

内容は上記のaddGeoJsonと似ていますが、GeoJsonデータはC#にて作成し、C#からJavaScriptへデータを渡して表示するのを試してみます。

 
WPFC#からJavaScriptへとデータを渡すためには、System.Windows.Controls.ObjectForScriptingを使います。 WebBrowser.ObjectForScripting プロパティ - MSDN

 
ただ、ObjectForScriptingは依存プロパティではないことからデータバインディングできないため、同じサイトの別記事を参考に、添付プロパティを使ってデータバインディングができるようにします。
WPF で Google Map その 2 | アカベコマイリ

 
全体では以下の実装を行い、C#で作ったJSON文字列をJavaScript側に渡してGoogleMap上に表示させています。

 

Mapクラスを作成

このクラスで、

  • [ComVisible(true)]属性をクラスに付ける
  • GeoJSON文字列を返すプロパティを用意する (C#オブジェクトからJSON文字列へシリアライズするために、DynamicJSONを使用)
  • INotifyPropertyChangedインタフェースを実装する

なお、Googleという文字を描くGeoJsonオブジェクトを作成するのが手間だったので、中央にピンを立てるようなGeoJSONにしています。

[ComVisible(true)]
public class Map : INotifyPropertyChanged
{
    string _geoJson;
    public string GeoJson
    {
        get
        {
            if (!string.IsNullOrEmpty(_geoJson)) return _geoJson;

            var j = new
            {
                type = "FeatureCollection",
                features = new[] {
                    new {
                        type = "Feature", 
                        property = new {},
                        geometry = new {
                            type = "Point",
                            coordinates = new [] { 137.883, -28}
                        }
                    }
                }
            };
            //  DynamicJSONでJSON文字列化
            return DynamicJson.Serialize(j);
        }
        set
        {
            this._geoJson = value;
            RaisePropertyChanged();
        }
    }


    //  INotifyPropertyChangedインタフェースの実装
    public event PropertyChangedEventHandler PropertyChanged;

    public void RaisePropertyChanged([CallerMemberName]string propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

 

MainWindowViewModelクラスに、Map型のプロパティを作成するとともに、コンストラクタで初期化

Uriプロパティと同じなので、省略します。

 

WebBrowserUtilityクラスに、ObjectForScriptingプロパティへデータバインディングするための依存プロパティを追加

上記の記事通りなので、省略します。

 

MainWindow.xamlのWebBrowser要素に、データバインディングの記述を追加

Uriのデータバインディングと同じようにして記述します。

local:WebBrowserUtility.ObjectForScripting="{Binding GoogleMap}"

 

HTMLのJavaScriptに、C#のプロパティを参照して表示するコードを追加

window.external.GeoJsonC#のGeoJSONを返すプロパティを参照します (ここでは、ObjectForScriptingプロパティにデータバインディングされているMapクラスのGeoJsonプロパティ)。

その後、JSON.parse()JSONオブジェクトにして、Google Maps JavaScript API v3に渡して表示します。

var j = JSON.parse(window.external.GeoJson);
map.data.addGeoJson(j);

 

できなかったこと - ローカルの geo.json ファイルを読み込んで表示すること

ローカルにgeo.jsonファイルを作り、ビルド時に常にコピーするように設定したものの、map.data.loadGeoJson('geo.json'); としても「アクセスが拒否されました」というエラーになりました。

ローカルファイルに対するCORSのやり方が分からなかったので、今回はローカルのgeo.jsonファイルを使うのは諦めました。
Google Maps Tutorials — Google Developers

 

ソースコード

GitHubに上げました。
CSharp-Sample/WebbrowserGeoJson at master · thinkAmi/CSharp-Sample

HTMLは3種類ありますが、MainWindowViewModelのコンストラクタで切り替えをハードコーディングしているので(手抜き)、1種類のみ表示されます。