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

1192
3
04-16-2019 10:57 AM
MarkCederholm
Occasional Contributor III
0 3 1,192

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