Saturday, March 28, 2009

ArcGIS Server Sample Flex Viewer: Capturing and Using Configuration Data


The Sample Flex Viewer (SFV) uses a simple xml file (config.xml) for application initialization and configuration. Application properties managed by this configuration file include, among others, UI attributes such as banner, title and logo, primary menu configuration, widget management, and layer management.



It is almost a certainty that any customization will require custom configuration settings, either populated by passing in URL parameters (in a future post) or by setting custom properties in the config.xml file. These data can be captured and used by making a few changes to core classes in the SFV. This example focuses on capturing a custom attribute added to one of the existing elements, in this case, the <mapservice> element.


The ultimate goal of this example is to provide some means of categorizing map services identified in the configuration file for display in multiple instances of the livemaps widget, each containing subsets of specific map services. We will do this by adding a custom attribute to the <mapservice> element called group.



<mapservice label="map label" type="dynamic, tiled, arcims, etc" visible="true/false" alpha="0..1" group="wlci">url</mapservice>



Now, getting the data. But first, we need some background. While we won’t be using it directly in this example, it is important to mention the configData class. The configData class does nothing more than provide a place to store the configuration data. It is a collection of arrays which correspond to application functional groups such as UI, menus, maps services, widgets etc. If your intent is to create an entirely new class of configuration data, let’s say for url parameters (more on that in a later post), you would provide the framework for that data in this class. Our example is a bit simpler, we are simply adding a new attribute to an existing class.


The class that does most of the legwork is the configManager. It begins by establishing and HTTPService connection (configService) to the config.xml file and then listens for a result (ResultEvent.RESULT). The important stuff occurs in the handler for this event. Within this handler, an instance of configData is created along with an xml dataset containing the contents of config.xml:



var configData:ConfigData = new ConfigData();

var configXML:XML = event.result as XML;



It then proceeds to parse out the functional collections of data as needed. The following illustrates how it parses out <mapservice> configuration data. This pattern can be used to obtain any attributes added to the mapservice element in the config file.


//================================================

//map

var configMap:Array = [];

var mapserviceList:XMLList = configXML..mapservice;

for (i = 0; i < mapserviceList.length(); i++) {


var msLabel:String = mapserviceList[i].@label;

var msType:String = mapserviceList[i].@type;

var msVisible:Boolean = true;

if (mapserviceList[i].@visible == "false")

msVisible = false;

var msAlpha:Number = 1;

if (!isNaN(mapserviceList[i].@alpha))

msAlpha = Number(mapserviceList[i].@alpha);

var msURL:String = mapserviceList[i];

var mapservice:Object =

{



label: msLabel,

type: msType,

visible: msVisible,

alpha: msAlpha,

url: msURL,



}

configMap.push(mapservice);



}

configData.configMap = configMap;



The previous block of code begins by creating an empty array for populating local data.



var mapserviceList:XMLList = configXML..mapservice;



creates an XMLList of all nodes in the DOM hierarchy called mapservice. The XMLList can then be examined just like an array, stepping through it by using a for statement. Attributes of the elements within the XMLList are then accessed explicitly via the attribute name. For example:



var msLabel:String = mapserviceList[i].@label;



retrieves the value assigned to the label attribute for that particular mapservice node. The value of the node itself is retrieved using the following syntax:



var msURL:String = mapserviceList[i];



For each node instance, a generic object of name/values pairs is then created containing each attribute value along with the value of the node itself. That object is then added to a local array for each node instance and then is assigned to the configData instance upon parsing completion.


For this example, recall that we have added a new attribute called group to each <mapservice> element. To retrieve that value, we simply need to create and assign a new local variable with the attribute value, add it to the generic name/value pairs object and we are golden.



//================================================

//map

...


for (i = 0; i < mapserviceList.length(); i++)

{



...

//retrieve newly added group attribute from mapservice entry

var msGroup:String = mapserviceList[i].@group;

...

var mapservice:Object =

{



...

//add newly added group attribute to mapservice object

group: msGroup,



}



configMap.push(mapservice);

}

configData.configMap = configMap;



Our configData instance now contains the newly added "group" attribute. Once it is completely populated, the CONFIG_LOADED event is dispatched which essentially notifies the application that the configData instance is now available for use. Using the data is even easier. All that is needed is to listen for the CONFIG_LOADED event:



SiteContainer.addEventListener(AppEvent.CONFIG_LOADED, config);



where config is a function whos argument is an event containing the data itself. It is then accessed via the following:



private function config(event:AppEvent):void

{



configData = event.data as ConfigData;



}

This is done by default in the MapManager.mxml component. Have a look at the function called config is this component and you will see that pattern for obtaining data from configData.

private function config (event:AppEvent):void

{

configData = event.data as ConfigData;

....

for (i = 0; i <donfigData.configMap.length; i++)

{

...

var url:String = confData.configMap[i].url;

...

}

}

