2015年7月4日土曜日

Windowsの「追加の時計」をいじる

さて、Windowsには追加の時計というものがありますね。


このように、タスクバーの右下の時計を押したら出てくるやつに世界時計を2つまで追加できる機能です。家族、職場の知り合い等が海外に出かけているとき、これを設定しておくとその人が今何時なのかが一目でわかるので、電話を掛けるタイミングが見当付くようになるなど、とても便利なものです。

しかしこれ、設定のUIがとてつもなく不便です。


このように、一覧から表示するタイムゾーンを選択しなければなりません。しかも、表示されるのは国名ではなく、都市名、もしくは地域名です。そして、このリストにはUTCとの時差が同じ都市がいくつもありますが、これの選択を誤るとサマータイムの反映がうまくされず、実際とは異なる時刻が表示されることがあります。
時刻を表示したい地域について、周辺都市やタイムゾーンを熟知している人なら簡単に選択できるでしょうが、それができるのは多分その国に住んでいる人か、もしくはよほどの地理オタクくらいなものです。

すなわち、普通に使うには、これはとても不便なのです。

これをもうちょっと、例えば地図で選択した地点から直接タイムゾーンを判定して表示したり、 国名を入力したら自動的に選択してくれたり、はたまたTwitterの位置情報からタイムゾーンを入力したりするようなUIが必要ですよね。
そういった設定ができるようなソフトウェア開発について考えていきましょう。

「追加の時計」の設定を変更する

さて、それではどうやってこの時計を設定すればいいでしょう。
たいてい、こういうのはレジストリに書き込まれていますから、レジストリでそれっぽいキーを探します。
そうすると、見事に出てきました。

HKEY_CURRENT_USER\Control Panel\TimeDate\AdditionalClocks

ここに「1」というキーと「2」というキーがあり、これがそれぞれ追加の時計1,2に対応しています。


値は非常にシンプルです。
DisplayNameは表示名ですね。時計のタイトルで、任意の文字列です。
Enableはその時計が有効かどうかです。0で無効、1で有効ですね。
TzRegKeyNameがタイムゾーンを表すキー名です。なんのこっちゃってなるかもしれませんが、HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zonesに格納されている各タイムゾーンに対応するレジストリキー名となります。

さて、となると、このタイムゾーンのレジストリキーまでパースしてやらなきゃいけないのかという話になりそうですが、そんなことはありません。.NETのTimeZoneInfoクラスIdプロパティの値が、このレジストリキーに対応しています。TimeZoneInfoはGetSystemTimeZonesメソッドによって一覧を取得できるので、これを適当に選択してレジストリに書き込んであげれば時計は設定できるわけですね。

というわけで、追加の時計を変更するメソッドは下記のような形になります。

/// <summary>
/// 「追加の時計」に時刻を設定するメソッド
/// </summary>
/// <param name="ClockNumber">時計の番号。1または2</param>
/// <param name="DisplayName">表示名</param>
/// <param name="IsEnable">有効にするか、無効にするか。</param>
/// <param name="TimeZone">設定するタイムゾーン</param>
public static void SetAdditionalClock(int ClockNumber, string DisplayName, bool IsEnable, TimeZoneInfo TimeZone)
{
    if(ClockNumber < 1 || ClockNumber > 2)
        throw new ArgumentOutOfRangeException("ClockNumber");
    if(IsEnable) {
        if(DisplayName == null)
            throw new ArgumentNullException("DisplayName");
        if(TimeZone == null)
            throw new ArgumentNullException("TimeZone");
    }

    using(RegistryKey Key = Registry.CurrentUser.CreateSubKey(string.Format(@"Control Panel\TimeDate\AdditionalClocks\{0}", ClockNumber))) {
        if(IsEnable) {
            Key.SetValue("DisplayName", DisplayName, RegistryValueKind.String);
            Key.SetValue("Enable", 1, RegistryValueKind.DWord);
            Key.SetValue("TzRegKeyName", TimeZone.Id, RegistryValueKind.String);
        } else
            Key.SetValue("Enable", 0, RegistryValueKind.DWord);
    }
}

今まで追加の時計を設定したことが無い環境ではレジストリキーすら存在しないようなので、そのような環境のためにCreateSubKeyにてキーを作成(もしくは存在する場合はそれを開く)しています。
あとは上記の3つのパラメーターを書き込んであげているだけなので至ってシンプルです。これで追加の時計の設定ができてしまいます。驚くほどあっさりしていました。

緯度経度からタイムゾーンを取得する

さて、追加の時計の設定変更は簡単にできましたが、そもそもTimeZoneInfo.GetSystemTimeZonesが先ほどのWindows標準のタイムゾーン一覧を表示するコンボボックスの内容を返すだけなので、どうにかして何かしら別の情報(緯度経度など)をタイムゾーンに変換する必要があります。

まあ、もっともお手頃そうなのはGoogle Time Zone APIですかね。
時刻と緯度経度を指定すると、JSONでその夏時刻を含めたタイムゾーンを返してくれます。なので、いつも通りJson.NETを使ってタイムゾーンを取得するクラスを作りました。

public class GoogleTimeZone
{
    private GoogleTimeZone(InternalGoogleTimeZone Source)
    {
        DaylightSavingTimeOffset = TimeSpan.FromSeconds(Source.dstOffset);
        RawOffset = TimeSpan.FromSeconds(Source.rawOffset);
        Status = Source.status;
        TimeZoneId = Source.timeZoneId;
        TimeZoneName = Source.timeZoneName;
    }

