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.

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

Postby webdirekt » Wed Aug 30, 2017 2:48 pm

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
ActiveRegionImage.png
Active Region image
User avatar
webdirekt
 
Posts: 28
Joined: Wed Jul 15, 2015 10:18 am

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

Postby Oliver Heilig » Mon Sep 04, 2017 12:47 pm

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/
User avatar
Oliver Heilig
 
Posts: 127
Joined: Tue May 13, 2014 12:10 pm
Location: Karlsruhe, Germany


Return to PTV xServer.NET (Admin=OH)

cron