[fusion-commits] r2428 - in trunk: layers layers/MapGuide layers/MapGuide/php lib/OpenLayers widgets

svn_fusion at osgeo.org svn_fusion at osgeo.org
Mon Sep 26 08:44:33 EDT 2011


Author: jng
Date: 2011-09-26 05:44:33 -0700 (Mon, 26 Sep 2011)
New Revision: 2428

Modified:
   trunk/layers/Layers.js
   trunk/layers/MapGuide/MapGuide.js
   trunk/layers/MapGuide/php/LoadMap.php
   trunk/layers/MapGuide/php/LoadScaleRanges.php
   trunk/layers/MapGuide/php/Utilities.php
   trunk/lib/OpenLayers/OpenLayers.js
   trunk/widgets/Legend.js
Log:
#460: Port over the legend performance enhancements from my github fork. A summary of how this works:

The existing implementation will send a mapagent icon request for each layer style icon in the legend. For a map with 100s of layers and/or themes, this results in an excessive amount of requests to the mapagent, causing CPU spikes in the client's web browser and general terrible performance on large maps.

This submission attempts a multi-pronged approach to tackling this problem:

 1. We utilise the new GetResourceContents API in MgResourceService to fetch layer definition contents in a single batch. Also we create the series of DOMDocuments in a single batch as well and pass this document to any utility function that interrogates the XML content. Such utility functions have been modified to accept a DOMDocument parameter.

 2. We pre-cache layer icons by rendering them out as inline data URIs in the JSON returned by LoadScaleRanges.php. Each inline data URI eliminates a mapagent icon request. We currently use a metric of pre-caching 25 layer icons per scale range. This can be tweaked in LoadScaleRanges.php. For general maps, this pretty much eliminates all mapagent icon requests (and the bulk of the legend bottlenecks). Now unfortunately, if you are using a certain web browser from redmond that is older than 8.0, this will not work (data URIs are not supported by these versions), so for these browsers, the default behaviour applies.

 3. We add support to OpenLayers.Layer.MapGuide for passing in a series of alternate mapagent urls. If an array of alternate urls is indeed passed to the ctor, mapagent icon requests will use a random mapagent alternate url if there are any specified. This approach workaround the outbound request limitations of one certain web browser from redmond. Alternate mapagent urls can be specified in the ApplicationDefinition with a comma-separated list of IP addresses and/or host names in a <AlternateHostName> element of the <Options> element of your MapGuide map in your fusion MapGroup. As a positive side-effect, tile requests also benefit from this change.

Modified: trunk/layers/Layers.js
===================================================================
--- trunk/layers/Layers.js	2011-09-01 08:37:45 UTC (rev 2427)
+++ trunk/layers/Layers.js	2011-09-26 12:44:33 UTC (rev 2428)
@@ -682,6 +682,7 @@
     initialize: function(o, layerType, iconOpt) {
         this.minScale = o.minScale;
         this.maxScale = o.maxScale;
+        this.isCompressed = o.isCompressed;
         if (this.maxScale == 'infinity' || this.maxScale == 'auto') {
           this.maxScale = Infinity;
         }
@@ -694,6 +695,14 @@
         var staticIcon = o.styles.length>1 ? false : layerType;
         for (var i=0; i<o.styles.length; i++) {
             var styleItem = new Fusion.Layers.StyleItem(o.styles[i], staticIcon, iconOpt);
+            if (o.styles[i].imageData)
+            {
+                styleItem.iconOpt = {
+                    url: o.styles[i].imageData,
+                    width: iconOpt.width,
+                    height: iconOpt.height
+                };
+            }
             this.styles.push(styleItem);
         }
     },

Modified: trunk/layers/MapGuide/MapGuide.js
===================================================================
--- trunk/layers/MapGuide/MapGuide.js	2011-09-01 08:37:45 UTC (rev 2427)
+++ trunk/layers/MapGuide/MapGuide.js	2011-09-26 12:44:33 UTC (rev 2428)
@@ -50,6 +50,7 @@
       query: true,
       edit: true
     },
