How to use Spatialite with Xamarin on Android

3.1k views Asked by At

I want to use Spatialite instead of plain SQLite with Xamarin on Android, to manage and display geographical data. Built-in SQLite does not allow to load extensions. How can I do it?

1

There are 1 answers

0
JaakL On BEST ANSWER

Short answer: you need to use own customized SQLite as Android Native Library, like with other NDK libraries. The tricky part is to get useful not so trivial C# API for the database. Xamarin docs seems to have guides for very simple single method APIs only.

As I am way more familiar to Java than .Net, then I used combination of Android Java library (.jar) and Android native library (.so). Android Java library has already Java API wrapper for the database, it is exactly the same wrapper as can be used in the usual Android Java applications. Of course, technically direct access of the native library from C# would be also possible, so java/jar could be excluded from the story. If you know good tools for that, let me know also.

  1. Create .jar binding project for Xamarin, add it to the same solution as your Android project
  2. Add jsqlite.jar to Jars folder of the bindings project. Get it from here: jsqlite.jar
  3. Add native library binaries (libjsqlite.so and libproj.so) to your application project, create folder libs/armeabi for this. Get these from Nutiteq AdvancedMap3D project
  4. Define the .so files as AndroidNativeLibrary, and set Copy to Output Directory
  5. Fix binding definitions to remove build errors. Add following to Transforms/Metadata.xml of your bindings project:
<remove-node path="/api/package[@name='jsqlite']/class[@name='Backup']/field[@name='handle']" />
<remove-node path="/api/package[@name='jsqlite']/class[@name='Database']/field[@name='handle']"/>
<attr path="/api/package[@name='jsqlite']" name="managedName">jsqlite</attr>

This should generate you working C# API to bundled SQLite, together with Spatialite, Proj.4 and GEOS included. The jsqlite DB API itself is different from other C# SQLite APIs, you need to use callback classes. See following examples To check versions of the modules:

try {
  db.Open ("/sdcard/mapxt/estonia-latest-map.sqlite", Constants.SqliteOpenReadonly);
  // show versions to verify that modules are there
  db.Exec ("SELECT spatialite_version(), proj4_version(), geos_version(), sqlite_version()", new GeneralQryResult ());
} catch (jsqlite.Exception ex) {
  Log.Error( ex.LocalizedMessage );
}

...

// prints query results as text
public class GeneralQryResult : Java.Lang.Object, ICallback
{

    public bool Newrow (string[] rowdata)
    {
        string row = "";
        foreach (var data in rowdata) {
            row += data + " | ";
        }

        Log.Info(row);
        return false;
    }

    public void Types (string[] types)
    {
        // never called really
    }

    public void Columns (string[] cols){
        Log.Debug ("Query result:");
        string row = "";
        foreach (var col in cols) {
            row += col + " | ";
        }
        Log.Info (row);
    }
}

Finally now a query of real spatial data, using Nutiteq 3D Maps SDK for Xamarin to visualize it:

// Spatialite query, show results on map
// 1. create style and layer for data

LineStyle.Builder lineStyleBuilder = new LineStyle.Builder ();
lineStyleBuilder.SetColor (NutiteqComponents.Color.Argb(0xff, 0x5C, 0x40, 0x33)); //brown
lineStyleBuilder.SetWidth (0.05f);
LineStyle lineStyle = lineStyleBuilder.Build ();

GeometryLayer geomLayer = new GeometryLayer (view.Layers.BaseLayer.Projection);
view.Layers.AddLayer (geomLayer);

// 2. do the query, pass results to the layer
Database db = new Database ();

try {
    db.Open ("/sdcard/mapxt/estonia-latest-map.sqlite", Constants.SqliteOpenReadonly);

    // spatial query. Limit to 1000 objects to avoid layer overloading
    String qry = "SELECT id, HEX(AsBinary(Transform(geometry,3857))), sub_type, name FROM ln_railway LIMIT 1000";
    db.Exec (qry, new SpatialQryResult (geomLayer, lineStyle));
} catch (jsqlite.Exception ex) {
    Log.Error( ex.LocalizedMessage );
}

...

// adds query results to given layer, with given style
public class SpatialQryResult : Java.Lang.Object, ICallback
{

    GeometryLayer _geomLayer;
    Style _geomStyle;

    public SpatialQryResult(GeometryLayer geomLayer, Style geomStyle){
        _geomLayer = geomLayer;
        _geomStyle = geomStyle;
    }

    public bool Newrow (string[] rowdata)
    {

        string id = rowdata [0];
        string geomHex = rowdata [1];
        string type = rowdata [2];
        string name = rowdata [3];

        Label label;
        if (name != null && name.Length > 1) {
            label = new DefaultLabel (name, type);
        } else {
            label = null;
        }

        Geometry[] lineGeoms = WkbRead.ReadWkb(new ByteArrayInputStream(Utils
            .HexStringToByteArray(geomHex)), rowdata);

        // following fails if not Line, change for other geometries
        foreach (Line lineGeom in lineGeoms) {
            _geomLayer.Add(new Line(lineGeom.VertexList, label, (LineStyle)_geomStyle, _geomLayer));
        }

        return false;
    }
}