CodeZine(コードジン)

特集ページ一覧

Bing Mapsよりも使いやすい? ComponentOne Studioの地図コンポーネントでストアアプリを作ろう

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

 Windowsストアアプリで地図を扱うときの王道は、Bing Map SDKを使用することです。しかし、Bing Maps SDKを使用する場合、プラットフォームとして「Any CPU」が選択できなくなるため、構成マネージャーで明示的にVisual Studioと同じx86プラットフォームを明示的に指定してデバッグ実行を行います。また、ストアに申請するときも、x86、x64、ARMの3タイプを同時に登録します。

 ComponentOne Studioの地図コンポーネントを使えば、このような煩雑は不要で「Any CPU」でデバッグ実行が可能です。

Windowsストアアプリの下準備

 Windowsストアアプリのテンプレートを使って新規プロジェクトを作成すると、ツールボックスには「C1 XAML 8.1 Controls」タブがすでに作成されていて、Windowsストアアプリで使えるコンポーネントが表示されています。

図1 ツールボックス
図1 ツールボックス

 今回使う地図コンポーネントは「C1Maps」コンポーネントなのでツールボックスにあることを確認したら、Windowsストアアプリの下準備を開始しましょう。

プロジェクト構成の準備

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

 そのため、Windowsストアアプリのテンプレートから新規プロジェクトを作成した直後の状態から、Modelsフォルダ、ViewModelsフォルダ、Viewsフォルダを作成して、それぞれのフォルダに、AEDModelクラス、MainViewModelクラス、MainPage.xamlを配置します。

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

追加ライブラリの準備

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

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

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

 サンプルアプリで使用するオープンデータは、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コンポーネントをXAMLエディタ上にドラッグ&ドロップします。これだけで必要な参照設定が行われます。

画面デザイン

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

図4 画面デザイン
図4 画面デザイン
リスト2 MainPage.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">
        <Viewbox VerticalAlignment="Top" HorizontalAlignment="Left">
            <Custom:C1Maps x:Name="C1Maps1" 
                    VerticalAlignment="Top"
                    HorizontalAlignment="Left"
                    Zoom="13"
                    Width="480" Height="600">
                <Custom:C1Maps.Resources>
                    <DataTemplate x:Key="PinStyle">
                        <Custom:C1VectorPlacemark 
                            Fill="Red" Stroke="Red"
                            LabelPosition="Top"
                            Foreground="Red"
                            GeoPoint="{Binding LatLng}"
                            Geometry="M7.4166234,16.197001L13.…(中略)…z">
                            <Custom:C1VectorPlacemark.RenderTransform>
                                <TransformGroup>
                                    <ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
                                    <TranslateTransform Y="-20" X="-10"/>
                                </TransformGroup>
                            </Custom:C1VectorPlacemark.RenderTransform>
                        </Custom:C1VectorPlacemark>
                    </DataTemplate>
                </Custom:C1Maps.Resources>
                <Custom:C1Maps.Source>
                    <Custom:VirtualEarthRoadSource ApplicationId="{x:Null}" />
                </Custom:C1Maps.Source>
                <Custom:C1VectorLayer ItemsSource="{Binding City.Items}"
                                        ItemTemplate="{StaticResource PinStyle}"
                                        LabelVisibility="Visible"/>
            </Custom:C1Maps>
        </Viewbox>
    </StackPanel>
        :
     (中略)
        :

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

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

という手順で行います。

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

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

 MainPage.xamlのコードビハインド側に検索用のロジックを一部記載します。

リスト3 MainPage.xaml.vb
Namespace Views
    Public NotInheritable Class MainPage
        Inherits Page
        :
     (中略)
        :
        Public Sub New()
            InitializeComponent()
        :
     (中略)
        :
            Me.DataContext = App.MainVM
        End Sub

        Private Async Sub NavigationHelper_LoadState(sender As Object, e As Common.LoadStateEventArgs)
            Try
                Await App.MainVM.GetItems("愛知県", "豊田市")
                SetCenterPos()
                Me.C1Maps1.Zoom -= 1
                Me.C1Maps1.Zoom += 1
            Catch ex As Exception

            End Try
        :
     (中略)
        :