+    alternateHostNames: null, //a comma-delimited list of alternate host names to use
 
     initialize: function(map, mapTag, isMapWidgetLayer) {
         // console.log('MapGuide.initialize');
@@ -92,6 +93,11 @@
           }
         }
         
+        //Store the list of alternate host names
+        if (mapTag.layerOptions.AlternateHostNames) {
+            this.alternateHostNames = mapTag.layerOptions.AlternateHostNames;
+        }
+        
         rootOpts = {
           displayInLegend: this.bDisplayInLegend,
           expandInLegend: this.bExpandInLegend,
@@ -442,9 +448,12 @@
         var sl = Fusion.getScriptLanguage();
         var loadmapScript = 'layers/' + this.arch + '/' + sl  + '/LoadScaleRanges.' + sl;
 
+        //IE7 or lower: No pre-caching for you!
+        var preCacheIcons = !(Browser.Engine.trident4 || Browser.Engine.trident5);
+        //console.log("Layer icon pre-caching enabled: " + preCacheIcons);
         var sessionid = this.getSessionID();
 
-        var params = {'mapname': this._sMapname, "session": sessionid};
+        var params = {'mapname': this._sMapname, "session": sessionid, "preCacheIcons": preCacheIcons};
         var options = {onSuccess: OpenLayers.Function.bind(this.scaleRangesLoaded,this),
                        parameters:params};
         Fusion.ajaxRequest(loadmapScript, options);
@@ -685,6 +694,26 @@
       } else {
         url = Fusion.getConfigurationItem('mapguide', 'mapAgentUrl');
       }
+      
+      if (this.alternateHostNames)
+      {
+        var hosts = this.alternateHostNames.split(",");
+        var httpIndex = url.indexOf("http://") + 7;
+        if (httpIndex < 7) {
+            httpIndex = url.indexOf("https://") + 8;
+        }
+        var proto = url.substring(0, httpIndex);
+        var relIndex = url.indexOf("/", httpIndex+1);
+        var relPath = url.substring(relIndex);
+        
+        layerOptions.alternateUrls = [];
+        
+        for (var i = 0; i < hosts.length; i++) {
+            var altUrl = proto + hosts[i] + relPath;
+            layerOptions.alternateUrls.push(altUrl);
+        }
+      }
+      
       var oNewLayerOL = new OpenLayers.Layer.MapGuide( layerName, url, params, layerOptions );
       return oNewLayerOL;
     },
