A Look Into The Future–Using the Ignite UI Geographic Proportional Symbol Series Map

Jordan Tsankov / Sunday, December 16, 2012

Infragistics Ignite UI Geographic Proportional Symbol Series Map jQueryThe Ignite UI map is already a control that boasts a wide variety of features , proving useful in a number of different scenarios. I have been posting about the map’s current capabilities; this post and another one which will follow shortly will shed some light on some upcoming features.

Today we shall be looking into the Proportional Symbol Series – a nice touch on the standard symbol series. With this new type of series , you will be able to visually display some of the series’ item statistics by utilizing the size of the symbol as a visual cue.

 

 

 

Now without further ado – let’s get on with it !

 

 

 

 

Setup

As with all other jQuery-based controls , you will have to load up a set of required JavaScript scripts in order for the Ignite UI map to work and be rendered.

Here’s the set of includes you need on your webpage , as usual I’m using the CDN-hosted versions; you’re free to use locally-stored files if you wish.

   1: <script src="http://www.modernizr.com/downloads/modernizr-latest.js" type="text/javascript"></script>
   1:  
   2: <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.js" type="text/javascript">
   1: </script>
   2: <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.17/jquery-ui.js" type="text/javascript">
   1: </script>
   2: <script src="js/infragistics.loader.js">
   1: </script>
   2:  
   3: <script src="MapHelper.js">
</script>

 

The last file being loaded is a custom script with some useful functions for the Ignite UI map control – they can be used alongside the control’s default methods. Feel free to check the file out – it’s provided in the archived sample on the bottom of this post. You can also see the code here:

   1: function MapHelper(options) {
   2: var self = this;
   3: //  Initialize with the options supplied
   4: this.mapSelector = options.mapSelector;
   5: this.shapeDataSource = options.shapeDataSource;
   6:  
   7: // Attaches an event handler which zooms the map so that all map shapes are visible
   8: this.autoZoom = function() {
   9:     $(document).on("igmaprefreshcompleted", self.mapSelector, autoZoomHandler);
  10: }
  11:  
  12: //  Finds the extent of all shapes in the map control and calculates a proper
  13: //  map window so that all shapes are visible.
  14: //  Detaches from the refreshCompleted event of the map control after 
  15: //  themap rectangle is set to the map control
  16: function autoZoomHandler() {
  17:     var shapes = self.getAllShapesExtent();
  18:     if (shapes.length > 0) {
  19:         var allShapesBounds = self.findShapeArrayBounds(shapes);
  20:         var mapWindow = self.calculateMapWindow(allShapesBounds);
  21:         $(self.mapSelector).igMap("option", "windowRect", mapWindow);
  22:         $(document).off("igmaprefreshcompleted", self.mapSelector, autoZoomHandler);
  23:     }
  24: }
  25:  
  26: this.mapShapes = function (mapShape) {
  27:     var shapeData = self.shapeDataSource;
  28:     var shapeEnumerator = shapeData.converter().getEnumerator();
  29:     var mappedShapes = [];
  30:     while (shapeEnumerator.moveNext()) {
  31:         mappedShapes.push(mapShape(shapeEnumerator.current()));
  32:     }
  33:  
  34:     return mappedShapes;
  35: }
  36:  
  37: this.getAllShapesExtent = function () {
  38:     var shapeData = self.shapeDataSource;
  39:     var shapeEnumerator = shapeData.converter().getEnumerator();
  40:     var shapesArray = [];
  41:     while (shapeEnumerator.moveNext()) {
  42:         shapesArray.push(shapeEnumerator.current());
  43:     }
  44:     return shapesArray;
  45: }
  46:  
  47: this.findShapeArrayBounds = function(shapeArray) {
  48:     //  Store to improve performance and readability
  49:     var sCount = shapeArray.length;
  50:  
  51:     if (sCount > 0) {
  52:         var left, top, right, bottom;
  53:  
  54:         //  Enumerate shapes
  55:         for (var s = 0; s < sCount; s++) {
  56:             var currentShapeBounds = self.findShapeBounds(shapeArray[s]);
  57:  
  58:             if (currentShapeBounds.left < left || !left) left = currentShapeBounds.left;
  59:             if (currentShapeBounds.right > right || !right) right = currentShapeBounds.right;
  60:  
  61:             if (currentShapeBounds.top > top || !top) top = currentShapeBounds.top;
  62:             if (currentShapeBounds.bottom < bottom || !bottom) bottom = currentShapeBounds.bottom;
  63:         }
  64:  
  65:         return {
  66:             left: left,
  67:             right: right,
  68:             top: top,
  69:             bottom: bottom
  70:         };
  71:     }
  72: }
  73:  
  74: this.findShapeBounds = function(shape) {
  75:     var left, top, right, bottom;
  76:     var points = shape.points.item(0);
  77:     var pCount = points.count();
  78:     //  Enumerate shape points
  79:     if (pCount > 0) {
  80:         //  Find bounds of the state
  81:         for (var i = 0; i < pCount; i++) {
  82:             currentPoint = points.item(i);
  83:  
  84:             if (currentPoint.__x < left || !left) left = currentPoint.__x;
  85:             if (currentPoint.__x > right || !right) right = currentPoint.__x;
  86:  
  87:             if (currentPoint.__y > top || !top) top = currentPoint.__y;
  88:             if (currentPoint.__y < bottom || !bottom) bottom = currentPoint.__y;
  89:         }
  90:  
  91:         return {
  92:             left: left,
  93:             right: right,
  94:             top: top,
  95:             bottom: bottom
  96:         };
  97:     }
  98: }
  99:  
 100: this.calculateMapWindow = function(minViewWindow, zoomRatio) {
 101:     if (!zoomRatio) {
 102:         zoomRatio = 1;
 103:     }
 104:     //  Calculate central point and required radius
 105:     var width = minViewWindow.right - minViewWindow.left;
 106:     var height = minViewWindow.top - minViewWindow.bottom;
 107:     var centered = {
 108:         longitude: minViewWindow.right - width / 2,
 109:         latitude: minViewWindow.top - height / 2,
 110:         radius: (width > height) ? width / 2 * zoomRatio : height / 2 * zoomRatio
 111:     };
 112:     //  Calculate map window in relative units
 113:     var zoomRect = $(self.mapSelector).igMap("getZoomFromGeographic", self.geographicFromCentered(centered));
 114:     return zoomRect;
 115: }
 116:  
 117: //  Calculates the geographic coordinates of a square around a central point and radius
 118: this.geographicFromCentered = function(centered) {
 119:     var geographic =
 120:     {
 121:         left: centered.longitude - centered.radius,
 122:         top: centered.latitude - centered.radius,
 123:         width: centered.radius * 2,
 124:         height: centered.radius * 2
 125:     };
 126:     return geographic;
 127: }
 128: }

 

