CodeZine(コードジン)

特集ページ一覧

ComponentOne Studioの地図コンポーネント+オープンデータでマッシュアップしよう

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2014/09/25 14:00

 行政機関からさまざまなオープンデータが提供されるようになっただけではなく、LinkData.orgのようにオープンデータ活用支援の環境も整ってきました。オープンデータを使うことで、アプリ作成時の軸足をデータ集めからアプリそのものに専念できるようになります。また、オープンデータには位置情報を持っているものも多く、そのデータを地図に表示することで次の価値がみえてくることがあります。

 データを地図に表示するアプリをつくろうと思うと、Google MapやBing Mapを使うWebアプリとして作成するのが手軽ですが、ComponentOne Studioの地図コンポーネントを使えば同じような手軽さで地図対応のWPFアプリが作成できます。

WPFアプリの下準備

プロジェクト構成の準備

 今回作成するWPFアプリはModel、ViewModel、Viewのクラス構成をとり、Modelにロジック、Viewに画面デザイン、その両者をViewModelで接続することで画面定義とロジックコードの結合を疎にしてそれぞれの役目が分かりやすくまとまるように考慮しています。

 そのため、WPFアプリのプロジェクトを生成した直後の構成から一度「MainWindow.xaml」を削除し、Modelsフォルダ、ViewModelsフォルダ、Viewsフォルダを作成して、それぞれのフォルダに、AEDModelクラス、MainViewModelクラス、MainWindow.xamlを配置します。

図1 プロジェクト構成
図1 プロジェクト構成

追加ライブラリの準備

 今回使用するオープンデータはREST/JSON形式でデータを取得するので、JSONデータの取り扱いが楽になるようにJSON.NETライブラリをNuGetから導入します。

図2 nugetパッケージの追加
図2 nugetパッケージの追加

地図コンポーネントの準備

 ComponentOne StudioのインストーラーでWPFを選択してインストールしてあれば、ツールボックスのアイテム追加でWPFコンポーネントとしてC1.Mapsが表示されるので、チェックしてツールボックスに追加します。

 ComponentOne Studioのトライアル版は、グレープシティのWebページから申し込むことができます。

図3 C1Mapsコンポーネントのツールボックスへの追加
図3 C1Mapsコンポーネントのツールボックスへの追加

データ取得ロジックの実装

 サンプルアプリで使用するオープンデータは、Microsoft Azure Mobile Service上で動作しているAED検索オープンデータを使用します 。

リスト1 AEDModel.vb
Imports System.Collections.ObjectModel
Imports System.ComponentModel
Imports System.Net
Imports System.Runtime.CompilerServices
Imports Newtonsoft.Json

Namespace Models
    Public Class AedInfo
        Public Property Id As Long
        Public Property LocationName As String                '/* 場所_地名【名称】*/
        Public Property FullAddress As String                 '/* 住所_住所【住所】*/
        Public Property Perfecture As String                  '/* 構造化住所_都道府県 */
        Public Property City As String                        '/* 構造化住所_市区町村 */
        Public Property AddressArea As String                 '/* 構造化住所_町名 */
        Public Property AddressCode As String                 '/* 住所コード */
        Public Property Latitude As Single                    '/* 緯度経度座標系_緯度 */
        Public Property Longitude As Single                   '/* 緯度経度座標系_経度 */
        Public Property FacilityId As String                  '/* 公共設備_ID */
        Public Property FacilityName As String                '/* 公共設備_名称 */
        Public Property FacilityPlace As String               '/* 公共設備_設置場所【設置場所】※受付横とか */
        Public Property ScheduleDayType As String             '/* 公共設備_利用可能時間【利用可能時間】 */
        Public Property PhotoOfAedUrl As String               '/* 公共設備_写真URL【写真】 */
        Public Property FacilityNote As String                '/* 公共設備_補足【補足】 */
        Public Property LatLng As Point
    End Class

    Public Class TCity
        Public Property Perfecture As String                  '/* 構造化住所_都道府県 */
        Public Property City As String                        '/* 構造化住所_市区町村 */
        Private _Items = New ObservableCollection(Of AedInfo)
        Public Property Items As ObservableCollection(Of AedInfo)
            Get
                Return Me._Items
            End Get
            Set(value As ObservableCollection(Of AedInfo))
                Me._Items = value
            End Set
        End Property
    End Class

    Public Class AedModel
        Implements INotifyPropertyChanged

        Public Sub New()
        End Sub

        Private _City = New TCity
        Public Property City As TCity
            Get
                Return Me._City
            End Get
            Set(value As TCity)
                Me._City = value
                OnPropertyChanged()
            End Set
        End Property

        Public Async Function SelectData(perfectureName As String, cityName As String) As Task
            Dim targetAeds = New ObservableCollection(Of AedInfo)
            Try
                Dim urlString = String.Format(
                    "https://aed.azure-mobile.net/api/aedinfo/{0}/{1}/",
                    perfectureName,
                    cityName)
                Dim request = CType((WebRequest.Create(urlString)), HttpWebRequest)
                request.Method = "GET"
                request.ContentType = "application/x-www-form-urlencoded"
                Try
                    Dim response = CType((Await request.GetResponseAsync()), HttpWebResponse)
                    Dim responseDataStream = New System.IO.StreamReader(response.GetResponseStream())
                    Dim responseResult = responseDataStream.ReadToEnd()
                    Dim json = JsonConvert.DeserializeObject(Of IEnumerable(Of AedInfo))(responseResult)
                    Dim aeds = From item In json Order By item.FullAddress
                    targetAeds = New ObservableCollection(Of AedInfo)
                    For Each aed In aeds
                        aed.LatLng = New Point(aed.Longitude, aed.Latitude)
                        targetAeds.Add(aed)
                    Next
                Catch ex As Exception
                    OnFaild("NetworkError" + ex.Message)
                End Try
            Catch ex As Exception
                OnFaild("NetworkError" + ex.Message)
            Finally
                Me.City = New TCity With {.Perfecture = perfectureName,
                                          .City = cityName, .Items = targetAeds}
            End Try
        End Function

        Public Event Faild(sender As Object, e As String)
        Protected Sub OnFaild(line As String)
            RaiseEvent Faild(Me, line)
        End Sub

        Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
        Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
        End Sub
    End Class
