Friday, June 10, 2016

Creating a simple TileServer with .NET Core RC2 (Part 1 - Setting up the project)

Part 1. Setting up the the project
Part 2. Improving drawing logic

As most of you might have heard Microsoft has recently released .Net core RC2. Although it's still subject to lots of changes I think now is a good time to get on the bandwagon, particularly with the various improvements that have been done.

On this series I'm going to do a cross-platform tile-server that generates tiles with hexagons displayable on a map. As I'm just learning .NET core this will be a learning exercise for me and I'll post all of the steps that I've done in order to achieve the end-result.

1. First step, install .NET core and Visual Studio

The instructions on Microsoft's page are actually quite good. I've tried them both on Windows and Mac and worked like a charm. Just don't omit any of the steps (like uninstalling previous versions):

This page also explains how to setup Visual Studio 2015 and Visual Studio Code. I've actually setup both and I'm forcing myself to also use Visual Studio Code as an exercise to properly understand how the plumbing and client tools work.

2. Create the project structure

My project will be structured as:
  • A class library to hold the drawing logic
  • A class library to hold the hexagon logic
  • A unit test project to validate the drawing logic
  • A unit test project to validate the hexagon logic
  • A server that will generate images for specific tile coordinates
You may ask: "Do you need so many projects for such a simple project?". Definitely not, although useful for a learning exercise.

The convention for .net core projects is to create a folder for the main projects (under /src) and a folder for the corresponding tests (under /test). Also, a global.json is defined at the top to specify these two main folders. The project structure will be:

/CoreTiles
|__global.json
|__/src
   |__/CoreTiles.Drawing
      |__<files>
      |__project.json
   |__/CoreTiles.Hexagon
      |__<files>
      |__project.json
   |__/CoresTiles.Server
      |__<files>
      |__project.json
|__/test
   |__/CoreTiles.Drawing.Tests
      |__<files>
      |__project.json
   |__/CoreTiles.Hexagon.Tests
      |__<files>
      |__project.json
I've started by creating the tree structure in the file-system. Then, inside each project folder creating an empty project using the dotnet cli tool:
dotnet new
This creates a console application that ouputs "Hello World", containing both a "project.json" and a "Program.cs" files. A couple of changes need to be done to the default generated projects:
  • The class libraries (CoreTiles.Drawing and CoreTiles.Hexagon) are not executable, so the project.json should be changed. Basically specifying that this project doesn't have an entry execution point and making it more compatible across the board, as "netstandard" can be implemented by multiple .NET platforms. Additional info here
From:
{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.NETCore.App": {
      "type": "platform",
      "version": "1.0.0-rc2-3002702"
    }
  },
  "frameworks": {
    "netcoreapp1.0": {
      "imports": "dnxcore50"
    }
  }
}
To:
{ 
  "version": "1.0.0-*", 
  "dependencies": { 
    "NETStandard.Library": "1.5.0-rc2-24027" 
  }, 
  "frameworks": { 
    "netstandard1.5": { 
      "imports": "dnxcore50" 
    } 
  } 
}
  • The test projects are slightly different, as they're actually executables. Regardless, the Main method is provided by the test runner, which in this case is xunit. So, the entry point should be removed and the xunit dependencies should be added:
From:
{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.NETCore.App": {
      "type": "platform",
      "version": "1.0.0-rc2-3002702"
    }
  },
  "frameworks": {
    "netcoreapp1.0": {
      "imports": "dnxcore50"
    }
  }
}
To:
{
  "version": "1.0.0-*",
  "testRunner": "xunit",

  "dependencies": {
    "Microsoft.NETCore.App": {
      "type":"platform",
      "version": "1.0.0-rc2-3002702"
    },
    "xunit":"2.1.0",
    "dotnet-test-xunit": "1.0.0-rc2-build10015"
  },
  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dnxcore50",
        "portable-net45+win8" 
      ]
    }
  }
}
  • The server project (CoreTiles.Server) is an actual console application, so doesn't need to be changed (for now)
3. Create the Server Web API 

Start by adding the various dependencies to project.json
"dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.0-rc2-3002702",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final"
  }
On RC1 a MVC project did the wiring up of the Startup class automatically. On RC2 this is a regular console app. As such, all the plumbing needs to be done explicitly on Main:
public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseStartup<Startup>()
            .Build();

        host.Run();
    }
}
We need to register the MVC framework on the pipeline
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();
}

// This method gets called by the runtime. 
// Use this method to configure the HTTP request pipeline.
public void Configure(
    IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseMvc();
}
Now creating a controller that will return a tile images, depending on specified parameters (z/x/y).

