Here's How to Open a Mobile Map with Group Layers

466
3
04-16-2019 10:57 AM
Regular Contributor
0 3 466

Screen shot of example app in action

Given the fact that the ArcGIS Runtime SDK for .NET supports group layers at 100.5, and that they work perfectly fine in 2D maps, it's somewhat disappointing that mobile maps with group layers are still not hydrated properly.  Esri can mansplain their issues until their faces turn blue, but the fact is that the tools exist right now to do it. In this post I will show you a functional workaround.

Before I start showing any code, let's take a peek at the internal structure of a mobile map package, as produced by ArcGIS Pro 2.3.2:

MMPK top level

The .info file is a JSON file, and it's important because it provides the link between the map index and the map name:

"maps": [ "Mohave Electric MMPK" ]‍‍‍‍‍

The "Create Mobile Map Package" tool enforces unique map names, so the problem of duplicate map names should never arise.  My own practice is to have only one map per MMPK for operational layers only, no base layers.  This allows separation of packages based on frequency of updates:  facility data (frequent updates), landbase (infrequent updates), and basemaps (vector or raster tile packages).  A Runtime app can then open each package, grab its layers, and add them to the main map.

Now let's look at the p14 subfolder:

MMPK p14 folder contents

The .mapx file is a JSON file containing all the information needed for ArcGIS Pro to hydrate the map; it uses the CIM (Cartographic Information Model) specification.  In an ideal world, that would be the file that Runtime uses to open a map, but it's not.  Runtime uses the .mmap file to open maps:  it's also a JSON file, but it doesn't use CIM.  Although the mobile map specification is not fully aligned with CIM, nonetheless it does store group layer information:

          {
"id" : "b4a38ad5025b46d1ac809b3ed5258935",
"title" : "Transformers",
"visibility" : true,
"layerType" : "GroupLayer",
"layers" : [
{
"id" : "2c4d3bafe82c4344a0bc2019b67097cc",
"title" : "Capacitor Bank",
"visibility" : true,
"layerType" : "ArcGISFeatureLayer",
"layerDefinition" : {
"minScale" : 4000
},
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

What it does not store, however, is any minScale or maxScale information for the group layer.  You can look that up in the .mapx file, as long as the group layers are uniquely named, but I don't bother.  The workaround is to design the mobile map without scale ranges on the group layers.

I've attached an example Visual Studio project which retrieves the .info and .mmap files to extract the operational layer definitions:

			// Open mmpk as zip archive

string sMapName = null;
object[] oplayers = null;
using (ZipArchive zip = ZipFile.Open(sPath, ZipArchiveMode.Read))
{

// Parse the info file to get map name

string sInfoFile = Path.GetFileNameWithoutExtension(sPath) + ".info";
sMapName = GetMapName(zip, sInfoFile, iMapIndex);

// Parse corresponding .mmap file and get operational layers

if (!string.IsNullOrEmpty(sMapName))
{
string sMapFile = "p14/" + sMapName + ".mmap";
oplayers = GetOperationalLayers(zip, sMapFile);
}

}
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

The code uses the .NET JavaScriptSerializer object to parse the JSON:

		private static string GetMapName(ZipArchive zip, string sInfoFile, int iMapIndex)
{

string sJSON = GetEntry(zip, sInfoFile);
if (string.IsNullOrEmpty(sJSON))
return null;
Dictionary<string, object> dict = js.DeserializeObject(sJSON) as Dictionary<string, object>;
if (!dict.ContainsKey("maps"))
return null;
if (!(dict["maps"] is object[] maps))
return null;
if (iMapIndex > maps.Count() - 1)
return null;
return maps[iMapIndex].ToString();

}

private static object[] GetOperationalLayers(ZipArchive zip, string sMapFile)
{

string sJSON = GetEntry(zip, sMapFile);
if (string.IsNullOrEmpty(sJSON))
return null;
Dictionary<string, object> dict = js.DeserializeObject(sJSON) as Dictionary<string, object>;
if (!dict.ContainsKey("map"))
return null;
if (!(dict["map"] is Dictionary<string, object> map))
return null;
if (!map.ContainsKey("operationalLayers"))
return null;
return map["operationalLayers"] as object[];

}

private static string GetEntry(ZipArchive zip, string sEntryName)
{
ZipArchiveEntry zipInfo = zip.GetEntry(sEntryName);
if (zipInfo == null)
return null;
string sJSON = null;
using (StreamReader reader = new StreamReader(zipInfo.Open()))
{
sJSON = reader.ReadToEnd();
}
return sJSON;
}

‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Then it opens the map and restructures it to insert group layers in the appropriate places.  

		private static void AddLayers(Map MyMap, GroupLayer glParent, object[] sublayers)
{

foreach (object obj in sublayers)
{

if (!(obj is Dictionary<string, object> layer))
continue;
if (!layer.ContainsKey("layerType"))
continue;
string sLayerType = layer["layerType"].ToString();

// Process layer

Layer lyr = null;
if (sLayerType == "GroupLayer")
{

// Create group layer
// **** LIMITATION: .mmap group layer entry does not store minScale or maxScale ****
// [MMPKs should be created with that limitation in mind]

GroupLayer glChild = new GroupLayer();
string sDisplayName = "";
if (layer.ContainsKey("title"))
sDisplayName = layer["title"].ToString();
glChild.Name = sDisplayName;
bool bVisibility = true;
if (layer.ContainsKey("visibility"))
bVisibility = (bool)layer["visibility"];
glChild.IsVisible = bVisibility;
if (!layer.ContainsKey("layers"))
continue;
if (!(layer["layers"] is object[] layers))
continue;
AddLayers(null, glChild, layers);
lyr = glChild;

}
else
{

// Get layer and add to parent

if (!layer.ContainsKey("id"))
continue;
string sID = layer["id"].ToString();
if (!OpLayers.ContainsKey(sID))
continue;
lyr = OpLayers[sID];

}
if (lyr == null)
continue;
if (glParent == null)
MyMap.OperationalLayers.Add(lyr);
else
glParent.Layers.Add(lyr);

}

}
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

As a bonus, the example code also builds an interactive TOC.

Enjoy!

[P.S. -- I've heard rumors that ArcGIS Pro 2.4 will introduce potentially code-breaking changes to the mobile map specification.  Watch out for that.]

3 Comments
Regular Contributor

UPDATE:  The example project has been updated with an option to allow older (pre-100.5) behavior, where instead of actually creating group layers, the app has TOC items imitate them.  This is a workaround for a recently discovered 100.5 bug that affects the ability of sublayers of a group layer to query related records.

Regular Contributor

100.6 UPDATE:  100.6 properly populates group layers in a MMPK.  However, a bug identified at 100.5 is still present: QueryRelatedFeaturesAsync will not return results for feature layer that is a sublayer of a group layer.

Regular Contributor

100.7 UPDATE:  100.7 fixes the above bug, and also fixes another bug where changing ScaleSymbols for a sublayer of the group layer would cause the app to crash.  Group layers appear to be safe to use now.