Alternate solution to implement use case "Map and Market".

Within this forum we want to offer discussions all around our .NET based Map control including all .Net-language specific questions concerning calls of the xServer-API. Attention: xServer-relevant concepts can be found in further forums such as xRoute or xTour.
Post Reply
User avatar
webdirekt
Posts: 28
Joined: Wed Jul 15, 2015 10:18 am

Alternate solution to implement use case "Map and Market".

Post by webdirekt »

Use case : Displaying the active region for medical experts using the use case of "Map & Market" of Demo Center.

1. We have table in access database to store the zip code details with following fields.
  • ZipCode (type : Double)
  • XMIN (type : Double)
  • YMIN (type : Double)
  • XMAX (type : Double)
  • YMAX (type : Double)
  • WKB_GEOMETRY (type : Long binary data)
  • XCoordinate (type : Double)
  • YCoordinate (type : Double)
2. We have more than 4000 zip codes whose binary data (WKB_GEOMETRY) we are fetching so that we can display the region in map.

3. We are adding the BaseLayer of Active region in the map.

Detailed Implementation :
--------------------------------------------------------------------------------
- We are having structure (GeoItem) to store the data which are fetched from the access database, with following properties.
  • byte[] Wkb
  • object Id
  • double XMin
  • double YMin
  • double XMax
  • double YMax
  • double XCoord
  • double YCoord
  • Dictionary<string, object> Atributes;
IGeoProvider(interface)
------------------------------------------------------

Code: Select all

public interface IGeoProvider
{
   IEnumerable<GeoItem> QueryBBox(double xmin, double ymin, double xmax, double ymax, string[] attributes);
}
MMProvider.cs
------------------------------------------------------

Code: Select all

public IEnumerable<GeoItem> QueryBBox(double xmin, double ymin, double xmax, double ymax, string[] attributes)
{
	// Query to get the geo items from Ole database.
	String queryForActiveRegion = "SELECT *
					"FROM Germany_5_digit_postcode as tGeo " +
					"WHERE ZipCode in ( <Here we have list of more than 4000 zip codes> );


	// initializing the command by query and the connection
	using (var command = new OleDbCommand(queryForActiveRegion, Connection))
	{
		// executing the query so that we can get the geoItem result from the access database
		using (var dataReader = command.ExecuteReader())
		{
			// reading the data from the reader
			while (dataReader.Read())
			{
				// storing the data in GeoItem structure
				var geoItem = new GeoItem
				{
					Wkb = (byte[])dataReader[0],
					XMin = Convert.ToDouble(dataReader[1]),
					YMin = Convert.ToDouble(dataReader[2]),
					XMax = Convert.ToDouble(dataReader[3]),
					YMax = Convert.ToDouble(dataReader[4]),
					XCoord = Convert.ToDouble(dataReader[5]),
					YCoord = Convert.ToDouble(dataReader[6])
				};
				geoItem.Atributes = new Dictionary<string, object>();
				geoItem.Atributes["COLOR"] = dataReader[8];
					
				}
				else // this means that no attribute needed for the geoitem
				{
					// do nothing
				}

				// returing geoItem
				yield return geoItem;
			}
		}
	}
}
TileRenderer.cs (this class is inherited from ITiledProvider of Ptv.XServer.Controls.Map.Layers.Tiled which is present in Ptv.XServer.Controls.Map.dll assembly)
-------------------------------------------------------------------
We have two properties and one function as follows
  • public IGeoProvider Provider { get; set; }
  • public GdiTheme Theme { get; set; }
  • public string CacheId{ get { return "TileRenderer"; }}
  • public int MinZoom{get { return 0; }}
  • public int MaxZoom{get { return 19; }}

Code: Select all

public Stream GetImageStream(int x, int y, int zoom)
{
	using (var bmp = new Bitmap(256, 256))
	{
		var rect = GeoTransform.TileToPtvMercatorAtZoom(x, y, zoom);
		Func<double, double, Point> mercatorToImage =
			(
				mercatorX, mercatorY) => new Point(
					x = (int)((mercatorX - rect.Left) / (rect.Right - rect.Left) * 256),
					y = 256 + (int)-((mercatorY - rect.Top) / (rect.Bottom - rect.Top) * 256)
			);
		
		using (var graphics = Graphics.FromImage(bmp))
		{
			var result = Provider.QueryBBox(rect.Left, rect.Top, rect.Right, rect.Bottom, Theme.RequiredFields);
			foreach (var item in result)
			{
				var path = WkbToGdi.Parse(item.Wkb, mercatorToImage);
				var style = Theme.Mapping(item);
				if (style.Fill != null)
					graphics.FillPath(style.Fill, path);
				if (style.Outline != null)
					graphics.DrawPath(style.Outline, path);
			}
		}
	   
		var stream = new MemoryStream();	   
		bmp.Save(stream, ImageFormat.Png);		
		stream.Seek(0, SeekOrigin.Begin);
		return stream;
	}
}
- We have code for SelectionCanvas.cs same as given in Demo Center demo.