I've created a simple TileController with this signature:
[Route("[controller]")]
public class TileController : Controller
{
    [HttpGet("{z}/{x}/{y}")]
    public IActionResult Get(int z, int x, int y)
    {
        return Ok();   
    }
}

Now I'm just missing:
  • Returning an actual image
  • Creating a map view that uses this API 
The map image being returned needs to be generated dynamically. Luckily there's a component that
does exactly that, including support for .NET Core: ImageProcessor.
Unfortunately ImageProcessor doesn't yet include drawing capabilities so I'll need to extend it to include some simple drawing actions (like drawing lines, rectangles, polylines, etc).

Thus, for this first post I'll just return a simple red square for each tile. I'll do the proper hexagon drawing on the next post, on which I'll do some extensions to ImageProcessor.
[Route("[controller]")]
public class TileController : Controller
{
    private const int TileSize = 256;

    [HttpGet("{z}/{x}/{y}")]
    public async Task<IActionResult> Get(int z, int x, int y)
    {
        using (Image image = new Image(TileSize, TileSize))
        using (var outputStream = new MemoryStream())
        {
            //Drawing code goes here

            image.SaveAsPng(outputStream);

            var bytes = outputStream.ToArray();

            Response.ContentType = "image/png";
            await Response.Body.WriteAsync(bytes, 0, bytes.Length);
            return Ok();
        }
    }
}
As the Image class of ImageProcessor only supports putting and getting individual pixels creating a square method is quite simple:
public static void DrawRectangle(this Image image, int x, int y, 
                                 int width, int height, Color color)
{
    //Draw horizontal lines
    for(var i = x; i < x + width; i++)
    {
        image.SetPixel(i, y, color);
        image.SetPixel(i, y + height - 1, color);
    }

    //Draw vertical lines
    for(var j=y+1; j < y + height; j++)
    {
        image.SetPixel(x, j, color);
        image.SetPixel(x + width - 1, j, color);
    }
}
Updated the Tile drawing logic to:
[Route("[controller]")]
public class TileController : Controller
{
    private const int TileSize = 256;

    [HttpGet("{z}/{x}/{y}")]
    public async Task<IActionResult> Get(int z, int x, int y)
    {
        using (Image image = new Image(TileSize, TileSize))
        using (var outputStream = new MemoryStream())
        {
            image.DrawRectangle(0,0,256,256,Color.Red);

            image.SaveAsPng(outputStream);

            var bytes = outputStream.ToArray();

            Response.ContentType = "image/png";
            await Response.Body.WriteAsync(bytes, 0, bytes.Length);
            return Ok();
        }
    }
}
Running the CoreTile.Server app (dotnet run) and opening a browser at: http://localhost:5000/tile/0/0/0 shows this:

Now I'll create a web-page to show a simple map using this new tile-layer.

I'm going to use Openlayers 3 for this, particularly as I've been wanting to experiment with it for some time.

First I need to change the Startup class to define a proper default routing logic as well as being able to serve static files from the server.
public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();
            
    app.UseMvc(routes =>
    {
        routes.MapRoute(
                 name: "default",
                 template: "{controller=Home}/{action=Index}");
        });
    }
}
Then creating a simple controller to serve the main View
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }        
}
And finally the actual view
<!DOCTYPE html>
<html>
<head>
    <title>Canvas Tiles</title>
    <link rel="stylesheet" href="http://openlayers.org/en/v3.16.0/css/ol.css" 
                              type="text/css">
    <script src="http://openlayers.org/en/v3.16.0/build/ol.js">
    </script>
</head>
<body>
    <div id="map" class="map"></div>
    <script>
      var osmSource = new ol.source.OSM();
      var map = new ol.Map({
        layers: [
          new ol.layer.Tile({
            source: osmSource
          }),
          new ol.layer.Tile({
              source: new ol.source.XYZ({
                  url: "/tile/{z}/{x}/{y}"
              })
          }),
        ],
        target: 'map',
        controls: ol.control.defaults({
          attributionOptions: ({
            collapsible: false
          })
        }),
        view: new ol.View({
          center: ol.proj.transform(
              [-0.1275, 51.507222], 'EPSG:4326', 'EPSG:3857'),
          zoom: 10
        })
      });
    </script>
</body>
</html>

Running the application and opening http://localhost:5000 shows the basemap with the dynamically generated tiles


I've tried this both on a Mac and a Windows. I'm assuming it should also work without any problem in Linux.

The complete code can be viewed at: https://github.com/pmcxs/CoreTiles

Reference:

2 comments:

  1. The issue that you need a fast drawing library, so you can render the tiles very very fast!!!! But as an early project is fine.
    Regards,
    Christos

    ReplyDelete
    Replies
    1. From my experiments ImageProcessor seems reasonably fast. I'll try to include some benchmarks on my next blog post

      Delete