End Namespace

 NavigationHelper_LoadStateプロシージャの中で、GetItemsメソッドによりデータを取得後にZoomプロパティに-1および+1を実行しているのは、強制的に再描画を発生させて正しく画面が表示されるようにするためです。これは本来不要な処理ですが、ComponentOne Studio 2014 v2では描画方法が変わったのか、このような操作を行わないとマークが画面に表示されません。

 ここまでできたら、後はMainViewModelクラスを記述すれば出来上がりです。

サンプル実行

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

使用する地図を切り替える

 C1Mapsが通常使用する地図はBing Mapsですが、これを他の地図に切り替えることもできます。

 C1Mapsでは、巨大な地図画像を例えば256ピクセルx256ピクセルの小さなタイルに分割しておき、描画に必要な箇所のタイルだけを読み込むことでスムーズな地図描画を実現しています。このタイルを読み込む部分がC1MultiScaleTileSourceのGetTileLayersメソッドになります。

 このGetTileLayersメソッドをオーバーライドして、本来のBing MapsではなくOpenStreetMapの画像を渡すようにすれば地図を切り替えることができます。具体的には、C1MapsのSourceプロパティにGetTileLayersメソッドをオーバーライドしたC1MultiScaleTileSourceを設定することになります。

リスト4 MainPage.xaml.vb
Namespace Views
    Public NotInheritable Class MainPage
        Inherits Page

        :
     (中略)
        :

        Private Async Sub NavigationHelper_LoadState(sender As Object, e As Common.LoadStateEventArgs)
            Try
                Await App.MainVM.GetItems("愛知県", "豊田市")
                Me.C1Maps1.Source = New OfflineMapsSource
                SetCenterPos()
                Me.C1Maps1.Zoom -= 1
                Me.C1Maps1.Zoom += 1
            Catch ex As Exception

            End Try

        End Sub

        :
     (中略)
        :

        Public Class OfflineMapsSource
            Inherits C1.Xaml.Maps.C1MultiScaleTileSource

            Private Const uriFormat As String = "http://a.tile.openstreetmap.org/{Z}/{X}/{Y}.png"

            Sub New()
                MyBase.New(&H8000000, &H8000000, &H100, &H100, 0)
            End Sub

            Protected Overrides Sub GetTileLayers(tileLevel As Integer,
                                                    tilePositionX As Integer,
                                                    tilePositionY As Integer,
                                                    tileImageLayerSources As IList(Of Object))
                If (tileLevel > 8) Then
                    Dim zoom = tileLevel - 8
                    Dim Uri = uriFormat
                    Uri = Uri.Replace("{X}", tilePositionX.ToString())
                    Uri = Uri.Replace("{Y}", tilePositionY.ToString())
                    Uri = Uri.Replace("{Z}", zoom.ToString())
                    tileImageLayerSources.Add(New Uri(Uri))
                End If
            End Sub
        End Class
    End Class
End Namespace

 GetTileLayersはタイルが必要になった時に呼び出されるので、その中でa.tile.openstreetmap.orgを呼び出してタイルを取得します。

図6 サンプル実行
図6 サンプル実行

まとめ

 ComponentOne Studioの地図コンポーネントが通常利用している地図は、本文中でも触れていますがBing Mapsです。しかし、Bing Maps SDKは使っていないため、プラットフォームに「Any CPU」が指定できます。これはBing Maps SDKが内部的にC++のモジュールを使っているがためにプラットフォームを明示的に指定しなければならないのとは対照的に、ComponentOne Studioの地図コンポーネントがとてもWindowsストアアプリにフィットした実装になっているともいえるでしょう。

 またBing Maps SDKでも可能なのですが、ComponentOne Studioの地図コンポーネントが使用する地図を切り替えられることも、より日本に即した地図を選択することでアプリの見やすさを向上できるなど、幅広く応用が効きます。

 標準の方法があるけれど、それを置き換えて機能アップするという市販コンポーネントを選択する利点をぜひ体験してみてください。

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

著者プロフィール

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

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

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