A nice feature in the Sample Flex Viewer is that the BaseWidget class (that which we extend for all new widgets) exposes the configData object by default. This means we can then obtain data parsed from the config.xml file by the ConfigManager from any widget that extends the BaseWidget. To do this, we do something similar to what the MapManager component does.

for (var i:Number = 0; i <configData.configMap.length; i++)

{

...

var group:string = configData.configMap[i].group.toString();

...

}

26 comments:

Julia Kernitz said...

Hey,
I'm trying to just add two local shapefiles as layers to the Sample Viewer. Is this what I would have to do if I don't have an ArcServer license? I'm not great at this kind of thing and definitely can not afford to do anything through ESRI. Is there an easier way?

Map Vibe (Gregory L. Gunther) said...

Not so much an easy way. This post refers to configuration information stored in the config.xml file which is really pretty generic. It does however, point to AGS REST services. I am not sure the AGS Flex API has any components for parsing Shapefiles. You could however, use some sort of opensource (free) map server (ie. Map Server) to serve up a non-ESRI map service spec and consume that within this application. Your other option might be to encode your data as GML via GeoRSS and consume it that way. Hope this helps.

Manolo Frias said...

Hi,

where do you place the function config and the last loop in your example? I'm close to get it but I am a bit lost about where to put them.

Thanks,
Manolo

Map Vibe (Gregory L. Gunther) said...

Sorry, I left that out. Obtaining the info stored in configData is done in the MapManager class in the Sample Flex Viewer but you could to the same from anywhere. Have a look at MapManager and you will see that it is already listening for the CONFIG_LOADED and the config function is already defined there. You can also see the pattern for getting the data you need.

max said...

Hello GIS Hack. This has been a very educational experience. Unfortunately I am unable to produce the desired result.

I'm trying to either have two livemap widgets in the same map document or be able to group layers within a group attribute.

Should I be able to achieve these results using your instructions?

Are modifications needed to the LiveMapsWidget.mxml for this to work that are not listed here, such as creating a new private function? Does the for loop go into an existing function?

Thanks for the help.

Map Vibe (Gregory L. Gunther) said...

Max,

Yes, that is precisely how I have used it. See http://certmapper.cr.usgs.gov/data/envision/index.html. In this example, there are several implementations of the livemaps widget. You might look at a previous comment response I posted. I had left out an important detail that might help. Do you have a specific question related to this that I could help with?

max said...
This comment has been removed by the author.
max said...

guess I'm just having some trouble understanding where in the MapManager.mxml file to make the changes.

Here is what my code looks like:
//config
private function config(event:AppEvent):void...
...
for ... configExtents..

for (i = 0; i < configData.configMap.length; i++)
{
var label:String = configData.configMap[i].label;
var type:String = configData.configMap[i].type;
var url:String = configData.configMap[i].url;
var visible:Boolean = configData.configMap[i].visible;
var alpha:Number = Number(configData.configMap[i].alpha);
var group:String = configData.configMap[i].group;
switch (type.toLowerCase())
{
case "tiled":
{
var tiledlayer:ArcGISTiledMapServiceLayer = new ArcGISTiledMapServiceLayer(url);
tiledlayer.id = label;
tiledlayer.name = label;
configData.configMap[i] = group;
tiledlayer.visible = visible;
tiledlayer.alpha = alpha;
map.addLayer(tiledlayer);
break;
}

case "dynamic":
{
var dynlayer:ArcGISDynamicMapServiceLayer = new ArcGISDynamicMapServiceLayer(url);
dynlayer.id = label;
dynlayer.name = label;
configData.configMap[i] = group;
dynlayer.visible = visible;
dynlayer.alpha = alpha;
map.addLayer(dynlayer);
break;
}
}

}

I was hoping you could tell me what I'm doing wrong.

thanks.

Map Vibe (Gregory L. Gunther) said...

I'll have a look.

max said...

Oh, I got it, thanks for the link to your page. That helped a lot! I'll be in touch!

Map Vibe (Gregory L. Gunther) said...

That is great. Let me know if you have additional questions.

Manolo Frias said...

Hello,

I am stack at the same point as max was. I did exactly like him but I still don't get it.

Max: can you provide your solution? Or Greg can you help me on this?

Thanks,
Manolo

Map Vibe (Gregory L. Gunther) said...

Max, perhaps to could help me explain how you were able to come up with a solution. That might help me know what I missed in the description.

max said...

Hello,

you have to check if the config xml has a group layer tag

...
if(configXML)
{
//labels
...
layerGroup = configXML.group.groupname;
}

if it does, you have to make sure to use only the groups in the config xml file

toc.map = map;
widgetLayerLabels = getWidgetayerLabels(layerGroup);
...

you also need to get the group labels for the application by