- We are adding BaseLayer in map as follows.

Code: Select all

MMProvider provider = new MMProvider
{
	Connection = oleDbConnection,
	IdColumn = "IntId",
	GeometryColumn = "WKB_GEOMETRY",
	XMinColumn = "XMIN",
	YMinColumn = "YMIN",
	XMaxColumn = "XMAX",
	YMaxColumn = "YMAX",
	XCoordColumn = "XCoord",
	YCoordColumn = "YCoord",
	ActiveRegionZipCodes = activeRegionZipCodes
};

// getting the color for AK-Region so that we can apply that color for the same
System.Drawing.Color[] palette =
{
	System.Drawing.ColorTranslator.FromHtml("#009EE3"),
	System.Drawing.Color.LightGray
};

// getting the GdiTheme which has the color as well as the outlining specification; so that we can apply those specifications to AK-Region
var theme = new GdiTheme
{
	RequiredFields = new[] { "COLOR" }, // need to fetch the field kk_kat 
	Mapping = gdiStyle => new GdiStyle
{
	Fill = new System.Drawing.SolidBrush(palette[System.Convert.ToInt16(gdiStyle.Atributes["COLOR"]) - 1]),
	Outline = System.Drawing.Pens.Black,
}
};

// creating the tileRenderer object using the MMProvider and GdiTheme; so that we can use the tile rendered to create tile canvas
var tileRenderer = new TileRenderer
{
	Provider = provider,
	Theme = theme,
};

// the collection of selected elements so that it will help us to color the AK-Region using those points
var selectedRegions = new ObservableCollection<System.Windows.Media.Geometry>();

// insert layer with two canvases
var activeRegionLayer = new BaseLayer("AK-Region")
{
	CanvasCategories = new[] { CanvasCategory.Content, CanvasCategory.SelectedObjects },
	CanvasFactories = new BaseLayer.CanvasFactoryDelegate[]
	{
	m => new TiledCanvas(m, tileRenderer) { IsTransparentLayer = true },
	m => new SelectionCanvas(m, provider, selectedRegions)
	},
	Opacity = .5,
};

// insert the active region layer above the Background layer of map
ptvMap.Layers.Insert(ptvMap.Layers.IndexOf(ptvMap.Layers["Background"]), activeRegionLayer);
- Issue is that it takes so long time to display the region for large number of zip code.

- Is there any alternate way to implement usecase like "Map and Market" of Demo center ?
Attachments
Active Region image
Active Region image
User avatar
Oliver Heilig
Posts: 154
Joined: Tue May 13, 2014 12:10 pm
Location: Karlsruhe, Germany
Contact:

Re: Alternate solution to implement use case "Map and Market

Post by Oliver Heilig »

The implementation should be the gold standard for rendering many polygons out-of a database.

I have the same code inside ASP apps, and it renderes several 10k polygons well, you can check the performance here:
https://github.com/ptv-logistics/SpatialTutorial/wiki

I've also tested the DemoCenter Code with Europe 2-digit postcodes (> 90k), and in runs quite well on my notebook.
MMTiles.png
What you can check:
  • The code in the web-sample reduces the vertices according to the resoltuion pixel-size. This should improve the performance. You can use this code in the attachment as shown below.
  • Check the detail-level of the regions. I don't know your source data, but the shapes sholdn't have too many vertices. There are tools to reduce the vertices, like http://mapshaper.org/
  • Disable the display for the layer for certain zoom levels (with the MinZoom property).
  • Check the indexes: XMIN, YMIN, XMAX, YMAX should be an indexed
  • Use another database, preferably a spatial db. The web-sample uses SpatialLite, which is an alternative to MS Access for Desktop. Another benefit is that it also run on 64-Bit.
  • The code runs multi-threaded, which means for many cores on the client (up to 8) the performance should scale well.
  • Implement some caching, which means store the (serialized) tile images as files and reload them from disk.
  • Is the .mdb on the local machine?
  • If your client is still slow, the only sustainable solution would be to offload the rendering code to a middleware server (like in the web sample), and use the RemoveTiledProvider instead of in-process rendering.