End Namespace

C1Mapsコンポーネントの配置

 ツールボックスからC1MapsコンポーネントをWPFウィンドウ上にドラッグ&ドロップします。これだけで必要な参照設定とライセンスファイルのプロジェクトファイルへの追加が行われます。

画面デザイン

 サンプル画面の構成は、左に地図、右に一覧表の2カラム構成になっていて、検索ボタンをクリックすると特定市区町村のAED情報を検索表示します。

図4 画面デザイン
図4 画面デザイン
リスト2 MainWindow.xaml
        :
      (中略)
        :
<Grid Grid.Row="1" Margin="15,0,0,0">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <StackPanel Grid.Column="0" Margin="20,0,20,0">
        <Custom:C1Maps x:Name="C1Maps1" 
                       VerticalAlignment="Top"
                       HorizontalAlignment="Left"
                       Zoom="13"
                       Width="480" Height="600">
            <Custom:C1Maps.Resources>
                <DataTemplate x:Key="PinStyle">              …(2)'
                    <Custom:C1VectorPlacemark 
                        Fill="Red" Stroke="Red"
                        LabelPosition="Top"
                        Foreground="Red"
                        GeoPoint="{Binding LatLng}">         …(3)
                        <Custom:C1VectorPlacemark.Geometry>  …(4)
                            <EllipseGeometry                                                                             RadiusX="2"
                                RadiusY="2" />
                            </Custom:C1VectorPlacemark.Geometry>
                    </Custom:C1VectorPlacemark>
                </DataTemplate>
            </Custom:C1Maps.Resources>
            <Custom:C1Maps.Source>
                <Custom:VirtualEarthRoadSource ApplicationId="{x:Null}"/>
            </Custom:C1Maps.Source>
            <Custom:C1VectorLayer ItemsSource="{Binding City.Items}"           …(1)
                                  ItemTemplate="{StaticResource PinStyle}"/>   …(2)
        </Custom:C1Maps>
    </StackPanel>
        :
      (中略)
        :

 C1Mapsコンポーネントで緯度経度に合わせてマークを描画するには、

  • (1)緯度経度が含まれたコレクションをItemsSourceプロパティにBindingする
  • (2)マーク描画用スタイルをItemTemplateに指定する
  • (3)マーク描画用スタイルの中で、C1VectorPlacemarkのGeoPointに緯度経度をBindingする
  • (4)マーク自体はC1VectorPlacemarkのGeometryで形を指定する

という手順で行います。今回はEllipseGeometryを指定しているので●を描画します。

 コードビハインド側でループを回して1点1点位置を指定して描画する必要はありません。

検索イベント時のコードの記述

 MainWindow.xamlのコードビハインド側に検索用のロジックを一部記載してますが、検索ボタンのCommandプロパティにViewModelのメソッドをBindingする方法の方がより結合度が疎になります。

リスト3 MainWindow.xaml.vb
Imports C1.WPF.Maps.ImageryService