@@ -1278,7 +1307,20 @@
         }
         else
         {
-            var url = Fusion.getConfigurationItem('mapguide', 'mapAgentUrl');
+            if (style.iconOpt && style.iconOpt.url)
+            {
+                //if (style.iconOpt.url.indexOf("data:image") >= 0)
+                //    console.log("Fetching pre-cached icon");
+                return style.iconOpt.url;
+            }
+                
+            var origUrl = Fusion.getConfigurationItem('mapguide', 'mapAgentUrl');
+            var altUrl = null;
+            if (this.oLayerOL && this.oLayerOL.alternateUrls && this.oLayerOL.alternateUrls.length > 0) {
+                altUrl = this.oLayerOL.getNextAltURL();
+            }
+            var url = (altUrl == null) ? origUrl : altUrl;
+            
             url += "?OPERATION=GETLEGENDIMAGE&SESSION=" + layer.oMap.getSessionID();
             url += "&VERSION=1.0.0&SCALE=" + fScale;
             op = /\(/g; cp = /\)/g; 

Modified: trunk/layers/MapGuide/php/LoadMap.php
===================================================================
--- trunk/layers/MapGuide/php/LoadMap.php	2011-09-01 08:37:45 UTC (rev 2427)
+++ trunk/layers/MapGuide/php/LoadMap.php	2011-09-26 12:44:33 UTC (rev 2428)
@@ -127,34 +127,50 @@
 
     //layers
     $mapObj->layers = array();
-
-
-    $mapObj->layers = array();
+    $layerDefinitionIds = new MgStringCollection();
+    
     for($i=0;$i<$layers->GetCount();$i++)
     {
+        $layer = $layers->GetItem($i);
+        $lid = $layer->GetLayerDefinition();
+        $layerDefinitionIds->Add($lid->ToString());
+    }
+    
+    //Get the layer contents in a single batch
+    $layerDefinitionContents = $resourceService->GetResourceContents($layerDefinitionIds, null);
+    $layerDocs = array();
+    for($i=0;$i<$layers->GetCount();$i++)
+    {
+        $content = $layerDefinitionContents->GetItem($i);
+        $doc = DOMDocument::LoadXML($content);
+        array_push($layerDocs, $doc);
+    }
+    
+    for($i=0;$i<$layers->GetCount();$i++)
+    {
         //only output layers that are part of the 'Normal Group' and
         //not the base map group used for tile maps.  (Where is the test for that Y.A.???)
 
         $layer=$layers->GetItem($i);
-        $layerDefinition = $layer->GetLayerDefinition();
+        $content = $layerDocs[$i];
         $layerObj = NULL;
-        $mappings = GetLayerPropertyMappings($resourceService, $layer);
+        $mappings = GetLayerPropertyMappings($resourceService, $layer, $content);
         $_SESSION['property_mappings'][$layer->GetObjectId()] = $mappings;
 
         $layerObj->uniqueId = $layer->GetObjectId();
         $layerObj->layerName = addslashes($layer->GetName());
 
         //$aLayerTypes = GetLayerTypes($featureService, $layer);
-        $aLayerTypes = GetLayerTypesFromResourceContent($layer);
+        $aLayerTypes = GetLayerTypesFromResourceContent($layer, $content);
         $layerObj->layerTypes = $aLayerTypes;
 
-        $layerObj->resourceId = $layerDefinition->ToString();
+        $layerObj->resourceId = $layerDefinitionIds->GetItem($i);
         $layerObj->parentGroup = $layer->GetGroup() ? $layer->GetGroup()->GetObjectId() : '';
 
         $layerObj->selectable = $layer->GetSelectable();
         $layerObj->visible = $layer->GetVisible();
         $layerObj->actuallyVisible = $layer->isVisible();
-        $layerObj->editable = IsLayerEditable($resourceService, $layer);
+        $layerObj->editable = IsLayerEditable($resourceService, $layer, $content);
 
         $isBaseMapLayer = ($layer->GetLayerType() == MgLayerType::BaseMap);
         $layerObj->isBaseMapLayer = $isBaseMapLayer;

Modified: trunk/layers/MapGuide/php/LoadScaleRanges.php
===================================================================
--- trunk/layers/MapGuide/php/LoadScaleRanges.php	2011-09-01 08:37:45 UTC (rev 2427)
+++ trunk/layers/MapGuide/php/LoadScaleRanges.php	2011-09-26 12:44:33 UTC (rev 2428)
@@ -40,7 +40,25 @@
 include('../../../common/php/Utilities.php');
 include('Utilities.php');
 
+//This flag indicates whether to pre-cache the legend icons (in the form of data URIs that will be written back as part of the JSON response)
+//Browsers that do not support data URIs will not pass "true" and thus no pre-caching is performed.
+$preCacheIcons = false;
 
+//This is used by pre-caching to determine how many legend icons to pre-cache up-front (if $preCacheIcons = true)
+//$maxScaleRangeDepth = 3; //The maximum number of scale ranges to go through (topmost to bottom)
+$maxIconsPerScaleRange = 25; //The maximum number of icons to pre-cache per scale range. If the number of rules exceeds this value, the themed result will be compressed.
+//$maxLegendHeight = 800; //The maximum screen space available to pre-cache icons
+//$legendPos = 0; //Indicates how much screen space has already been allocated by pre-cached icons. Pre-caching stops after this value exceeds $maxLegendHeight
+//$advanceHeight = 20; //16px with 4px padding. This is just a logical guess of how much actual space one legend icon occupies in the legend widget
+//$maxGroupIndex = 5; //An initial guess of how many groups whose layer icons we can pre-cache.
+
+// Determine if we should pre-cache legend icons
+if (isset($_REQUEST['preCacheIcons']) && ($_REQUEST['preCacheIcons'] == "1" || strtolower($_REQUEST['preCacheIcons']) == "true")) {
+    $preCacheIcons = true;
+}
+
+$mappingService = $siteConnection->CreateService(MgServiceType::MappingService);
+
 $map = new MgMap();
 $map->Open($resourceService, $mapName);
 $layers=$map->GetLayers();
@@ -57,10 +75,50 @@
         $scaleranges = $_SESSION['scale_ranges'][$layer->GetObjectId()];
         $layerObj = NULL;
         $layerObj->uniqueId = $layer->GetObjectId();
+        $layerObj = NULL;
+        $layerObj->uniqueId = $layer->GetObjectId();
+        
+        $ldfId = $layer->GetLayerDefinition();
+        foreach ($scaleranges as $sr)
+        {
+            $scaleVal = 42;
+            if (strcmp($sr->maxScale, "infinity") == 0)
+                $scaleVal = intval($sr->minScale);
+            else
+                $scaleVal = (intval($sr->minScale) + intval($sr->maxScale)) / 2.0;
+         
+            //Set compression flag
+            $styleCount = count($sr->styles);
+            $sr->isCompressed = ($styleCount > $maxIconsPerScaleRange);
+            if ($sr->isCompressed)
+            {
+                //First
+                $style = $sr->styles[0];
+                if ($preCacheIcons == true)
+                    $style->imageData = GetLegendImageInline($mappingService, $ldfId, $scaleVal, $style->geometryType, $style->categoryIndex);
+                
+                //Pass over ones in between
+                
+                //Last
+                $style = $sr->styles[$styleCount - 1];
+                if ($preCacheIcons == true)
+                    $style->imageData = GetLegendImageInline($mappingService, $ldfId, $scaleVal, $style->geometryType, $style->categoryIndex);
+            }
+            else
+            {
+                if ($preCacheIcons == true)
+                {
+                    foreach ($sr->styles as $style)
+                    {
+                        $style->imageData = GetLegendImageInline($mappingService, $ldfId, $scaleVal, $style->geometryType, $style->categoryIndex);
+                    }
+                }
+            }
+        }
         $layerObj->scaleRanges = $scaleranges;
         array_push($scaleObj->layers, $layerObj);
     }
- }
+}
 
 header('Content-type: application/json');
 