Oli

Rendering code:

Code: Select all

        public Stream GetImageStream(int x, int y, int zoom)
        {
            // Create a bitmap of size 256x256
            using (var bmp = new Bitmap(256, 256))
            {
                // calc rect from tile key
                var rect = GeoTransform.TileToPtvMercatorAtZoom(x, y, zoom);

                // PTV_Mercator to Image function
                Func<System.Windows.Point, System.Drawing.Point> mercatorToImage =
                    p => new Point(
                        x = (int)((p.X - rect.Left) / (rect.Right - rect.Left) * 256),
                        y = 256 + (int)-((p.Y - rect.Top) / (rect.Bottom - rect.Top) * 256));

                // get graphics from bitmap
                using (var graphics = Graphics.FromImage(bmp))
                {
                    // query the provider for the items in the envelope
                    var result = Provider.QueryBBox(rect.Left, rect.Top, rect.Right, rect.Bottom, Theme.RequiredFields);

                    foreach (var item in result)
                    {
                        // create GDI path from wkb
                        var path = WkbToGdi.Parse(item.Wkb, mercatorToImage);
                        if (path == null)
                            continue;

                        // evalutate style
                        var style = Theme.Mapping(item);

                        // fill polyon
                        if(style.Fill != null)
                            graphics.FillPath(style.Fill, path);

                        // draw outline
                        if(style.Outline != null)
                            graphics.DrawPath(style.Outline, path);
                    }
                }

                // crate a memory stream
                var stream = new MemoryStream();

                // save image to stream
                bmp.Save(stream, ImageFormat.Png);

                // rewind stream
                stream.Seek(0, SeekOrigin.Begin);

                return stream;
            }
        }
Parser which reduces vertices:

Code: Select all

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;

namespace Ptv.XServer.Controls.Map.Tools
{
    /// <summary> Converts Well-known Binary representations to a GraphicsPath instance. </summary>
    public class WkbToGdi
    {
        #region public methods
        public static GraphicsPath Parse(byte[] bytes, Func<System.Windows.Point, System.Drawing.Point> geoToPixel)
        {
            // Create a memory stream using the suppiled byte array.
            using (MemoryStream ms = new MemoryStream(bytes))
            {
                // Create a new binary reader using the newly created memorystream.
                using (BinaryReader reader = new BinaryReader(ms))
                {
                    // Call the main create function.
                    return Parse(reader, geoToPixel);
                }
            }
        }

        public static GraphicsPath Parse(BinaryReader reader, Func<System.Windows.Point, System.Drawing.Point> geoToPixel)
        {
            // Get the first byte in the array.  This specifies if the WKB is in
            // XDR (big-endian) format of NDR (little-endian) format.
            byte byteOrder = reader.ReadByte();

            if (!Enum.IsDefined(typeof(WkbByteOrder), byteOrder))
            {
                throw new ArgumentException("Byte order not recognized");
            }

            // Get the type of this geometry.
            uint type = (uint)ReadUInt32(reader, (WkbByteOrder)byteOrder);

            if (!Enum.IsDefined(typeof(WKBGeometryType), type))
                throw new ArgumentException("Geometry type not recognized");

            switch ((WKBGeometryType)type)
            {
                case WKBGeometryType.Polygon:
                    return CreateWKBPolygon(reader, (WkbByteOrder)byteOrder, geoToPixel);

                case WKBGeometryType.MultiPolygon:
                    return CreateWKBMultiPolygon(reader, (WkbByteOrder)byteOrder, geoToPixel);

                default:
                    throw new NotSupportedException("Geometry type '" + type.ToString() + "' not supported");
            }
        }
        #endregion

        #region private methods
        private static Point CreateWKBPoint(BinaryReader reader, WkbByteOrder byteOrder)
        {
            // Create and return the point.
            return new Point((int)ReadDouble(reader, byteOrder), (int)ReadDouble(reader, byteOrder));
        }