Namespace Views
    Public Class MainWindow

        Public Sub New()
            ' この呼び出しはデザイナーで必要です。
            InitializeComponent()
            ' InitializeComponent() 呼び出しの後で初期化を追加します。
            Me.DataContext = Application.MainVM
        End Sub

        Private Async Sub Search_Click(sender As Object, e As RoutedEventArgs)
            Await Application.MainVM.GetItems("愛知県", "豊田市")
            SetCenterPos()
        End Sub

        Private Sub SetCenterPos()
            Try
                Dim centerLoc = New Point(0, 0)
                Dim counter = 0
                Dim items = Application.MainVM.City.Items
                '/* 地図の中心位置表示 */
                For Each item In items
                    Dim loc = New Point(item.Longitude, item.Latitude)
                    centerLoc.Y = Math.Abs(centerLoc.Y * counter + loc.Y) / (counter + 1)
                    centerLoc.X = Math.Abs(centerLoc.X * counter + loc.X) / (counter + 1)
                    counter += 1
                Next
                Me.C1Maps1.Center = centerLoc
            Catch ex As Exception
            End Try
        End Sub
    End Class
End Namespace

 あとはMainViewModelクラスを記述すれば出来上がりです。

サンプル実行

図5 サンプル実行
図5 サンプル実行

 なお、ライセンス認証しているのにもかかわらず実行時にトライアル版の表示が出る場合は、Propertiesフォルダがプロジェクトに自動追加されていない可能性があります。その場合は、ソリューションエクスプローラーですべてのファイルを表示してPropertiesフォルダごとライセンスファイルをプロジェクトに追加すれば表示が消えますので、もし、トライアル版表示に悩まされている方は一度確認してみるとよいでしょう。

マーカーのカスタマイズ

 C1Mapsコンポーネントでは地図マーカーとして特殊な形を描画したいときは、C1VectorPlacemarkのGeometryにXAMLのパス定義を指定します。今回のサンプルで表示するのはAEDの情報なのでハート形マーカーとして、位置をハートの最下部の頂点で指し示すようにカスタマイズしてみましょう。

パスの作成

 地道にパスを作成してもいいのですが、Syncfusion Metro Studio 2を使えば登録されているアイコンからパス定義を生成できるので、今回はこの方法でパスを作成します。

図6 マーカーのXAML定義を生成する
図6 マーカーのXAML定義を生成する

XAMLへの設定

 C1VectorPlacemarkにGeometryパラメタを追加して、そこにパス定義を設定します。

リスト4 MainWindow.xamlより抜粋
        :
      (中略)
        :
<Custom:C1VectorPlacemark 
    Fill="Red" Stroke="Red"
    LabelPosition="Top"
    Foreground="Red"
    GeoPoint="{Binding LatLng}"
    Geometry="M7.4166234,16.197001L13.497334,16.345396 …(中略)…E-06z" />
        :
      (中略)
        :

マーカーの先端位置調整

 ハートマークの下部の先端は、Y軸方向最下層でありX軸方向は中心となっています。そこでRenderTransform定義を追加して位置をずらしたいと思います。手作業でXAMLを変更してもいいのですが、このようなときに便利なのがBlendです。

図7 マーカーの先端を位置情報に合わせる
図7 マーカーの先端を位置情報に合わせる

 ついでに大きさも縦横0.5倍になるように調整しておきましょう。

リスト5 MainWindow.xamlより抜粋
        :
      (中略)
        :
<Custom:C1VectorPlacemark 
    Fill="Red" Stroke="Red"
    LabelPosition="Top"
    Foreground="Red"
    GeoPoint="{Binding LatLng}"
    Geometry="M7.4166234,16.197001L13.497334,16.345396 …(中略)…E-06z">
    <Custom:C1VectorPlacemark.RenderTransform>
        <TransformGroup>
            <ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
            <TranslateTransform Y="-20" X="-10"/>
        </TransformGroup>
    </Custom:C1VectorPlacemark.RenderTransform>
        :
      (中略)
        :

実行

 カスタマイズしたマーカーが想定した位置に表示されるかを確認します。

図8 カスタマイズマーカーでのサンプル実行
図8 カスタマイズマーカーでのサンプル実行

まとめ

 意外にもBing MapのWPF対応は、2012/1/12に提供されたBing Maps Windows Presentation Foundation (WPF) Control, Version 1.0 が最新版のようです。デスクトップアプリとして作成するのであれば、描画速度やストアアプリと同じXAML(利用できるパラメタなどに違いはありますが)で画面定義ができてBindingが使える点から考えても、WindowsフォームではなくWPFだと思います。ストアアプリではなくデスクトップアプリとしてオープンデータを活用するのであれば、ぜひWPFとComponentOne Studioを組み合わせて使いやすいアプリを設計構築してみてはいかがでしょうか。

  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 初音玲(ハツネアキラ)

     国内SIerのSEでパッケージ製品開発を主に行っており、最近は、空間認識や音声認識などを応用した製品を手掛けています。  個人的には、仕事の内容をさらに拡張したHoloLensなどのMRを中心に活動しています。  Microsoft MVP for Windows Development...

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5