Now , let’s try to understand how does the scaling work.

   1: $.ig.loader({
   2:     scriptPath: "./js/",
   3:     cssPath: "./css/",
   4:     resources: "igMap"
   5: });
   6:  
   7: $.ig.loader("igMap", function () {
   8:     var worldCitiesSource = new $.ig.ShapeDataSource({
   9:         shapefileSource: 'world_cities.shp',
  10:         databaseSource: 'world_cities.dbf',
  11:         importCompleted: function (shapeSource) {
  12:             var helper = new MapHelper({
  13:                 mapSelector: "#map",
  14:                 shapeDataSource: shapeSource
  15:             });
  16:             var citiesData = helper.mapShapes(function (shape) {
  17:                 return {
  18:                     longitude: shape.points.item(0).item(0).__x,
  19:                     latitude: shape.points.item(0).item(0).__y,
  20:                     name: shape.fieldValues.NAME,
  21:                     //  23620000 is the max number in the data
  22:                     //  32 is scaling factor -> change to increase/decrease the size of the circles
  23:                     population: shape.fieldValues.POPULATION / 23620000 * 32,
  24:                     country: shape.fieldValues.COUNTRY,
  25:                     isCapital: shape.fieldValues.CAPITAL === "Y"
  26:                 };
  27:             });
  28:             createMap(citiesData);
  29:         }
  30:     });
  31:     worldCitiesSource.dataBind();
  32: });
  33:  
  34: function createMap(worldCities) {
  35:     $("#map").igMap({
  36:         width: "700px",
  37:         height: "700px",
  38:         verticalZoomable: true,
  39:         horizontalZoomable: true,
  40:         windowResponse: "immediate",
  41:         dataSource: worldCities,
  42:         series: [{
  43:             type: 'geographicProportionalSymbol',
  44:             name: 'worldCities',
  45:             latitudeMemberPath: 'latitude',
  46:             longitudeMemberPath: 'longitude',
  47:             radiusMemberPath: 'population',
  48:             markerType: 'automatic' //  It is mandatory to set any kind of marker because the default is (?!) 'none'
  49:                                     //  Whatever you set the control will always draw circles of varying size
  50:         }]
  51:     });
  52: }

The first few lines are loading of the appropriate resources , this is present in all of the setups where we use the Infragistics Loader to load our control resource files.

What we do next is we create a shape data source , specifying the location of our shape files. Once the files have been loaded up , we use the shape files source as a parameter to pass to our Map Helper object , which we use to enumerate through the source’s shapes. The symbols’ size is each city’s population value , based on the largest population value in the data source. Once we’ve established a formula for calculating the size of each symbol , we pass the now “prepared” data source to the map initialization function , which we’ve described in the createMap function. It’s just the standard way of initializing the map control.

On line 47 , we’re binding the radius of the symbol shapes to the radius calculating function we defined on line 23.  We also define the mandatory latitude and longitude properties , which are needed to pinpoint individual entries within the series.

To download the sample project , click on this link.

Check out my previous blogs on the Ignite UI Map here , here , here and here. And also  here.