    /// <summary>
    /// サマータイムのオフセット
    /// </summary>
    public TimeSpan DaylightSavingTimeOffset { get; private set; }

    /// <summary>
    /// サマータイム関係なしのオフセット
    /// </summary>
    public TimeSpan RawOffset { get; private set; }

    /// <summary>
    /// ステータス
    /// </summary>
    public string Status { get; private set; }

    /// <summary>
    /// タイムゾーンのID
    /// </summary>
    public string TimeZoneId { get; private set; }

    /// <summary>
    /// タイムゾーン名
    /// </summary>
    public string TimeZoneName { get; private set; }

    public override string ToString()
    {
        if(Status != "OK")
            return Status;
        else {
            string RawOffsetText = (RawOffset < TimeSpan.Zero ? "-" : "+") + RawOffset.ToString("hh\\:mm");
            string DSTText = string.Empty;

            if(DaylightSavingTimeOffset != TimeSpan.Zero) {
                DSTText = (DaylightSavingTimeOffset < TimeSpan.Zero ? "-" : "+") + DaylightSavingTimeOffset.ToString("hh\\:mm");
                DSTText += "(DST)";
            }

            return string.Format("(UTC{0}{1}) {2}/{3}", RawOffsetText, DSTText, TimeZoneName, TimeZoneId);
        }
    }

    public static async Task<GoogleTimeZone> CoordinateToTimeZoneAsync(double Latitude, double Longitude, DateTime? UtcTime = null, string ApiKey = null)
    {
        if(UtcTime == null)
            UtcTime = DateTime.UtcNow;

        long UnixTime = (long)(UtcTime.Value - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds;

        string url = string.Format(@"https://maps.googleapis.com/maps/api/timezone/json?location={0},{1}&timestamp={2}", Latitude, Longitude, UnixTime);

        if(!string.IsNullOrEmpty(ApiKey))
            url += "&key=" + ApiKey;

        string json;

        using(WebClient wc = new WebClient()) {
            json = await wc.DownloadStringTaskAsync(url);
        }
        return new GoogleTimeZone(JsonConvert.DeserializeObject<InternalGoogleTimeZone>(json));
    }
}

internal class InternalGoogleTimeZone
{
    public int dstOffset { get; set; }
    public int rawOffset { get; set; }
    public string status { get; set; }
    public string timeZoneId { get; set; }
    public string timeZoneName { get; set; }
}

Json.NET用に単純なInternalGoogleTimeZoneクラスを作り、そこからプロパティのアクセスレベルや型をしっかり作ったクラスに変換してあげる形にしています。これで、緯度経度をタイムゾーンに変換できるようになりました。
ちなみに、このAPIでは緯度経度のほかに時刻を渡す必要があります。その時刻によって、サマータイムの季節かどうかを判別してくれるようです。

GoogleのタイムゾーンをWindowsのタイムゾーンに変換する

さて、Googleからタイムゾーンが取得できるようになりましたが、Windowsの形式とは若干異なります。UTCからの時差はどちらも出ますが、サマータイムが採用されているかどうかの判別はGoogleのほうには無く、というよりも、タイムゾーンを求める段階で時刻を指定してサマータイム中ならばサマータイム中だという返答が返ってくるだけになります。
なので、この辺を上手く扱って、GoogleのタイムゾーンをWnidowsのタイムゾーンに変換してやらねばなりません。これは結構苦労しました。