        private static List<Point> ReadCoordinates(BinaryReader reader, WkbByteOrder byteOrder, Func<System.Windows.Point, System.Drawing.Point> geoToPixel)
        {
            // Get the number of points in this linestring.
            int numPoints = (int)ReadUInt32(reader, byteOrder);

            // Create a new array of coordinates.
            var coords = new List<Point>();

            Point p0 = new Point(0, 0);
            // Loop on the number of points in the ring.
            for (int i = 0; i < numPoints; i++)
            {
                double x = ReadDouble(reader, byteOrder);
                double y = ReadDouble(reader, byteOrder);

                var dx = geoToPixel(new System.Windows.Point(x, y));

                if (i == 0)
                {
                    coords.Add(new Point(dx.X, dx.Y));

                    p0 = dx;
                }
                else if (i == numPoints - 1)
                {
                    if (Math.Abs(coords[0].X - dx.X) >= 1 || Math.Abs(coords[0].Y - dx.Y) >= 1)
                        coords.Add(new Point(dx.X, dx.Y));
                }
                else
                {
                    if (Math.Abs(p0.X - dx.X) >= 1 || Math.Abs(p0.Y - dx.Y) >= 1)
                    {
                        // Add the coordinate.
                        coords.Add(new Point(dx.X, dx.Y));

                        p0 = dx;
                    }
                }
            }

            return coords;
        }

        private static List<Point> CreateWKBLinearRing(BinaryReader reader, WkbByteOrder byteOrder, Func<System.Windows.Point, System.Drawing.Point> geoToPixel)
        {
            return ReadCoordinates(reader, byteOrder, geoToPixel);
        }

        private static GraphicsPath CreateWKBPolygon(BinaryReader reader, WkbByteOrder byteOrder, Func<System.Windows.Point, System.Drawing.Point> geoToPixel)
        {
            // Get the Number of rings in this Polygon.
            int numRings = (int)ReadUInt32(reader, byteOrder);

            Debug.Assert(numRings >= 1, "Number of rings in polygon must be 1 or more.");

            GraphicsPath gp = new GraphicsPath();

            var arr = CreateWKBLinearRing(reader, byteOrder, geoToPixel);

            if (arr.Count > 2)
                gp.AddPolygon(arr.ToArray());

            // Create a new array of linearrings for the interior rings.
            for (int i = 0; i < (numRings - 1); i++)
            {
                var rarr = CreateWKBLinearRing(reader, byteOrder, geoToPixel);

                if (arr.Count > 2 && rarr.Count > 2)
                {
                    gp.AddPolygon(rarr.ToArray());
                }
            }

            // Create and return the Poylgon.
            if (arr.Count > 2)
                return gp;
            else
            {
                int dummy = 42;
                return null;
            }
        }

        private static GraphicsPath CreateWKBMultiPolygon(BinaryReader reader, WkbByteOrder byteOrder, Func<System.Windows.Point, System.Drawing.Point> geoToPixel)
        {
            GraphicsPath gp = new GraphicsPath();

            // Get the number of Polygons.
            int numPolygons = (int)ReadUInt32(reader, byteOrder);

            // Loop on the number of polygons.
            for (int i = 0; i < numPolygons; i++)
            {
                // read polygon header
                reader.ReadByte();
                ReadUInt32(reader, byteOrder);
                var p = CreateWKBPolygon(reader, byteOrder, geoToPixel);

                // TODO: Validate type

                // Create the next polygon and add it to the array.
                if (p != null)
                    gp.AddPath(p, false);
            }

            //Create and return the MultiPolygon.
            if (gp.PointCount > 0)
                return gp;
            else
                return null;
        }

        private static uint ReadUInt32(BinaryReader reader, WkbByteOrder byteOrder)
        {
            if (byteOrder == WkbByteOrder.Xdr)
            {
                byte[] bytes = BitConverter.GetBytes(reader.ReadUInt32());
                Array.Reverse(bytes);
                return BitConverter.ToUInt32(bytes, 0);
            }
            else
                return reader.ReadUInt32();
        }

        private static double ReadDouble(BinaryReader reader, WkbByteOrder byteOrder)
        {
            if (byteOrder == WkbByteOrder.Xdr)
            {
                byte[] bytes = BitConverter.GetBytes(reader.ReadDouble());
                Array.Reverse(bytes);
                return BitConverter.ToDouble(bytes, 0);
            }
            else
                return reader.ReadDouble();
        }
        #endregion
    }
}
Oliver Heilig
Chief Developer Logistic Services
PTV GROUP - Germany

https://github.com/oliverheilig/
Post Reply