Net Topology Suite Xamarin Android/iOSで動くようにしてみた


[あめい師匠の記事](http://qiita.com/amay077/items/7a99df1c0da881cc47f6)を読んで「やった！XamarinでもGEOS(NTSとほぼ機能的等価のネイティブライブラリ)相当機能が使えるようになった！」と喜んでいた私ですが、Xamarin StudioのXamarin.Android/iOSでNuGetインストールしようとしても、GeoAPIがプラットフォームに対応してないと出て動かない。

「師匠！NuGet動きませんよ？」
「んー、あれはコンソールアプリだったから…」

がーん！まあXamarin = モバイルと思い込んでいた私が悪いのですが、もう溢れる「GEOS使いたい」の思いは止められず、ソースからAndroid/iOS向けにコンパイルしてみることにしました。

## とりあえずソースコード取得
[NTSのページ](https://code.google.com/p/nettopologysuite/)から、ソースコードを取得。
レポジトリがSVNだったので、こっちでgit管理するならSVNからとる必要もないかなと思い、[zip](https://code.google.com/p/nettopologysuite/downloads/detail?name=NTS-v1-13-0.zip)で取得しました。

解凍して開けてみて、MonoDroidというソリューションがあるのを発見。
いきなりこの記事のタイトルに偽りありになるのですが、Android版のプロジェクトは既に存在していました。
が、後述のとおり、どこまで動いているのか定かではないポーティングなのですが、とりあえずこれをベースに同程度までiOSを動かすのを目標にしました。
一応、あめい師匠が前記事で書いたサンプルは問題なく動くようです。

## ポーティング用ソリューションを作ってみる
NetTopologySuite.MonoDroid.sln を開いてみると、

* GeoAPI.MonoDroid
* NetTopologySuite.MonoDroid
* NetTopologySuite.IO.Spatialite.MonoDroid
* PowerCollections.MonoDroid

の4つのプロジェクトが含まれているようでしたが、NetTopologySuite.IO.Spatialite.MonoDroidは存在しないとエラーを吐いていて、かつNetTopologySuite.MonoDroidからも参照されていないようでしたので、とりあえず無視しました。
iOS向けにも、GeoAPI、NTS、PowerCollectionの3つのプロジェクトを作ってやればよさそうです。

MonoDroid向けを参考にしつつ、MonoTouch向けを作るため、別途NetTopologySuite.Xamarin.slnというソリューションを切ってやって、その中にMonoDroidとMonoTouch向けの3APIのプロジェクトを読み込んでやりました。
もちろん、MonoTouch向けはiOS=>Library Projectで新規作成です。

* GeoAPI.MonoTouch
* NetTopologySuite.MonoTouch
* PowerCollections.MonoTouch

の3つを作成し、MonoDroid側がリンクで読み込んでいるソースコード等のファイル群を、こちらでも読み込んでやります。
読み込む時には、Add Files From Folderで指定してやると、ツリー構造をそのまま読み込めますし、一応どれを読み込んでどれを読み込まないかもチェックできるので、便利です。

また、MonoDroidプロジェクト自体が持っているFrameworkReplacementsフォルダやExtensionフォルダ下のソースコードも、MonoDroidプロジェクト直下のものを正本としてリンクで読み込んでやります。
リストとしては、

* GeoAPI/FrameworkReplacements/BitConverterEx.cs
* NetTopologySuite/Extension/ICoordinateEx.cs
* NetTopologySuite/Extension/IEnvelopeEx.cs
* NetTopologySuite/FrameworkReplacements/Trace.cs

があります。

## 条件付ディレクティブの編集

全リンクファイルを読み込み終わったら、条件付ディレクティブを編集してやります。

```cs:before
#if SILVERLIGHT || MONODROID
```

等、プラットフォームを指定して分岐しているところに、iOS向けの条件（__IOS__）も入れてやります。
また、Android向けも、MONDROIDディレクティブはレガシーで今は__ANDROID__を使うべきらしいので、ついでに修正してやりましょう。
上の例だと

```cs:after
#if SILVERLIGHT || __ANDROID__ || __IOS__
```

修正対象はソリューション全体に対しMONODROIDで検索してやれば出てきますが、一部、

```cs:NetTopologySuite/Index/QuadTree/DoubleBits.cs
#if !WINDOWS_PHONE && !MONODROID
using BitConverter = System.BitConverter;
#else

using BitConverter = GeoAPI.BitConverterEx;

#endif
```

という表現と、

```cs:その他のファイル
#if !WINDOWS_PHONE
using BitConverter = System.BitConverter;
#else

using BitConverter = GeoAPI.BitConverterEx;

#endif
```

が混在しているところがありますので、これはまとめて

```cs:修正後
#if !WINDOWS_PHONE && !__ANDROID__ && !__IOS__
using BitConverter = System.BitConverter;
#else

using BitConverter = GeoAPI.BitConverterEx;

#endif
```

に直してやった方がいいのかなと。

これを考えに入れると、修正対象は

* NetTopologySuite/Geometries/LineSegment.cs
* NetTopologySuite/Index/QuadTree/DoubleBits.cs
* NetTopologySuite/Precision/CommonBits.cs
* NetTopologySuite/FrameworkReplacements/Trace.cs
* GeoAPI/Geometries/Coordinate.cs
* GeoAPI/Geometries/Envelope.cs

でよさそうです。

## AOTコンパイラに対応してないところは回避
GeoAPIをコンパイルしてやると、

```cs:GeoAPI/GeometryServiceProvider.cs
#if !SILVERLIGHT
            var a = AppDomain.CurrentDomain.GetAssemblies();
            foreach (var assembly in a)
            {
                // Take a look at issue 114: http://code.google.com/p/nettopologysuite/issues/detail?id=114
                if (assembly is System.Reflection.Emit.AssemblyBuilder) continue;
                if (assembly.GetType().FullName == "System.Reflection.Emit.InternalAssemblyBuilder") continue;
                if (assembly.GlobalAssemblyCache && assembly.CodeBase == Assembly.GetExecutingAssembly().CodeBase) continue;

                foreach (var t in GetLoadableTypes(assembly))
                {
                    if (t.IsInterface) continue;
                    if (t.IsAbstract) continue;
                    if (t.IsNotPublic) continue;
                    if (!typeof(IGeometryServices).IsAssignableFrom(t)) continue;

                    var constuctors = t.GetConstructors();
                    foreach (var constructorInfo in constuctors)
                    {
                        if (constructorInfo.IsPublic && constructorInfo.GetParameters().Length == 0)
                            return (IGeometryServices)Activator.CreateInstance(t);
                    }
                }
            }
#endif
```

この部分で、

```
The type or namespace name `AssemblyBuilder' does not exist in the namespace `System.Reflection.Emit'. Are you missing an assembly reference? (CS0234)
```

と怒られます。
調べてみると、MonoTouchはAOTコンパイラ使わないといけない関係で、System.Reflection.Emitに対応していないそうです。
正直、このエラーの部分の処理が何をしているのかは判ってないのですが、利用できない型が入ってくる事はないだろう、もっと前でエラーになっているだろうと思い、

```cs:GeoAPI/GeometryServiceProvider.cs(修正後)
#if !__IOS__
                if (assembly is System.Reflection.Emit.AssemblyBuilder) continue;
#endif
```

で回避しました。
もしかしたら、!SILVERLIGHTと同じところで回避するのが正しいのかもしれませんが、とりあえずコンパイルできないところだけ回避してみました。

## コンパイルできないソースは全部削除！ワイルドだろぉ〜？

ここまでやってみましたが、MonoDroidの方はコンパイル通るのに、MonoTouchはあちこち引っかかって通りません。
そんなクラスは定義されてないとか、キャストできないとか…。

なんかよく判らんなー、と思いつつよくよく確かめてみると、MonoDroid側ではここまでやったところでコンパイル通らないソースは、全部削除されていた…。

MonoDroid側で削除されていたファイルは、以下の通りです。

* NetTopologySuite/Utilities/GeoToolsStreamTokenizer.cs  
発生していたエラー：  
The type or namespace name `TokenType' could not be found. Are you missing an assembly reference? (CS0246)
* NetTopologySuite/Algorithm/IndexedPointInAreaLocator.cs  
発生していたエラー：  
Argument `#1' cannot convert `GeoAPI.Geometries.ICoordinate' expression to type `GeoAPI.Geometries.Coordinate' (CS1503) 
* NetTopologySuite/Algorithm/SimplePointInAreaLocator.cs  
発生していたエラー：  
Argument `#1' cannot convert `GeoAPI.Geometries.ICoordinate' expression to type `GeoAPI.Geometries.Envelope' (CS1503)
* NetTopologySuite/Geometries/Prepared/LineIntersectionAdder.cs  
発生していたエラー：  
Argument `#1' cannot convert `GeoAPI.Geometries.ICoordinate' expression to type `GeoAPI.Geometries.Coordinate' (CS1503)
* NetTopologySuite/Geometries/Prepared/LineTopology.cs  
発生していたエラー：  
Argument `#1' cannot convert `GeoAPI.Geometries.ICoordinate[]' expression to type `GeoAPI.Geometries.Coordinate[]' (CS1503)
* NetTopologySuite/Geometries/Prepared/PreparedPolygonLineIntersection.cs  
発生していたエラー：  
The type or namespace name `LineTopology' could not be found. Are you missing an assembly reference? (CS0246)

とりあえず最初の、GeoTools云々は、GeoToolsというライブラリとの連携のためのレガシーらしいのでいいとして。
それ以外のは、クラス名が「Intersection」だの「Topology」だの重要そうな名前がついていて、本当にこれ抜いてしまっていいの？的な、脳内の警戒信号マックスな感じだったんですが、お手本にすると決めたプロジェクトが抜いているので、逆らうと道しるべがなくなってしまうため抜く事に。

よく判らないままに抜いているので、もしかすると動かない機能で例外、というケースにいきなり出くわすかもしれません。
そういう場合は、抜いたという事実を上記に残しておきますので、それを手がかりに対処してみてください。

## 組み込んで、あめい師匠のサンプル実行…動いた！
これでコンパイルできるようになりました。
MonoTouch、MonoDroid双方で、アプリケーションプロジェクトを作って、

```log:Android実行結果
Intersection - POLYGON ((35 138, 35.666666666666664 138, 36 137, 35 137, 35 138))
Union - POLYGON ((34 136, 34 138, 35 138, 35 140, 35.666666666666664 138, 37 138, 37 136, 34 136))
SymmetricDifference - MULTIPOLYGON (((34 136, 34 138, 35 138, 35 137, 36 137, 35.666666666666664 138, 37 138, 37 136, 34 136)), ((35.666666666666664 138, 35 138, 35 140, 35.666666666666664 138)))
Difference - POLYGON ((34 136, 34 138, 35 138, 35 137, 36 137, 35.666666666666664 138, 37 138, 37 136, 34 136))
Buffer - POLYGON ((36.474341649025256 137.15811388300841, 36.495617628087523 137.06605427108747, 36.499194825230774 136.97163582430565, 36.484945496749909 136.87823027805737, 36.453378494112243 136.78917319649409, 36.405621092587779 136.70764485766813, 36.343378735620611 136.63655668402956, 36.268874132492371 136.57844727390696, 36.1847678841341 136.53539174674506, 36.0940634715831 136.5089276394321, 36 136.5, 35 136.5, 34.902454838991936 136.50960735979839, 34.808658283817458 136.53806023374435, 34.722214883490196 136.58426519384872, 34.646446609406723 136.64644660940672, 34.584265193848729 136.72221488349021, 34.538060233744353 136.80865828381746, 34.509607359798387 136.90245483899193, 34.5 137, 34.5 140, 34.510107989507731 140.10002908604923, 34.54002327222338 140.19601380028945, 34.588536314691694 140.28407329278286, 34.653685639615645 140.3606471458248, 34.732837132224162 140.42263932860277, 34.822790542690022 140.46754337578443, 34.919908878496521 140.49354372881874, 35.020265455166296 140.499589142523
```

```log:iOS実行結果
2013-12-21 14:28:09.312 NTSTestTouch[392:60b] Intersection - POLYGON ((35 138, 35.666666666666664 138, 36 137, 35 137, 35 138))
2013-12-21 14:28:09.331 NTSTestTouch[392:60b] Union - POLYGON ((34 136, 34 138, 35 138, 35 140, 35.666666666666664 138, 37 138, 37 136, 34 136))
2013-12-21 14:28:09.347 NTSTestTouch[392:60b] SymmetricDifference - MULTIPOLYGON (((34 136, 34 138, 35 138, 35 137, 36 137, 35.666666666666664 138, 37 138, 37 136, 34 136)), ((35.666666666666664 138, 35 138, 35 140, 35.666666666666664 138)))
2013-12-21 14:28:09.361 NTSTestTouch[392:60b] Difference - POLYGON ((34 136, 34 138, 35 138, 35 137, 36 137, 35.666666666666664 138, 37 138, 37 136, 34 136))
2013-12-21 14:28:09.399 NTSTestTouch[392:60b] Buffer - POLYGON ((36.474341649025256 137.15811388300841, 36.495617628087523 137.06605427108747, 36.499194825230774 136.97163582430565, 36.484945496749909 136.87823027805737, 36.453378494112243 136.78917319649409, 36.405621092587779 136.70764485766813, 36.343378735620611 136.63655668402956, 36.268874132492371 136.57844727390696, 36.1847678841341 136.53539174674506, 36.0940634715831 136.5089276394321, 36 136.5, 35 136.5, 34.902454838991936 136.50960735979839, 34.808658283817458 136.53806023374435, 34.722214883490196 136.58426519384872, 34.646446609406723 136.64644660940672, 34.584265193848729 136.72221488349021, 34.538060233744353 136.80865828381746, 34.509607359798387 136.90245483899193, 34.5 137, 34.5 140, 34.510107989507731 140.10002908604923, 34.54002327222338 140.19601380028945, 34.588536314691694 140.28407329278286, 34.653685639615645 140.3606471458248, 34.732837132224162 140.42263932860277, 34.822790542690022 140.46754337578443, 34.919908878496521 140.49354372881874, 35.020265455166296 140.49958914252304, 35.119802659803312 140.48543518898413, 35.214496008327167 140.45165414025746, 35.300516863244532 140.39961183028743, 35.374387232959613 140.33141243156655, 35.43312039378435 140.24981337932164, 35.474341649025256 140.15811388300841, 36.474341649025256 137.15811388300841))
```

とりあえず動いてるみたいです！
Androidで後ろの方が切れているのは、Console.Logの仕様なんでしょうか…でも値等は合ってるので動いていると思います。

どこで落ちるか判らない仕様ではありますが、とりあえず上記テスト位は動くので、At your own riskでお使いください。

今回のコードは、
https://github.com/tilemapjp/NetTopologySuite.XamarinMobile
に公開しております。