 結果から言うと、下記の手順で求めました。
  1. 子午線が一致するタイムゾーンをWindowsのタイムゾーンのリストから抜き出す
  2. 都市名等を単語ごとに分解し、都市名にかかわらないstandard, time, daylight, city, summerなどの文字列を除外する(それで残った単語を『キーワード』と呼ぶことにする)
  3. GoogleタイムゾーンとWindowsタイムゾーンで、一致するキーワードの個数がもっとも多いペアが、そのGoogleタイムゾーンに対応するWindowsのタイムゾーンということにする。キーワードの個数が同じ組み合わせが複数あった場合は、キーワードの個数がもっとも少ないものを採用する。
まあ何を言いたいかというと、Googleのタイムゾーンでは、例えばロサンゼルス周辺のタイムゾーンを取得しようとすると
America/Los_Angeles
Pacific Standard Time
の2種類の文字列を返してきます。一方、Windowsのタイムゾーンでは
Pacific Standard Time
という文字列が返ってきます。
この「Pacific」という文字列の一致から、対応するタイムゾーンを探し出そうというわけです。
Windowsのタイムゾーンにはほかにも
Pacific Standard Time (Mexico)
などがあります(メキシコの太平洋標準時)が、それらはGoogleのタイムゾーン側にMexicoとかそういった単語が無い場合は最も短い名前の都市名と一致を掛けるので、該当しなくなるわけです。

public async Task GetTimeZone()
{
    GoogleTimeZone gtz = await GoogleTimeZone.CoordinateToTimeZoneAsync(Latitude, Longitude);

    var WindowsTimeZones = TimeZoneInfo.GetSystemTimeZones().Where(p => p.BaseUtcOffset == gtz.RawOffset)
        .Select(p => new { Source = p, Keywords = TimeZoneTextToCityKeywords(p.Id) }).ToArray();
    var GoogleTimeZoneTexts = TimeZoneTextToCityKeywords(gtz.TimeZoneId + " " + gtz.TimeZoneName);

    //子午線が一致する都市のうち
    //1. 一致するキーワードが多いもの
    //2. そのうち、キーワードの長さが最も短いもの
    //を抽出する
    TimeZone = WindowsTimeZones.Select(p => new {
        Source = p.Source,
        SameTextCounts = p.Keywords.Concat(GoogleTimeZoneTexts).Count() - p.Keywords.Concat(GoogleTimeZoneTexts).Distinct().Count(),
        SourceTextCounts = p.Keywords.Count()
    }).OrderByDescending(p => p.SameTextCounts).ThenBy(p => p.SourceTextCounts).First().Source;
}

/// <summary>
/// タイムゾーンのテキストからその地域名に係るキーワードを抜き出すメソッド
/// </summary>
/// <param name="TimeZoneName"></param>
/// <returns></returns>
private static string[] TimeZoneTextToCityKeywords(string TimeZoneName)
{
    string[] spacers = { "/", "_" };
    string[] removes = { "(", ")", "standard", "time", "daylight", "city", "summer" };

    TimeZoneName = TimeZoneName.ToLower();

    foreach(string s in spacers)
        TimeZoneName = TimeZoneName.Replace(s, " ");
    foreach(string r in removes)
        TimeZoneName = TimeZoneName.Replace(r, string.Empty);

    return TimeZoneName.Split(' ').Where(p => !string.IsNullOrEmpty(p)).OrderBy(p => p).Distinct().ToArray();
}

実装はこんな感じになっています。
まあ、本当にこれで完全かと聞かれるとちょっと自信は無いですが、少なくとも最初に子午線の一致を確認しているので、誤判定があったところでサマータイムの問題くらいでしょう。
もしも問題が見つかれば個別にちょっと考えていこうとは思っています。

LINQ to Twitterで位置情報を取得する

さて、緯度経度をどこから持ってくるかと言う話ですが、海外に行っていることをTwitterでつぶやいている人がいたので、Twitterから持ってくることにしました。

しかしこれ、意外とドツボです。

最近、Twitter公式アプリとかを見てもらうとわかりますが、位置情報をつぶやくときに「正確な位置を共有」とかいうオプションがあります。従来のTwitterでは緯度経度ベースの正確な位置情報しかつぶやけませんでしたが、個人情報に配慮したのでしょうか、最近はこういうオプションが生まれました。
そのせいで、位置情報にかかわる機能がややこしくなっているんですね。

Twitterが返す生のJSONまでは調べていませんが、LINQ to Twitterではそのあたりが下記のような仕様になっているようです。

従来の(正確な)位置情報の場合

Status.Coordinates.Latitude
Status.Coordinates.Longitude
に緯度経度が入っています。正確な位置情報が共有されていない場合や、そもそも位置情報を付けてつぶやいていない場合は緯度経度がともにゼロになっています。
なお、正確な位置情報が共有されている場合でも、下記の大雑把な位置情報も入っていますので、そちらも併せて利用することができます。

大雑把な位置情報の場合

Status.Coordinatesはnullにはなりませんが、緯度経度がともにゼロになります。
そして、Status.Place.BoundingBox.Coordinatesに緯度経度がいくつか(多分4つ)入ります。Twitterの開発者向けサイトには
A bounding box of coordinates which encloses this place. 
と書いてあるので、その位置情報のエリアを囲う座標が格納されているという意味なのでしょう。
というわけで、正確な位置情報が共有されていない場合は、そのエリアの頂点の各座標の重心(座標の平均値)を現在位置とすることとしました。

ちなみに、このStatus.Placeには地名の情報も入っています。

Status.Place.FullName: 地域名(東京都千代田区など)
Status.Place.Country: 国名(日本など)

なので、このあたりも使えそうですね。


昔のTwitterクライアントなんかではこの大雑把な位置情報には対応していなかったりしますが、特に今回のタイムゾーンを表示する程度の場合では大雑把な位置情報でも問題はないので、積極的にこちら側の情報も使っていくこととしました。



まあ要するに、
  1. Twitterから緯度経度を取ってくる
  2. Google Time Zone APIでその緯度経度に対応したタイムゾーンを調べる 
  3. キーワード比較でWindowsのタイムゾーンに変換する
  4. その設定を時計に転送する
みたいな流れで時計を自動設定できるようなソフトを作ってみました。はい。
できればGoogleマップで表示している場所のタイムゾーンをみたいな機能も付けたかったのですが、何せWPFのWebBrowserコントロールの闇が深すぎて…

Twitter to WorldClock ver.0.1.0
 ※今回は諸事情あってソースコードは配布しておりません。悪しからず。

2015年7月1日水曜日

NTP時計のうるう秒

さて、先ほどうるう秒が実施されました。
我が家でも無事、NTP時計がうるう秒を表示してくれました。


うるう秒に対応していない時計では、うるう秒後に時計が1秒ずれた状態になります。


もちろん、その後の時計合わせでずれた時刻は修正されるので、対応していない時計でも後に正確な時刻を刻むようになるわけですがね。

それでは、NTP時計の様子は動画で撮影していましたので、こちらを御覧ください。


うるう秒がちゃんと表示できていることがわかると思います。また、右上の液晶に示したLeap Indicator (LI)ですが、8:59:60→9:00:00の遷移の時に0に切り替わっていることが分かるかと思います。これは、単にうるう秒が終わったら0にリセットしているだけで、直ちにNTPサーバーに問い合わせているわけではありません。LIの存在をうるう秒の1秒に読み替えていただけですからね。はい。

にしてもこのレンズ、液晶にピント合わせたら7セグがピンボケするんだなあ…。絞りをもうちょいと絞れば良かった…。

2015年6月30日火曜日

うるう秒前日

さて、本日は新幹線で焼身自殺由来の火災が発生したり、箱根が噴火警戒レベルが3に引き上げられたりと個人的に注目度の高いニュースが多いですが、それに加えて、ついにうるう秒前日となりました。

NTP時計のうるう秒対応

私が作ったNTP時計はすでにうるう秒に対応させており、今回は初めてその実証となります。
ワクワクしています。

そして、本日の9時過ぎ、ついにLeap Indicatorが降ってきました。


NTP時計上部に付いた液晶にLIという項目があり、そこに1と表示されているのが分かるかと思います。Leap Indicatorは2bitで、0が警告なし、1がその日の最後(UTC)に1秒追加されるタイプのうるう秒がある、2が1秒減らされるタイプのうるう秒があるという意味になります。すなわち、NTP時計は本日6/30の最後(=日本標準時の7/1 9:00:00)にうるう秒として1秒を挿入する準備ができている状態になっていることを示しています。

さて、このうるう秒に関して、例えばNICT(情報通信研究機構)では8:59:60を見ようというイベントが行われます。

午前8時59分60秒を見てみませんか?(2015年7月1日(水) 開催)

いつもうるう秒関係のニュースなどで出てくるあの時計ですね。
詳細のPDFなんかには「一般の時計ではこの60秒を表示することはできません」などの記述がありますが、私の家のNTP時計では見ることができます(ドヤァ

というわけで、私は朝9時は自宅待機です。NTP時計でこの8:59:60を見届けるつもりです。楽しみだなあ。

2015年5月27日水曜日

東京メトロAPIをC#で使う

お久しぶりです。
ちょっと最近いろいろと忙しくて、なかなかブログを書いている暇がありませんでした。プログラミングはちょくちょくやっていましたが。

ここ最近はまっているのは、表題の通り東京メトロのAPIです。

https://developer.tokyometroapp.jp/

昨年の9月に東京メトロは、駅情報や列車の運行情報、在線情報などを提供するためのAPIを公開しました。この時は、コンテストに参加する目的で利用が可能でしたが、そのコンテストが終わった後、今年4月から特にそういった目的でもなくAPIが使えるようになりました。
上記の東京メトロの開発者向けサイトに行き、必要な情報を入力してアカウントを取得することでAPIにアクセスするためのアクセストークンを入手することができます。

https://developer.tokyometroapp.jp/app

こちらにはコンテストに参加したアプリケーションや、コンテスト終了後に登録されたアプリケーションなど、様々なアプリケーションがあります。
最近流行りなのはやはりスマホアプリということで、コンテストでもスマホアプリに限定して募集されていましたし、その後もそういったアプリケーションの登録が非常に多いように見受けられます。
その中で、こんなアプリケーションがありました。

Tokyometro4J

東京メトロAPIのJavaラッパーです。

ん?じゃあこれは.NETラッパーを作るしかないな?

というわけで、作ってみました。

TokyoMetro.NET ver.1.0.0

ゆうて、やることはそんなに大変じゃないです。
URLにクエリとアクセストークンを入れてアクセスしたら、それに応じた各種情報をJSONで返してくれるだけです。なので、JSONを適宜パースして、適当なクラスに入れてやればいいだけです。JSONのパースもJson.NETに投げちゃいましたから、実質、ラッパークラスの実装とURLの作成がほとんどですかね。
それをやるだけなんですが、まあいろいろなデータがあるので、それぞれにおいてラッパークラスを実装してやるのは結構面倒でした。あと、最近流行りの非同期処理です。これを実現するために、2倍のメソッドの実装が必要でした。○○Asyncっていうメソッドを実装してやらなきゃいけないですからね…。

上記のライブラリ、例によってデモソフトとしてライブラリを使ったフロントエンドも同梱しています。普通にライブラリを使う分には何も問題無いですが、「とりあえずデモソフト立ち上げよう」と思った方、ごめんなさい。デモソフトはダウンロードしてそのままコンパイルするだけじゃ動きません。動くようにするためには然るべき場所にアクセストークンを入力し、コンパイルする必要があります。
アクセストークンは他人に漏らすわけにはいかないですし、.NETに関してはこんなソフトなどを使えば超簡単に逆コンパイルができてしまいます。オープンソースではもちろんのこと、難読化しない限り、なかなかこういうソフトをexeファイルですら公開しにくいんですよね。
難読化ソフトも無料のものではなかなか最新の.NETバージョンに対応していなかったり、そもそも文字列の難読化に対応していなかったりして使い物になりません。有料のは手を出すのがなかなか大変ですよね。

というわけで、自分で何かソフトを開発する方はもちろん、デモソフトを動かしてみたいと思った方でも東京メトロに開発者登録をして使うようにしてください。ごめんなさい。でもご理解の程お願いいたします。


さて、このライブラリは多分全部のAPIを網羅しています。見落としがあったらごめんなさい。
使い方はとても簡単で、

var api = new TokyoMetroApi("Consumer Key");

このようにTokyoMetroApiクラスのコンストラクタにアクセストークンを与えてインスタンス化するだけで準備は完了です。あとは、Search○○DatapointAsync()みたいな名前のメソッドを呼ぶだけで大体のデータは読み出せます。これらのメソッドは条件を指定することができて、例えば

var fukutoshin = (await api.SearchRailwayDatapointAsync(SameAs: IDs.Railway.TokyoMetro.Fukutoshin)).Single();

のように名前付き引数で条件を指定することで、この場合は副都心線のみの路線情報を取得しています。例えばRailwayFareなんかは条件を何も指定しないとあらゆる2駅間の組み合わせ(多分)の運賃が返ってくるので、サーバーにもクライアントにもネットワークにも大きな負荷を掛けることになってしまいます。必要十分なデータのみを読み出すことで、サーバーへの負荷を抑えましょう。
一応、こういった条件指定も少しだけできるので、LINQ to TokyoMetroとか実装できれば面白そうだなーって思ったりもしましたが、そもそもLINQの実装の話はあまり詳しくなかったり、LINQレベルでwhereが詳細に指定でいるわけでもないので、今回は見送りました。

ちなみに、非同期メソッドは末尾にAsyncが付いていて、付いていない場合は同期的に実行されます。ウェブアクセスが絡むメソッドは全部Asyncの有りバージョンと無しバージョンの2種類を実装しているはずですので、目的に合わせて使い分けてください。

それでは、各データのクラスの代表として、駅データを見てみましょう。

public class Station : ResponseBase
{
    internal Station(InternalStation Source) : base(new UCode(Source.Id))
    {
        //省略
    }

    public DateTime Date { get; private set; }

    public ID SameAs { get; private set; }

    public string Title { get; private set; }

    public string Region { get; private set; }

    public ID Operator { get; private set; }

    public ID Railway { get; private set; }

    public ReadOnlyCollection<ID> ConnectingRailway { get; private set; }

    public ID Facility { get; private set; }

    public ReadOnlyCollection<ID> PassengerSurvey { get; private set; }

    public string StationCode { get; private set; }

    public ReadOnlyCollection<UCode> Exit { get; private set; }

    public Coordinate Coordinate { get; private set; }

    public override string ToString()
    {
        return string.Format("{0} ({1})", string.IsNullOrEmpty(Title) ? string.Empty : Title, Railway == null ? string.Empty : Railway.Title);
    }
}

例えば、Stationクラスにはこのように幾つかIDクラスのプロパティが含まれています。これは、例えば"odpt.Railway:TokyoMetro.Ginza"などの文字列で定義されるIDが含まれています。
そっくりそのままこれをSearchRailwayDatapointAsync()メソッドのSameAs引数にセットして呼び出せば路線情報を引っ張り出せます。また、主要なIDはIDsクラスの中に固定データとして入れてありますので、そこから使うと良いでしょう。また、IDクラスはコンストラクタを持っておらず、ID.CreateOrGet()メソッドを通してインスタンス化する必要があります。シングルトン(と言っていいのかわかりませんが)のためですね。

Railwayとかその他のクラスに関してはVSのオブジェクトブラウザーでクラス一覧を見るなり何なりして使ってもらえればいいかと思います。

ちなみに、東京メトロAPIにはエラッタが多数あります。
コンテストに使われていた関係で、終盤にはAPIのエラッタを修正すると逆にアプリに問題を与えてしまう可能性があるので(コンテスト期間中はアプリのアップデートは禁止されていた)、不合理な仕様や単なるスペルミスやデータミスでもエラッタに放り込んで修正をしなかった背景があるようです。
これに関しては、私のライブラリではまあ最終的にはエラッタに相当するデータは修正した上で提供すると言うスタンスで設計しようかと思っておりますが、手を抜いて全部のエラッタを実装していません。と言うよりか、丸ノ内線支線にかかわるエラッタとスペルミス周辺しか修正していなかったと思います。ごめんなさい。
いやいや、アプリのコンテスト期間が過ぎたのでそろそろ東京メトロさんにはエラッタを直していただきたく…

とまあ、このへんがこのライブラリの概要です。
そうして、このライブラリを駆使して駅の緯度経度や列車の位置情報を求めて、リアルタイムに表示させるソフトを作りました。それがデモソフトです。デモソフトを眺めている様子を早回ししたものが下記の動画のようになります。



この日はたまたま14時半頃に東京で震度4の地震があって、緊急地震速報で全列車が停車する様子が見られました。面白いですね。
あ、別にデモソフトを使ったからってこういう動画が撮れるようになるわけじゃないです。これはあくまでもこのソフトを使って現在位置や運転情報が見られるよ~っていう様子を動画で示しただけなので…。

2015年1月27日火曜日

LINQメソッドのストリーミングと非ストリーミング

この前私の無知ゆえにTwitterで議論を起こしてしまったのでここにまとめておきます。

LINQの基本的な思想として遅延評価というものがあります。

Console.WriteLine("Start");
var e = Enumerable.Range(0, 10).Select(p => { Console.WriteLine(p); return p; });
Console.WriteLine("foreach Starting");
foreach(int i in e)
    Console.WriteLine("foreach " + i);

例えばこんなプログラムを実行したらどのような結果が出力されるでしょう。
Start
foreach Starting
0
foreach 0
1
foreach 1
2
foreach 2
3
foreach 3
4
foreach 4
5
foreach 5
6
foreach 6
7
foreach 7
8
foreach 8
9
foreach 9

こんな出力がされます。Select()メソッドが呼ばれたときではなく、値が必要になったときに値が列挙されています。これが遅延評価です。
では、次のようなコードの場合どうなるでしょう。Select()の後ろにReverse()が付きました。

Console.WriteLine("Start");
var e = Enumerable.Range(0, 10).Select(p => { Console.WriteLine(p); return p; }).Reverse();
Console.WriteLine("foreach Starting");
foreach(int i in e)
    Console.WriteLine("foreach " + i);

実行結果はこんな感じになります。
Start
foreach Starting
0
1
2
3
4
5
6
7
8
9
foreach 9
foreach 8
foreach 7
foreach 6
foreach 5
foreach 4
foreach 3
foreach 2
foreach 1
foreach 0

先ほどと違って、foreachで各値が列挙されるごとに1回ずつSelect()に与えたデリゲートが呼ばれるのではなく、foreachが始まるときに全ての値が評価されたうえで逆順に列挙されています。
というのも割と当然で、IEnumerator<T>は現在の値を示すCurrentプロパティと次の値へ移動するMoveNext()メソッドしか持っていません(まあReset()とかDispose()とかありますがここでは関係ないので置いておきます)。すなわち、コレクションを逆順にたどることができないので、Reverse()が返すIEnumerable<T>のオブジェクトは、いったん全ての値を評価して配列とかにストックしておき、それを逆順に返すことでその機能を実現しています。

私は、これを遅延評価とは言わないと思ってしまっていました。
最初の例のように、その値を使用しようとしたときに初めて評価するような機構が遅延評価であり、そうすることで分散してデータベースにアクセスすることができるメリットが享受できるところまで含めて遅延評価の定義だと認識していました。そのため、いったん全ての値を評価しないと最初の値を提供できないReverse()メソッドは遅延評価に当たらないと思っていました。

しかし、上記のWikipediaを見ていただくとわかるように、遅延評価そのものの定義には分散といったような言葉は出てきません。遅延評価では分散して実行できることは保証されておらず、都合が良いデータならもしかしたら分散して処理できるかもしれないくらいのもののようです。いわば、分散して処理ができるのは遅延評価の効果でしかなく、あくまでも「必要になった時に必要最低限の評価をする」が遅延評価です。

後者の場合もforeachが始まるまで値は評価されていません。なので遅延評価です。ただし、始まった瞬間に全ての値を評価しないと逆転させた時の最初の値(=元のコレクションの最後の値)がわからないので、必要最小限の評価として全ての値を評価しているというだけのことです。

私のこのもともとの発想は実は遅延評価とは若干違ったところで存在していたようです。

実行方法による標準クエリ演算子の分類

LINQには「即時評価」と「遅延評価」の2種類のメソッドがあり、その遅延評価のメソッドはさらに「ストリーミング」と「非ストリーミング」に分類できるようです。私がもともと遅延評価の定義だと勘違いしていた「実行時に分散的に評価を行う」メソッドがここでいうストリーミングに分類されます。逆に遅延評価ではあるけど、評価開始時にすべての値を評価してしまうのが「非ストリーミング」に分類されています。
例えばSelect()はストリーミングですが、Reverse()は非ストリーミングになっています。なるほど納得です。
ちなみに、ストリーミングと非ストリーミングの両方にチェックが付いているものがあります。例えばExcept()なんかがそれです。2つ以上のコレクションを扱うメソッドのときは両方の特性が付く場合があるようで、Except()の場合は例えばsecondで与えたコレクションを全て評価しないとその値を持つかどうかがわからないので、そういった仕様になっているということです。firstの値そのものはストリーミングで実行されます。


これでまた一つ賢くなりました。
今後LINQを使うときは単に「遅延評価」だけでなく、この辺も意識しながら使いたいですね。

2015年1月24日土曜日

WPF用縦書きテキストブロック Tategaki ver.2.1.0

さて、前回はコントロールの更新だったわけですが、今回はロジックの更新になります。

とは言っても、自分はあまりフォント周りに詳しくないうえ、この辺のことは昔からWindowsシステムから上位のAPIが提供されていたこともあり、あまり情報は転がっていません。しかし、かねてから、Uniscribeに頼らず何かしらの方法でフォントファイルを直接読み出してグリフ変換等ができればいいなあと思っておりました。
今までのUniscribeを使ったTategakiTextを使っていただくとわかると思うのですが、MSゴシックやMS明朝、メイリオと言ったような代表的なフォントは何とかちゃんと動くのですが、それ以外のフォントを使うと動かないものもしばしばあり、もっと言えば、TrueType Collectionのフォントファイル(拡張子が.ttcのやつ)の扱い方が分からなかったのでMS Pゴシックなどが使えないという問題もありました。さらに、不定期で発動する、なぜかグリフインデックスを縦書き用に変換できない問題など、非常に悩ましい現象がしばしばありました。

しかし、昨日ネットを巡回していると、非常に興味深いライブラリを見つけました。

TypeLoader

もはやなんで気づかなかったんだというレベルの話ですが、TrueTypeまたはOpenTypeのフォントファイルを直接パースしてグリフインデックスを変換してくれるライブラリになります。まさに求めていたものです。
と、同時に、このトップページを見てもらってもわかりますが、結構このライブラリもWPFを視野に入れたライブラリになっているようなので、こっちに立派なWPF縦書きコントロール機能があったら、このプロジェクト自体完全にリサーチ不足による車輪の再発明だったことになっちゃうじゃんという不安もよぎりました。実際、デモプログラムも付いていましたが、コードを読んでみるとMainWindowのコードビハインドにガッツリグリフインデックス変換まわりのコードが書いてあって、私のこのライブラリのコントロールとしての立ち位置も保てそうで一安心でした。
 

さて、こうと来たらグリフインデックスの変換周りをUniscribeからTypeLoaderに置き換えましょう。
TypeLoaderにはTrueType Collectionフォントを扱う機能もあるようなので、ついでに、使用可能フォントのサーチも強化して、より多くのフォントに対応していくことにしました。

/// <summary>
/// 有効なフォントの一覧を取得するメソッド
/// </summary>
/// <param name="cultures">フォントのカルチャの配列</param>
/// <returns>ファミリ名とstringのDictionary</returns>
internal static IDictionary<string, Uri> SearchFontNamePathPair(IEnumerable<CultureInfo> cultures)
{
    IDictionary<string, Uri> dic = new SortedDictionary<string, Uri>();
    string FontDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);

    var uris = Directory.GetFiles(FontDir, "*.ttf").Concat(Directory.GetFiles(FontDir, "*.otf")).Select(p => new Uri(p))
        .Concat(Directory.GetFiles(FontDir, "*.ttc").SelectMany(p => {
            using(var fs = new FileStream(p, FileMode.Open, FileAccess.Read)) {
                return Enumerable.Range(0, TypefaceInfo.GetCollectionCount(fs)).Select(i => new UriBuilder("file", "", -1, p, "#" + i).Uri);
            }
        })
    );

    foreach(Uri uri in uris) {
        try {
            GlyphTypeface gtf = new GlyphTypeface(uri);
            if(cultures.Where(p => gtf.FamilyNames.ContainsKey(p)).Count() > 0) {
                foreach(string FamilyName in gtf.FamilyNames.Values) {
                    if(!dic.ContainsKey(FamilyName))
                        dic.Add(FamilyName, uri);
                }
            }
        }
        catch(NullReferenceException) { }
    }

    return dic;
}

まずはフォントファミリ名とフォントファイルのUriを対応させるお仕事をします。システムディレクトリのフォントフォルダのパスを取得し、そこから.ttf、.otf、.ttcの3つの拡張子のファイルをすべて拾います。.ttcに関してはTypeLoaderのTypefaceInfo.GetCollectionCount()メソッドを使っていくつフォントが格納されているかを調べ、それぞれを登録していきます。
次に、UriをもとにGlyphTypefaceのインスタンスを作成し、ファミリ名を取得し登録していきます。それだけです。

GlyphTypeface gtf = new GlyphTypeface(FontUri);
TypefaceInfo ti = new TypefaceInfo(GlyphTypeface.GetFontStream(), string.IsNullOrEmpty(FontUri.Fragment) ? 0 : int.Parse(FontUri.Fragment.Replace("#", "")));
SingleGlyphConverter vert = ti.GetVerticalGlyphConverter();

ushort[] indices = Text.Select(p => vert.Convert(gtf.CharacterToGlyphMap[gtf.CharacterToGlyphMap.ContainsKey(p) ? p : '?'])).ToArray();

変換はこんな感じになります。GlyphTypefaceのインスタンスからTypefaceInfoとSingleGlyphConverterのクラスを作れば、あとはSingleGlyphConverter.Convert()を通して縦書き用グリフインデックスに変換することができます。

ですが、 TategakiMultilineは1文字ずつTategakiTextインスタンスを作っているので、TategakiMultiline.Text更新時はかなりの回数この変換ルーチンが呼ばれることになります。毎回フォントファイルをパースしていてはコストが大きいので1度生成したインスタンスは保存し、グリフインデックスはキャッシュするようにしています。まあでもそのキャッシュ機構を説明したところでわかりにくくなるだけでしょうから、興味ある人はソースコードでも眺めてください。


気が付いたらあっという間にver.1系のコードが跡形もなくなってしまいましたね。


ダウンロードはこちら
WPF用縦書きテキストブロック Tategaki ver.2.1.0

2015年1月21日水曜日

WPF用縦書きテキストブロック Tategaki ver.2.0.0

なんてういか、公開時に迂闊ににバージョンを1にしてしまったせいで、今回早速バージョン2になってしまいました。

今回は、Uniscribeのロジック以外ほぼゼロベースで作りなおしています。そのため、一部互換性が無かったり仕様が変更されていたりするところがあるので注意してください。


今までのTategakiTextとTategakiMultilineはUserControlクラスを使って作っておりました。UserControlだとUIデザイナーの支援が受けられるなどのメリットはあるのですが、まあ一番の理由は自分が詳しい知識がなかったというところですかね。

代わりに、Controlクラスを継承した、「カスタムコントロール」としてTategakiTextとTategakiMultilineを実装しました。 そのため、FontFamilyやFontSize等のプロパティを設定しなければそのまま親の値が引き継がれるなど、コントロールとして非常に自然な振る舞いをできるようになりました。

また、グリフインデックスのキャッシング機能を付けて、Uniscribe周りの関数の呼び出しを最小に抑え、高速化しました。これはとりあえず実装したものの、あまり高速化には効果が無かったかもしれませんね…。
他にも、今までは、Glyphsクラスは空文字列を渡すと例外を吐くため、TategakiTextではTextプロパティが空になるとダミーで半 角スペースをGlyphsクラスに渡すようにしていました。そのため、文字が無いはずなのにTategakiTextは半角スペース分の大きさを持ったコ ントロールになってしまうという現象がありました。そこで、Textが空文字列になったら、そのままVisibilityプロパティをVisibility.Collapsedにしてコントロールそのものを消滅させるようにしました。これによって、TextBlockにより近い挙動ができるようになりました。

一方、TategakiMultilineは、今までは1行の表示量を計算して横方向のStackPanelに行ごとのTategakiTextを積み重ねていくような設計にしていましたが、今回は1文字ずつTategakiTextにしてWrapPanelに入れて自動的に折り返させるようにしました。

<ItemsControl x:Name="PART_ItemsControl" RenderTransformOrigin="0.5,0.5" >
    <ItemsControl.Resources>
        <vc:LineMarginConverter x:Key="MarginConverter" />
    </ItemsControl.Resources>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <ItemsControl ItemsSource="{Binding}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <local:TategakiText Text="{Binding}" RenderTransformOrigin="0.5,0.5"
                                            Spacing="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TategakiMultiline}}, Path=Spacing}"
                                            Margin="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TategakiMultiline}}, Path=LineMargin, Converter={StaticResource MarginConverter}}"
                                            x:Name="tategaki">
                            <local:TategakiText.RenderTransform>
                                <ScaleTransform ScaleX="-1" />
                            </local:TategakiText.RenderTransform>
                        </local:TategakiText>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Vertical" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ItemsControl>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.RenderTransform>
        <ScaleTransform ScaleX="-1" />
    </ItemsControl.RenderTransform>
</ItemsControl>                        

このように、ItemsControlの中にItemsControlを入れたような構造になっていて、外側は水平方向StackPanel、内側は縦方向のWrapPanelになっています。段落ごとにWrapPanelを生成し、それをStackPanelに入れてるという形です。文字は基本的に1文字ずつTategakiTextのインスタンスにしますが、禁則処理によって改行が入っちゃいけないところはその前後の文字をつなげて1つのTategakiTextにすることで、WrapPanelに入れるだけで改行しては行けないところの前で改行が入るようにしています。
ちなみに、当初から使っているやり方ですが、StackPanelもWrapPanelも左から右へ積み重ねられる(日本語の縦書きとは逆)ので、表示テ キストを左右反転させてからWrapPanelやStackPanelに入れて、そのパネルを再び左右反転させるという手法で右から左へ積み重ねているよ うに見せています。 

void SetText()
{
    if(itemsctl != null) {
        if(!string.IsNullOrEmpty(Text))
            Uniscribe.GetGlyphs(Text, FontFamily.Source, FontSize);    //1回呼び出しておくとキャッシュされる

        IEnumerable<string> splited = (Text ?? string.Empty).Split('\n').Select(p=>string.IsNullOrEmpty(p) ? " " : p);

        itemsctl.ItemsSource = splited.Select(p => {
            if(p.Length == 0)
                return new string[] { };
            else if(p.Length == 1)
                return new string[] { p.First().ToString() };
            else {
                List<string> ret = new List<string>(p.ToCharArray().Select(p1 => p1.ToString()));

                for(int i = 0; i < ret.Count; i++) {
                    if(i > 0 && HeadForbiddenChars.Contains(ret[i].First())) {
                        ret[i - 1] = ret[i - 1] + ret[i];
                        ret.RemoveAt(i);
                        i -= 2;
                        continue;
                    }
                    if(i < ret.Count - 1 && LastForbiddenChars.Contains(ret[i].Last())) {
                        ret[i] = ret[i] + ret[i + 1];
                        ret.RemoveAt(i + 1);
                        i -= 1;
                        continue;
                    }
                }

                return ret.AsReadOnly().AsEnumerable();
            }
        }).ToArray();
    }
}

さきほどのXAMLに対応するメソッドはこんな感じになっていて、行ごとに区切った文字列を、さらに改行可能な文字単位に分割している様子がわかるかと思います。これで、文字単位でWrapPanelに入れることで実現するTategakiMultilineが作れました。

今回のTategakiMultilineではこのような実装をしたため、リサイズ時の負荷は、リサイズされるごとにTategakiTextのインスタンスを生成していた以前のものに比べて非常に軽くなっていて、特に縦方向の表示領域が狭いときにその速さが顕著に現れます。しかし、Spacingと文字のぶら下げが実装できなくなったのと、1文字ごとにTategakiTextインスタンスを生成しているので(しかもItemsControlをXAML+C#でWPFらしい書き方をしたので、すでに存在するインスタンスの再利用すらできていない)、表示テキスト変更時の負荷が信じられないほど大きくなってしまうという問題もまた新たに生まれました。しかし、例えば将来的に「半角文字だけ90°回転の横書きで表現する」とか、そういった拡張を考えた場合WrapPanelのほうが幾分か実装が楽そうな気もしますよね。
まあ、結局今までの実装と今回の実装のどちらがいいかって考えると、それは一長一短だということになるので、従来の手法のTategakiMultilineも実装しようかと思いました。が、まあ、個人的なモチベーションの都合上、それは延期することにしました。必要になったらやるかもしれません。


機能の対応表はこんな感じですかね。まあTategakiTextだけ使うなら更新しておいて損は無いかなと思います。


Spacingが削除されたので調整用スライダーを無効状態にしていますが、コントロールが作り直しにもかかわらずこのように以前のTategakiText等と同じように使えていることがわかるかと思います。

ダウンロードはこちら
WPF用縦書きテキストブロック Tategaki ver.2.0.0