Xamarin Google Maps iOS/Androidで、地図タイルキャッシュ/デジタルズーム

前記事「[Xamarin Google Maps iOS/Androidで、地図タイルアクセスの前後をフック](http://qiita.com/kochizufan/items/be19de4d9a4b6466a9cf)」を前提に、タイルのキャッシュ方法やデジタルズームの方法を書きます。
各コード、差分を書く形で。

効率の悪いコード等、指摘をいただけるとありがたいです。

## キャッシュ

キャッシュは何らかのキャッシュ機構作ってフック部分に突っ込むだけなのですが、その方法は各自の要件によって違うと思うので、どっちかっつうと **Tileオブジェクトからbyte[]を取り出す/byte[]からTileオブジェクトを作る手順**という事で…。
キャッシュはMyCacheというクラスでstatic実装されているものとします、実実装はメモリ、DB等ご自由に…。

### Android

```csharp
namespace MyApp
{
    public class MyTilesProviderInternal : UrlTileProvider
    {
        //省略
    }

    public class MyTilesProvider : Java.Lang.Object, ITileProvider
    {
        //プロパティ定義〜コンストラクタ省略

        //タイル取得
        public Tile GetTile (int x, int y, int zoom)
        {
            //キャッシュから取得
            byte[] image = MyCache.GetCachedTile (x, y, zoom);
            if (image != null) {
                //キャッシュされていればTileオブジェクトを生成して返す
                return new Tile (WebTilesProviderInternal.TILE_WIDTH, WebTilesProviderInternal.TILE_HEIGHT, image);
            }

            //標準のタイルプロバイダから画像タイル取得
            var tile = intern.GetTile (x, y, zoom);

            //キャッシュに登録
            //Tileがちゃんと情報を持っていればキャッシュ
            if (tile != null && tile != TileProvider.NoTile && tile.Data != null) {
                //byte[]を取得
                image = new byte[tile.Data.Count];
                tile.Data.CopyTo (image, 0);
                MyCache.SetCachedTile (x, y, zoom, image);
            }

            return tile;
        }
    }
}
```

### iOS

```csharp
namespace MyApp
{
    public class MyTilesProvider : TileLayer
    {
        //プロパティ定義〜コンストラクタ、タイルURL生成省略

        //タイル要求メソッド
        public override void RequestTile (uint x, uint y, uint zoom, ITileReceiver receiver)
        {
            //画像取得前のフック
            byte[] image = MyCache.GetCachedTile (x, y, zoom);
            if (image != null) {
                //byte[] => UIImageは、NSDataを経由して
                receiver.ReceiveTile(x, y, zoom, new UIImage(NSData.FromArray(image)));
                return;
            }

            var wrapped = new MyTilesReceiver (receiver);
            intern.RequestTile (x, y, zoom, wrapped);
        }
    }

    //標準のreceiverをデコレートするオリジナルITileReceiver
    public class MyTilesReceiver : NSObject, ITileReceiver
    {
        //プロパティ定義〜コンストラクタ省略

        public void ReceiveTile (uint x, uint y, uint zoom, UIImage image)
        {
            origin.ReceiveTile (x, y, zoom, image);

            //キャッシュに登録
            //imageがちゃんと情報を持っていればキャッシュ
            if (image != null) {
                //JPEGタイルの場合はAsJPEG
                var data = image.AsPNG ();
                if ( data != null ) {
                    //bytes[]列をコピー（コピーしないとエラー）
                    var bytes = new byte[data.Length];
                    System.Runtime.InteropServices.Marshal.Copy(data.Bytes, bytes, 0, Convert.ToInt32(data.Length));
                    MyCache.SetCachedTile (x, y, zoom, bytes);
                }
            }
        }
    }
}
```

## デジタルズーム

**タイル地図はズームレベルを多く準備すればするほど、提供元のストレージ容量が加速度的に必要**になるので、ズームレベル毎に最適な解像度の画像を用意できるならばともかく、 **大縮尺でも単にデジタルズームしてるだけならクライアントサイドでデジタルズームした方がずっと全体最適です**。
タイルの用意されている最大ズームを与えて、最大ズーム以上のズームに対する要求がくれば、最大ズーム時のタイルの必要部分をズームするようにします。

実装的には、最大ズーム時のタイルに何度もアクセスしますので、キャッシュとの併用がほぼ必須と思います。
それでも、キャッシュされていない最初のアクセスタイミングでは、同じタイルへの並行ネットワークアクセスが発生する可能性もあると思いますので、この辺Rxとか使えばもっとうまくできるのではないかとも思いますが、Rx詳しくないので、Amay師匠あたりが助けてくれないかと期待しつつ…。

### Android

```csharp
namespace MyApp
{
    public class MyTilesProviderInternal : UrlTileProvider
    {
        //省略
    }

    public class MyTilesProvider : Java.Lang.Object, ITileProvider
    {
        protected MyTilesProviderInternal intern;
        //タイルの存在する最大ズーム
        protected int maxZoom = 0;

        //コンストラクタ
        public MyTilesProvider (int maxZoom) : base()
        {
            this.intern = new MyTilesProviderInternal ();
            this.max_zoom = maxZoom;
        }

        //タイル取得
        public Tile GetTile (int x, int y, int zoom)
        {
            //現ズームがタイルの最大ズーム以上の場合
            if (zoom > maxZoom) {
                //現ズームと最大ズーム間のスケールファクタ
                var pow = Math.Pow (2, zoom - maxZoom);
                //最大ズームでのx,y算出
                var maxX = (int)(x / pow);
                var maxY = (int)(y / pow);

                //最大ズームでのタイル取得
                //キャッシュ機構が働くよう、internではなくthisのGetTileを叩く
                var maxTile = this.GetTile (maxX, maxY, maxZoom);
                //最大ズームタイルの画像byte[]取得
                var maxImage = new byte[maxTile.Data.Count];
                maxTile.Data.CopyTo (maxImage, 0);
                //最大ズームタイルのBitmapオブジェクト生成
                var maxBitmap = BitmapFactory.DecodeByteArray (maxImage, 0, maxImage.Length);

                //切り出す画像のサイズや切り出し原点を算出
                var size = 256.0 / pow;
                var shiftX = (x - (uint)(maxX * pow)) * size;
                var shiftY = (y - (uint)(maxY * pow)) * size;
                //切り出し
                var cropBitmap = Bitmap.CreateBitmap (maxBitmap, (int)shiftX, (int)shiftY, (int)size, (int)size);

                //Bitmapオブジェクト=>byte[]
                byte[] cropImage;
                using (var stream = new MemoryStream())
                {
                    cropBitmap.Compress(Bitmap.CompressFormat.Png, 0, stream);
                    cropImage = stream.ToArray();
                }

                //切り出し画像からタイルを生成し返す
                //ズームはタイルオブジェクトの方でやってくれる模様
                return new Tile (WebTilesProviderInternal.TILE_WIDTH, WebTilesProviderInternal.TILE_HEIGHT, cropImage);
            }

            //以下、キャッシュ機構を含めたタイル取得ロジック、省略
        }
    }
}
```

### iOS

```csharp
namespace MyApp
{
    public class MyTilesProvider : TileLayer
    {
        protected UrlTileLayer intern;
        //タイルの存在する最大ズーム
        protected uint maxZoom = 0;

        //コンストラクタ
        public MyTilesProvider (uint maxZoom) : base ()
        {
            //internの生成等省略

            this.maxZoom = maxZoom;
        }

        public override void RequestTile (uint x, uint y, uint zoom, ITileReceiver receiver)
        {
            //現ズームがタイルの最大ズーム以上の場合
            if (zoom > maxZoom) {
                //現ズームと最大ズーム間のスケールファクタ
                var pow = Math.Pow (2, zoom - maxZoom);
                //最大ズームでのx,y算出
                var maxX = (uint)(x / pow);
                var maxY = (uint)(y / pow);

                //オリジナルサイズの情報も渡して独自コールバックオブジェクト生成
                var cropWrapped = new WebTilesReceiver (receiver, x, y, zoom);

                //最大ズームでのタイル要求
                //キャッシュ機構が働くよう、internではなくthisのRequestTileを叩く
                this.RequestTile (maxX, maxY, maxZoom, cropWrapped);
                return;
            }

            //以下、キャッシュ取得を含めたタイル要求ロジック、省略
        }

        protected string CreateTileUrl (uint x, uint y, uint z)
        {
        }
    }

    public class MyTilesReceiver : NSObject, ITileReceiver
    {
        public ITileReceiver origin;
        //デジタルズームを実行するための、オリジナルズームレベル、x、y
        //デジタルズーム不要の場合は全て0
        private uint originX = 0;
        private uint originY = 0;
        private uint originZoom = 0;

        public MyTilesReceiver (ITileReceiver origin) : base ()
        {
            this.origin      = origin;
        }

        //デジタルズームが必要な場合に、オリジナルズームレベル等を定義するためのコンストラクタ
        public MyTilesReceiver (ITileReceiver origin, uint originX, uint originY, uint originZoom) : this(origin)
        {
            this.originX = originX;
            this.originY = originY;
            this.originZoom = originZoom;
        }

        public void ReceiveTile (uint x, uint y, uint zoom, UIImage image)
        {
            //オリジナルズームが設定されている = デジタルズームが必要
            if (this.originZoom != 0) {
                //現ズームと最大ズーム間のスケールファクタ
                var pow = Math.Pow (2, this.originZoom - zoom);
                //切り出す画像のサイズや切り出し原点を算出
                var size = 256.0 / pow;
                var shiftX = (this.originX - (uint)(x * pow)) * size;
                var shiftY = (this.originY - (uint)(y * pow)) * size;
                var rect = new RectangleF ((float)shiftX, (float)shiftY, (float)size, (float)size);

                //切り出し
                using (CGImage cr = image.CGImage.WithImageInRect (rect)) {
                    image = UIImage.FromImage (cr);
                }
		
                //コールバックに切り出し画像を返す	
                origin.ReceiveTile (this.originX, this.originY, this.originZoom, image);
                return;
            }

            //以下、キャッシュ登録を含めたタイル取得ロジック、省略
        }
    }
}
```