Modified: trunk/layers/MapGuide/php/Utilities.php
===================================================================
--- trunk/layers/MapGuide/php/Utilities.php	2011-09-01 08:37:45 UTC (rev 2427)
+++ trunk/layers/MapGuide/php/Utilities.php	2011-09-26 12:44:33 UTC (rev 2428)
@@ -608,10 +608,12 @@
 
 
 /* retrieve the property mappings for a layer */
-function GetLayerPropertyMappings($resourceService, $layer) {
+function GetLayerPropertyMappings($resourceService, $layer, $xmldoc = NULL) {
     $mappings = array();
-    $byteReader = $resourceService->GetResourceContent($layer->GetLayerDefinition());
-    $xmldoc = DOMDocument::loadXML(ByteReaderToString($byteReader));
+    if ($xmldoc == NULL) {
+        $byteReader = $resourceService->GetResourceContent($layer->GetLayerDefinition());
+        $xmldoc = DOMDocument::loadXML(ByteReaderToString($byteReader));
+    }
     $mappingNodeList = $xmldoc->getElementsByTagName('PropertyMapping');
     for ($i=0; $i<$mappingNodeList->length; $i++) {
         $mapping = $mappingNodeList->item($i);
@@ -625,11 +627,13 @@
 }
 
 /* retrieve the property mappings for a layer */
-function IsLayerEditable($resourceService, $layer) {
+function IsLayerEditable($resourceService, $layer, $xmldoc = NULL) {
     $result = true;
     $dataSourceId = new MgResourceIdentifier($layer->GetFeatureSourceId());
-    $byteReader = $resourceService->GetResourceContent($dataSourceId);
-    $xmldoc = DOMDocument::loadXML(ByteReaderToString($byteReader));
+    if ($xmldoc == NULL) {
+        $byteReader = $resourceService->GetResourceContent($dataSourceId);
+        $xmldoc = DOMDocument::loadXML(ByteReaderToString($byteReader));
+    }
     $parameterList = $xmldoc->getElementsByTagName('Parameter');
     for ($i=0; $i<$parameterList->length; $i++) {
         $parameter = $parameterList->item($i);
@@ -903,4 +907,77 @@
     $serverVersion = $serverAdmin->GetSiteVersion();
     return $serverVersion;
 }
+
+// GetLegendImageInline
+//
+// Returns a data URI containing the base64 encoded content of the specified legend icon
+//
+// Due to the fixed size (16x16 px), the generated data URI will easily fall under the data URI limit of most (if not all) web browsers that support it.
+//
+function GetLegendImageInline($mappingService, $layerDefinitionId, $scale, $geomType, $themeCategory)
+{
+    $icon = $mappingService->GenerateLegendImage($layerDefinitionId, $scale, 16, 16, "PNG", $geomType, $themeCategory);
+    if ($icon != null)
+    {
+        $str = "";
+        $buffer = '';
+        while ($icon->Read($buffer, 50000) != 0)
+        {
+            $str .= base64_encode($buffer);
+        }
+        
+        $str = "data:image/png;base64,". $str;
+        return $str;
+    }
+    //$styleObj->imageData = "http://localhost/mapguide/mapagent/mapagent.fcgi?OPERATION=GETLEGENDIMAGE&VERSION=1.0.0&SESSION=$sessionID&SCALE=$scaleVal&LAYERDEFINITION=".$resID->ToString()."&TYPE=".$styleObj->geometryType."&THEMECATEGORY=".$styleObj->categoryIndex;
+    return null;
+}
+
+// Helper function to render out error messages for any PHP script that outputs images
+function RenderTextToImage($text) {
+    // Set font size
+    $font_size = 4;
+
+    $ts = explode("\n",$text);
+    $width = 0;
+    foreach ($ts as $k => $string) { //compute width
+        $width = max($width,strlen($string));
+    }
+
+    // Create image width dependant on width of the string
+    $width = imagefontwidth($font_size)*$width;
+    // Set height to that of the font
+    $height = imagefontheight($font_size)*count($ts);
+    $el = imagefontheight($font_size);
+    $em = imagefontwidth($font_size);
+    // Create the image pallette
+    $img = imagecreatetruecolor($width,$height);
+    // Dark red background
+    $bg = imagecolorallocate($img, 0xAA, 0x00, 0x00);
+    imagefilledrectangle($img, 0, 0,$width ,$height , $bg);
+    // White font color
+    $color = imagecolorallocate($img, 255, 255, 255);
+
+    foreach ($ts as $k=>$string) {
+        // Length of the string
+        $len = strlen($string);
+        // Y-coordinate of character, X changes, Y is static
+        $ypos = 0;
+        // Loop through the string
+        for($i=0;$i<$len;$i++){
+            // Position of the character horizontally
+            $xpos = $i * $em;
+            $ypos = $k * $el;
+            // Draw character
+            imagechar($img, $font_size, $xpos, $ypos, $string, $color);
+            // Remove character from string
+            $string = substr($string, 1);
+        }
+    }
+    // Return the image
+    header("Content-Type: image/png");
+    imagepng($img);
+    // Remove image
+    imagedestroy($img);
+}
 ?>

Modified: trunk/lib/OpenLayers/OpenLayers.js
===================================================================
--- trunk/lib/OpenLayers/OpenLayers.js	2011-09-01 08:37:45 UTC (rev 2427)
+++ trunk/lib/OpenLayers/OpenLayers.js	2011-09-26 12:44:33 UTC (rev 2428)
@@ -41182,6 +41182,13 @@
      **/
     tileOriginCorner: "tl",
 
+    /** 
+     * APIProperty: alternateUrls
+     * {Array} An array of alternate host urls to different mapagents that point to the same MapGuide Server. These host names can resolve to the same IP address
+     **/
+    alternateUrls: [],
+    altHostIndex: -1,
+
     /**
      * Constructor: OpenLayers.Layer.MapGuide
      * Create a new Mapguide layer, either tiled or untiled.  
@@ -41243,9 +41250,14 @@
         }
 
         if (options && options.useOverlay!=null) {
-          this.useOverlay = options.useOverlay;
+            this.useOverlay = options.useOverlay;
         }
         
+        if (options.alternateUrls && options.alternateUrls.length > 0) {
+            this.alternateUrls = options.alternateUrls;
+            this.altHostIndex = 0;
+        }
+        
         //initialize for untiled layers
         if (this.singleTile) {
           if (this.useOverlay) {
@@ -41299,6 +41311,22 @@
       return obj;
     },
 
+    getNextAltURL: function() { 
+        //If alternate hosts exist, then each call is a round-robin sequence of:
+        // [Original], [Alt Host #1], [Alt Host #2], ... [Alt Host #last], [Original]
+        var altUrl = null;
+        if (this.alternateUrls.length > 0) {
+            if (this.altHostIndex == this.alternateUrls.length) {
+                altUrl = null;
+                this.altHostIndex = 0;
+            } else { 
+                altUrl = this.alternateUrls[this.altHostIndex];
+                this.altHostIndex++;
+            }
+        }
+        return altUrl;
+    },
+
     /**
      * Method: getURL
      * Return a query string for this layer
@@ -41316,6 +41344,8 @@
         var url;
         var center = bounds.getCenterLonLat();
         var mapSize = this.map.getSize();
+        
+        var altUrl = this.getNextAltURL();
 
         if (this.singleTile) {
           //set up the call for GETMAPIMAGE or GETDYNAMICMAPOVERLAY with
@@ -41343,7 +41373,7 @@
             OpenLayers.Request.GET({url: url, async: false});
           }
           //construct the full URL
-          url = this.getFullRequestString( params );
+          url = this.getFullRequestString( params, altUrl );
         } else {
 
           //tiled version
@@ -41367,7 +41397,7 @@
                        tilecol: colidx,
                        tilerow: rowidx,
                        scaleindex: this.resolutions.length - this.map.zoom - 1
-                    });
+                    }, altUrl);
           }
        }
        return url;

Modified: trunk/widgets/Legend.js
===================================================================
--- trunk/widgets/Legend.js	2011-09-01 08:37:45 UTC (rev 2427)
+++ trunk/widgets/Legend.js	2011-09-26 12:44:33 UTC (rev 2428)
@@ -692,6 +692,8 @@
               }
             }
             if (style.iconOpt && style.iconOpt.url) {
+                //if (style.iconOpt.url.indexOf("data:image") >= 0)
+                //    console.log("Fetching pre-cached icon");
                 opt.image = style.iconOpt.url;
             } else {
                 opt.image = layer.oMap.getLegendImageURL(scale, layer, style);



More information about the fusion-commits mailing list