2014年9月28日日曜日

ごみ出しカレンダー を公開しました


ごみ出しカレンダー
Windows 8.1 / Windows Phone 8.1 で動作します

ストアアプリ 「ごみ出しカレンダー」を公開しました。
 Windows 8.1 / Windows Phone 8.1 でお使い頂ける Universal Windows App になっています。無料です。

Windows ストア からダウンロード
Windows Phone ストアからダウンロード

ごみ出しカレンダー で できる事


  • ごみ出し専用のカレンダー アプリです。
  • アプリ上で入力した曜日毎のごみ分別情報を、スタート画面上のタイルに表示します。
  • 同じMicrosoft アカウント を使用しているWindows / Windows Phone 上で同じごみ出しデータを共有します。例えば、Desktop・TabletでWin8.1、スマホはWindows Phone 8.1を使っているハワイ在住のAさんの場合、どれか一つでごみ出しデータを更新すると、数分~数十分で他のSystem でも同じデータが表示されるようになります。

スタート画面上のタイル表示


…今回のアプリ、技術的にやっていることは気絶するほど単純ですし、また機能的にも…OS標準のカレンダーで別にいいんじゃん?と言われると全くその通りです。

(OS標準カレンダーの貴重なスペースを、Weeklyなゴミ出し予定で埋めたくないという所はあり、そこは便利とは思うのですが)

では何で作ったかというと、Windows / Windows Phone で多くのCodeを共有できるUniversal Windows App の作成を頭からお尻まで通して行っておきたい、という学習目的です。
futa8 も、今作っているPICT8 も、Code baseはそこそこ大きいサイズなので…気軽に色々ひょいと試せる感じでは無くなっており、Universal App化のようなChallengeを気軽に試せるものを作っておきたかったというのも大きいです。

...

実際、実アプリとして書いてみることで収穫は多かったです。

正直な所、Universal Appっつっても同じBinary動く訳では無いし…と若干引いて見ている所が僕にはあったのですが、実際やってみると所謂Data Source側では気を付けて書く事でかなりそのままいけるなぁと。

UIはそのままとは行かないのですが、WinRT のUI コンポーネントの互換性はかなり高いので、例えばメイン画面の月別カレンダー作るところ等はサイズを変えている以外、Code BehindもXAMLも全く同じCodeになっています。編集ページもサイズ変更だけでほぼそのままです。

逆に、共用できないXAML部分をなるべくコピペで済ませるために、サイズ部分をなるべくStyleで外に出しておく的な(ダメな?当たり前?)知識も身につきました。

反面、Template Defaultでの各Pageの遷移、Instance化ではWinとWPでは違うところも多く、うお何これ!?という所も多かったです。

手を動かすと経験値上がるな!という当たり前の話を噛み締めているところです。



StoreApp で位置情報を扱う

この項では、
  1. 位置情報を取得する
  2. 位置情報を住所の文字列に変換する
方法について書きます。

1. 位置情報の取得


現在の位置情報を取得


Namespace Windows.Devices.Geolocation にあるGeolocator Class を使います。

 

// Appの初期化の辺りで一発Geolocatorを作成しておき、
this.geolocator = new Geolocator();
...
// 位置情報が必要な時にGetGeopositionAsyncを呼ぶ。
var pos = await this.geolocator.GetGeopositionAsync();

ここで要注意なのは、
  1. 位置情報を取得するアプリは、AppxManifest.xml内で位置情報の使用を宣言する必要があること
  2. 起動後初回の使用では「位置情報の取得」の確認メッセージが出ること
  3. GetGeopositionAsync では緯度経度のみが取得でき、住所データは取得出来ない事(今のところ)
1. については、VSのManifest Designerでチェックを入れるだけです。又、ストア上にはこのアプリが位置情報を使用する旨が表示されます。

2. これはAPIをCallしたところ(上の例だとGetGeopositionAsync)で問答無用に表示されます。
このため、なるべく、Userの操作の結果(ボタン押下のハンドラ等)…「○○をやろうとした結果」として呼ばれるところでAPIをCallすべきと思います。
Userの操作の無い所…初期化中等…でいきなりこれを出すのはあまり上品では無いように思います。そういうアプリたまに見ますが。

3. GetPositionAsync の返り値 として帰るGeoposition Classには以下二つのProperty…
  • CivicAddress
  • Coordinate
があり、前者には住所文字列、後者には緯度経度が入る、事になっています。
が、前者CivicAddressは、Address Provider、緯度経度を住所に変換するプロバイダがインストールされていない場合常に空文字列で返ります。
そして、現在のところAddress Providerは何処からも提供されていないです。

画像の位置情報を取得


JPEG画像にEXIF データとして位置情報が埋め込まれている場合があります。
これは大変に簡単で、画像ファイルのStorageFileからImagePropertiesを取得します。もし緯度経度データが存在すれば、ImageProperties.LatitudeとLongitudeにデータが詰まっています。