private function getWidgetLayerLabels(mapGroup:String):ArrayCollection
{
var theLabels:ArrayCollection = new ArrayCollection();
for (var i:Number = 0; i < configData.configMap.length; i++)
{
if (mapGroup == configData.configMap[i].group.toString()){
theLabels.addItem(configData.configMap[i].label);

and then modify the .xml doc that goes with the swf file by adding

group>
groupname>MyFirstGroup/groupname>
/group>
to the configuration
and make sure you have a reference to both the new swf file and the new or multiple group xml documents (one for each group type)

max said...
This comment has been removed by the author.
max said...

I was wondering if you could tell me how the xmeta.jsp metadata parser works? is that something I can download from usgs? the layer details section is pretty cool.

Map Vibe (Gregory L. Gunther) said...

Thanks Max. You actually took it to completion which is good.

I updated the blog entry in hopes of minimizing the confusion as of late. Hope this helps.

Manolo Frias said...

Hi Greg,

unfortunately I still don't get it to work. If I follow the correction of your instructions this doesn't work since group type is not defined:

var url:group = configData.configMap[i].group.toString();

If you meant type String that doesn't work either.

My code looks as you see below. Should I add the function made by Max? Where?

Regards,
Manolo

for (i = 0; i < configData.configMap.length; i++)
{
var label:String = configData.configMap[i].label;
var type:String = configData.configMap[i].type;
var url:String = configData.configMap[i].url;
//var url:Group = configData.configMap[i].group.toString();
var visible:Boolean = configData.configMap[i].visible;
var alpha:Number = Number(configData.configMap[i].alpha);
var group:String = configData.configMap[i].group.toString();
switch (type.toLowerCase())
{
case "tiled":
{
var tiledlayer:ArcGISTiledMapServiceLayer = new ArcGISTiledMapServiceLayer(url);
tiledlayer.id = label;
tiledlayer.name = label;
configData.configMap[i] = group;
tiledlayer.visible = visible;
tiledlayer.alpha = alpha;
map.addLayer(tiledlayer);
break;
}

case "dynamic":
{
var dynlayer:ArcGISDynamicMapServiceLayer = new ArcGISDynamicMapServiceLayer(url);
dynlayer.id = label;
dynlayer.name = label;
configData.configMap[i] = group;
dynlayer.visible = visible;
dynlayer.alpha = alpha;
map.addLayer(dynlayer);
break;
}
}
}

Map Vibe (Gregory L. Gunther) said...

Sorry, typo on my part. Should be variable group of type string.

var group:string.

So what error are you getting. Is the data in configData when you debug. If not, that means it isn't being captured earlier on in the process are it isn't populated in the config.xml file.

Greg Knight said...

Nice post Greg. Exactly what I needed today. Well done, thanks!

Map Vibe (Gregory L. Gunther) said...

Glad it was useful. Thanks for the kind words.

Manolo Frias said...

Hi again!

I don't get any error messages. The problem is that I get the same data in both widgets.

I followed your instructions thoroughly and modified the code (see above) but it didn't help.

I just wonder if I should apply the code Max wrote and in that case where.

Could you be so kind to help me? I feel I am so close but my (still) newbie status is a hinder!

Thanks,
Manolo

Unknown said...

Hello,

I have a a couple quick questions about using multiple live maps. When you implement this do you create new widgets that are clones of the live maps widget or do you have multiple instances of the live maps widget with different labels and the added group parameter? It looks to me like the group parameter is used to point to "blocks" of mapservice tags for each instance of live maps. So there would be a block of mapservice tags for each live maps instance and a group tag that defines the groups, correct?
thank you, Paul

Unknown said...

Hello again,

I realized that I could look at your config.xml on the USGS website linked in the comments. I see that there is a copy of the modified livemaps widget for each of your map sets. I can see that the mapservice tags for each set of maps has the same group value. What I'm not getting is how each of the live map widgets is linked to the proper group, since the group values are not referenced in the widget tag. Are the widgets linked to the groups somewhere else?

thank you, Paul

Unknown said...

Hello yet again..

I'm a doofus. I just realized that I could also look at the multigrouplivemapswidget.xml for one of your duplicate live maps widget and all was explained. Thank you for the great information on your blog.

cheers, Paul

Unknown said...

Hello,

I think I'm on the right track after looking at the xml files on the USGS site. Now I'm trying to figure out how the modification to the mapmanager mxml that max made fit in. When I try to use his code to check the configXML as below, I get a compile error that the private function getWidgetLayerLables does not return a value.

Here's the check configXML for groups code:

//test config xml for group data
if(configXML)
{
//labels
layerGroup = configXML.group.groupname;
}
toc.map = map;
widgetLayerLabels = getWidgetayerLabels(layerGroup);


And here's the private function..

private function widgetShowInfo(event:AppEvent):void
{
infoPopup.infoData = event.data;
}
// get group labels
private function getWidgetLayerLabels(mapGroup:String):ArrayCollection
{
var theLabels:ArrayCollection = new ArrayCollection();
for (var i:Number = 0; i < configData.configMap.length; i++)
{
if (mapGroup == configData.configMap[i].group.toString()){
theLabels.addItem(configData.configMap[i].label);
}}}

I'm somewhat at a loss as to where this code needs to be added to mapmanager.