※ 画像が「ファイル」である場合…Storage上のFileにはこれでいけるのですが…例えば、一度OpenFileAsyncしてStreamに全部ガーっと呼んだメモリ上のデータや、Httpか何かでがーっとDLしてまだファイルに落として無いデータ、に対して使用することは出来ないです。困った。


2. 位置情報を住所文字列に変換する


上で触れたようにWinRT組み込みのGeolocator Classだけでは不可能で、Bing.Maps APIを使う事になります。
緯度経度をAPI経由でBingに投げると、住所に変換して返してくれるという感じです。

Bing APIの使用には
  • アカウント登録とAPI Keyの作成
  • Bing Maps SDKのインストールと参照の追加
が必要になります。方法はMSのサイトでも見て頂ければいいのですが、無料の場合、
  • キーは3つまで
  • 50,000 Transaction/Day
となっています。間違えていると怖いのでご自分でご確認下さい。

※ Bing Maps API は Architecture毎のNative BinaryとしてBuildされています。このため、このAPIを「使う」Moduleは(.NETで書いたC#であっても)「Any CPU」としてBuildすることが出来なくなります。

Bing Maps API を使用した住所の取得


ここまで行くとソース見たほうが早いべ、という事でソースを貼って終わります。
注意点としては、LocationData は複数返ってくるので、中の属性を見て必要なものだけ拾う、という所です。

※ 以下の例は、アプリ「ごみ出しカレンダー」で使っているCodeです。
ごみ種別の入力画面でアプリバー上の「検索」ボタンを押すと、PCの現在地を使ってごみ収集ページを検索してあげる、という機能です。(自治体ごとに千差万別のごみ出しページをスクレイピングとかやってられないので、そこから先は手動です)

ごみ出しカレンダー
http://apps.microsoft.com/windows/app/c3cdaa23-5b99-4b28-803f-00b90c86a601


 
private async void GetAddressAppBarButton_Click(object sender, RoutedEventArgs e)
        {
             //「PCの位置情報から自治体のごみ出しページを検索してあげる」

            var loader = new Windows.ApplicationModel.Resources.ResourceLoader();

            var pos = await this.geolocator.GetGeopositionAsync();
            
            Bing.Maps.Search.ReverseGeocodeRequestOptions requestOptions =
                new Bing.Maps.Search.ReverseGeocodeRequestOptions(new Location(pos.Coordinate.Point.Position.Latitude, pos.Coordinate.Point.Position.Longitude));

            var searchManager = this.bingMaps.SearchManager;
            var response = await searchManager.ReverseGeocodeAsync(requestOptions);

            string query = "";
            Bing.Maps.Search.GeocodeAddress addr = null;

            if (null != response && 0 < response.LocationData.Count)
            {
                for (int i = 0; i < response.LocationData.Count; i++)
                {
                    /// 住所表示に使うLocationDataを探すCode
                    /// Bing.Mapsでは、Responseに複数のLocationDataが詰まって返ってくることがある。(普通は一個だが、たまに二つ)
                    /// 一つは地図上での住所表示用データ ... GeocodeLocationUsageType 'Display'
                    /// もう一つはナビ用の道路情報。 ... GeocodeLocationUsageType 'Route' または 'Both'.
                    /// 後者については住所の文字列が入っていないので、今回は必要が無いデータとなる。それを弾いて住所表示用だけをPickする。
                    /// (後者は大体Nameに「MajorRoad」とかが入ってる)

                    if (null != response.LocationData[i].GeocodeLocations && 0 < response.LocationData[i].GeocodeLocations.Count)
                    {
                        if (Bing.Maps.Search.GeocodeLocationUsageType.Display == response.LocationData[i].GeocodeLocations[0].UsageType)
                        {
                            /// 'Display'が複数ある場合は… 知らないし 最初の一つだけPickする
                            addr = response.LocationData[i].Address;
                            break;
                        }
                    }
                }

                if( null != addr )
                {
                    /// StrWebQuery ... "ごみ+収集日+"
                    /// AdminDistrict ... "神奈川県" 大体県まで
                    /// AdminDistrict2 ... データ見たこと無いのだけど 郡とか?
                    /// Locality ... "川崎市高津区" 大体市+区まで
                    /// AddressLine ... "foo町bar丁目1-2-3" 最後のアドレス ごみ収集日検索では却って結果出なくなるので使わない
                    
                    query = loader.GetString("StrWebQuery"); 
                    if( 0 < addr.AdminDistrict.Count() ) query += (addr.AdminDistrict + "+");
                    if( 0 < addr.AdminDistrict2.Count() ) query += (addr.AdminDistrict2 + "+");
                    if( 0 < addr.Locality.Count() ) query += (addr.Locality );
                }

                // URL形式にエンコ %1234...みたいなやつ
                query = System.Net.WebUtility.UrlEncode(query);

                // ブラウザ起動
                await Windows.System.Launcher.LaunchUriAsync(new Uri( "http://www.google.com/search?q=" + query));
            }
            else
            {
                // 何もすることが無い
                // Errormsgも出さなくていいかなと
            }
        }
    }