MapCruncherConsole\MapCruncherClass.cs
// Copyright 2010 ESRI // // All rights reserved under the copyright laws of the United States // and applicable international laws, treaties, and conventions. // // You may freely redistribute and use this sample code, with or // without modification, provided you include the original copyright // notice and use restrictions. // // See the use restrictions. // using System; using System.Collections.Generic; using System.Threading; using System.Runtime.InteropServices; using ESRI.ArcGIS.Display; using ESRI.ArcGIS.Geometry; using ESRI.ArcGIS.Carto; using ESRI.ArcGIS.Geodatabase; using ESRI.ArcGIS.esriSystem; namespace MapCruncher { // Create a class that implements the EventArgs public sealed class MapCruncherEventArgs : EventArgs { // Set up the event properties private int m_lod; private int m_totalLods; private int m_threadId; // create the event constructor public MapCruncherEventArgs(int lod, int totalLods, int threadId) { m_lod = lod; m_totalLods = totalLods; m_threadId = threadId; } public int LOD { get { return m_lod; } } public int TotalLods { get { return m_totalLods; } } public int ThreadId { get { return m_threadId; } } } // create a delegate for the event notification messages. public delegate void CookerThreadEventHandeler(object sender, MapCruncherEventArgs e); [ComVisible(false)] public sealed class MapCruncherClass { public event CookerThreadEventHandeler CookerProgress; // set target DPI to 96 const double c_dDPI = 96.0; // target raster tile size const int c_nTileWidthInPixels = 256; const int c_nTileHeightInPixels = 256; // a flag to indicate execution private bool m_bExecuting = false; // list containing the level of detail which will be shared amongst the processing threads // in order to instruct each thread which LOD to process private List<int> m_listLod = null; // the number of cooker threads which will be determined by the number of physical CPUs private int m_nNumOfCookerThreads = 0; // kill event in order to instruct the cooker threads to bail out. private ManualResetEvent m_killEvent = new ManualResetEvent(false); // the thread data which is shared to all processing threads. // this class defines mostly properties and will be passed as // a parameter to each thread. private sealed class ThreadData { public ThreadData() { } private string sCacheName; private string sCachePath; private List<int> listLod = null; //the event used to signal the main thread that all thread are finished processing private ManualResetEvent eDoneEvent; private ManualResetEvent eKillEvent; private int nAliveThreadCounter; string mapDocumentFullName; public string MapDocumentFullName { get { return mapDocumentFullName; } set { mapDocumentFullName = value; } } public List<int> LODs { get { return listLod; } set { listLod = value; } } public ManualResetEvent DoneEvent { get { return eDoneEvent; } set { eDoneEvent = value; } } public ManualResetEvent KillEvent { get { return eKillEvent; } set { eKillEvent = value; } } public string CacheName { get { return sCacheName; } set { sCacheName = value; } } public string CachePath { get { return sCachePath; } set { sCachePath = value; } } public int AliveThreadCounter { get { return nAliveThreadCounter; } set { nAliveThreadCounter = value; } } public int DecrementThreadCounter() { Interlocked.Decrement(ref nAliveThreadCounter); return nAliveThreadCounter; } } ThreadData m_threaData = null; public MapCruncherClass() { m_nNumOfCookerThreads = System.Environment.ProcessorCount; } /// <summary> /// The initialization method of the class. /// </summary> /// <param name="mapDocumentFullName"></param> /// <param name="targetCacheScale"></param> /// <param name="outputFolderPath"></param> /// <param name="outputCacheName"></param> /// <returns>true upon success</returns> public bool Init(string mapDocumentFullName, double targetCacheScale, string outputFolderPath, string outputCacheName) { if (m_bExecuting) return false; // get the active map from the map document in order to get the extent and the // spatial reference needed to generate the cache IMap map = GetMapFromDocument(mapDocumentFullName); if (map == null) return false; IActiveView activeView = map as IActiveView; // get the extent from the active view IEnvelope extent = activeView.ScreenDisplay.DisplayTransformation.FittedBounds; // calculate the cache extent given the input extent IEnvelope cacheExtent; if (!GetCacheBounds(map, extent, out cacheExtent) || cacheExtent == null) return false; // calculate the number of LODs to generate given the target scale of the cache int nNumOfLODs = CreateCacheStructure(System.IO.Path.Combine(outputFolderPath, outputCacheName), map, extent, targetCacheScale); //populate the m_listLod = new List<int>(nNumOfLODs); for (int i = 0; i < nNumOfLODs; ++i) m_listLod.Add(i); //create the thread data which will be shared among the cooker threads and set the relevant properties m_threaData = new ThreadData(); m_threaData.CachePath = outputFolderPath; m_threaData.CacheName = outputCacheName; m_threaData.DoneEvent = new ManualResetEvent(true); m_threaData.KillEvent = new ManualResetEvent(false); m_threaData.AliveThreadCounter = m_nNumOfCookerThreads; m_threaData.LODs = m_listLod; m_threaData.MapDocumentFullName = mapDocumentFullName; m_threaData.KillEvent.Reset(); m_threaData.DoneEvent.Reset(); return true; } /// <summary> /// starts the execution of the cache generation /// </summary> /// <returns>true upon success</returns> public bool Execute() { if (m_bExecuting) return false; m_bExecuting = true; System.Diagnostics.Trace.WriteLine("Main thread " + System.Threading.Thread.CurrentThread.ManagedThreadId + " starts execution..."); //create the subset threads array Thread[] threadTask = new Thread[m_nNumOfCookerThreads]; for (int i = 0; i < m_nNumOfCookerThreads; i++) { // instantiate the cooker threads threadTask[i] = new Thread(new ParameterizedThreadStart(CookerProc)); // Note the STA apartment which is required to run ArcObjects!!! threadTask[i].SetApartmentState(ApartmentState.STA); threadTask[i].IsBackground = true; // make sure to set the thread priority to below normal threadTask[i].Priority = ThreadPriority.BelowNormal; threadTask[i].Name = "Cooker " + (i + 1).ToString(); // spin the cooker threads and pass in the shared thread data threadTask[i].Start((object)m_threaData); } // wait on the done event for all threads are done processing System.Diagnostics.Trace.WriteLine("Main thread " + System.Threading.Thread.CurrentThread.ManagedThreadId + " is waiting..."); m_threaData.DoneEvent.WaitOne(); System.Diagnostics.Trace.WriteLine("Main thread " + System.Threading.Thread.CurrentThread.ManagedThreadId + ", Execution is done!!!"); for (int i = 0; i < m_nNumOfCookerThreads; i++) { threadTask[i].Join(); } threadTask = null; // at the end of the execution, set the flag back to false m_bExecuting = false; return true; } #region private methods /// <summary> /// Given the input extent, calculate the cache extent /// </summary> /// <param name="map"></param> /// <param name="inExtent"></param> /// <param name="cacheExtent"></param> /// <returns>true on success</returns> bool GetCacheBounds(IMap map, IEnvelope inExtent, out IEnvelope cacheExtent) { cacheExtent = null; if (map == null) return false; IActiveView activeView = map as IActiveView; ISpatialReference mapSR = map.SpatialReference; if (inExtent == null || inExtent.IsEmpty) inExtent = activeView.FullExtent; WKSEnvelope wksExtent; inExtent.QueryWKSCoords(out wksExtent); // in case that the input extent has a different spatial reference // from the map, make sure to project it. IEnvelope calculatedCacheExtent = new EnvelopeClass(); calculatedCacheExtent.PutWKSCoords(ref wksExtent); calculatedCacheExtent.SpatialReference = inExtent.SpatialReference; calculatedCacheExtent.Project(mapSR); // get the SR horizon envelope WKSEnvelope horizonEnv; GetSRHorizon(map, mapSR, out horizonEnv); IEnvelope horizonEnvelope = new EnvelopeClass(); horizonEnvelope.SpatialReference = mapSR; horizonEnvelope.PutWKSCoords(ref horizonEnv); // intersect the input extent with the SR horizon calculatedCacheExtent.Intersect(horizonEnvelope); // make sure that the result envelope isn't empty; if (calculatedCacheExtent.IsEmpty) return false; // get the cache center point IPoint cacheCenterPt = ((IArea)calculatedCacheExtent).Centroid; double dCacheXMin, dCacheYMin, dCacheXMax, dCacheYMax; calculatedCacheExtent.QueryCoords(out dCacheXMin, out dCacheYMin, out dCacheXMax, out dCacheYMax); // bound the cache extent with a square that will form the first LOD double dBoundingSquareEdge = System.Math.Max(dCacheXMax - dCacheXMin, dCacheYMax - dCacheYMin); WKSEnvelope boundingSquareEnv; boundingSquareEnv.XMin = 0; boundingSquareEnv.YMin = 0; boundingSquareEnv.XMax = dBoundingSquareEdge; boundingSquareEnv.YMax = dBoundingSquareEdge; IEnvelope cacheBounds = new EnvelopeClass(); cacheBounds.SpatialReference = mapSR; cacheBounds.PutWKSCoords(ref boundingSquareEnv); cacheBounds.CenterAt(cacheCenterPt); // return the bounding square cacheExtent = cacheBounds; return true; } /// <summary> /// get the square defining the horizon of the spatial reference /// </summary> /// <param name="map"></param> /// <param name="SR"></param> /// <param name="horizonEnv"></param> /// <returns></returns> bool GetSRHorizon(IMap map, ISpatialReference SR, out WKSEnvelope horizonEnv) { horizonEnv = new WKSEnvelope(); if (SR == null) return false; IProjectedCoordinateSystem4 PCS4 = SR as IProjectedCoordinateSystem4; IGeographicCoordinateSystem2 GCS2 = SR as IGeographicCoordinateSystem2; IUnknownCoordinateSystem unknownCoordinateSystem = SR as IUnknownCoordinateSystem; if (PCS4 != null) { bool bInclusive = true; IGeometry horizonGeometry = PCS4.GetPCSHorizon(out horizonEnv, out bInclusive); } else if (GCS2 != null) //handle unknown spatial reference { GCS2.GetHorizon(ref horizonEnv); } else if (unknownCoordinateSystem != null) // in case that the input SR is unknown { // use the map's extent IActiveView activeView = map as IActiveView; IEnvelope extent = activeView.ScreenDisplay.DisplayTransformation.Bounds; extent.QueryWKSCoords(out horizonEnv); } return true; } /// <summary> /// validate and create the folder containing the cache. Also write to the /// cache the cache info (CacheInfo class) as well as the cache extent. This information defines the /// cached raster dataset and will also be used by the cooker threads. /// </summary> /// <param name="bstrCacheFullName"></param> /// <param name="map"></param> /// <param name="cacheBounds"></param> /// <param name="targetCacheScale"></param> /// <returns></returns> int CreateCacheStructure(string bstrCacheFullName, IMap map, IEnvelope cacheBounds, double targetCacheScale) { if (cacheBounds == null || cacheBounds.IsEmpty) return -1; // given the target map scale, calculate the required resolution. double dTargetResolution = CalculateResolution(map, targetCacheScale); if (dTargetResolution <= 0) return -1; double dBoundingSquareEdge = cacheBounds.Width; // according to the cache tile size, calculate the required number of level of details List<double> listResolutions = new List<double>(); double dResolution = dBoundingSquareEdge / (double)c_nTileWidthInPixels; while (dResolution > dTargetResolution) { listResolutions.Add(dResolution); dResolution *= 0.5; } // write the cache folder and the objects defining it. ICacheInfo cacheInfo = CreateCacheInfo(listResolutions, cacheBounds, map.SpatialReference, bstrCacheFullName); if (cacheInfo == null) return -1; return listResolutions.Count; } /// <summary> /// Given the target map scale, calculate the equivalent resolution (number map units per on pixel). /// </summary> /// <param name="map"></param> /// <param name="targetCacheScale"></param> /// <returns></returns> double CalculateResolution(IMap map, double targetCacheScale) { if (targetCacheScale <= 0.0) return -1; if (map == null) return -1; IDisplayTransformation dt = ((IActiveView)map).ScreenDisplay.DisplayTransformation; ISpatialReference sr = dt.SpatialReference; esriUnits mapUnits = dt.Units; // map scale is defined as the ration between the size of an object comparing to // its size in the map. Display units are measured in dots per inches and therefore // we need to convert the given map units to inches as well. double dFittedBoundsWidth = dt.FittedBounds.Width; IUnitConverter unitConverter = new UnitConverterClass(); double dFittedBoundsWidthInches = unitConverter.ConvertUnits(dFittedBoundsWidth, mapUnits, esriUnits.esriInches); double dInchesFactor = dFittedBoundsWidthInches / dFittedBoundsWidth; return (targetCacheScale / (c_dDPI * dInchesFactor)); } /// <summary> /// create the cache info which defines the cache structure /// </summary> /// <param name="listResolutions"></param> /// <param name="cacheExtent"></param> /// <param name="mapSR"></param> /// <param name="sCacheFullName"></param> /// <returns></returns> ICacheInfo CreateCacheInfo(List<double> listResolutions, IEnvelope cacheExtent, ISpatialReference mapSR, string sCacheFullName) { if (cacheExtent == null || sCacheFullName.Length == 0) return null; // get the upper left corner of the cache (point of origin) IPoint origin = cacheExtent.UpperLeft; // calculate the LODs ILODInfos lODInfos = new LODInfosClass(); lODInfos.RemoveAll(); ILODInfo lODInfo; for (int lod = 0 ; lod < listResolutions.Count ; ++lod) { lODInfo = new LODInfoClass(); lODInfo.LevelID = lod; lODInfo.Resolution = listResolutions[lod]; lODInfos.Add(lODInfo); } // set the tiling schema ITileCacheInfo tileCacheInfo = new TileCacheInfoClass(); tileCacheInfo.TileCols = c_nTileWidthInPixels; tileCacheInfo.TileRows = c_nTileHeightInPixels; tileCacheInfo.Origin = origin; tileCacheInfo.SpatialReference = mapSR; tileCacheInfo.LODInfos = lODInfos; tileCacheInfo.ComputeScales(); // the output cache image format, 32 bit (RGBA) PNG. string sCacheFormat = "PNG32"; ITileImageInfo tileImageInfo = new TileImageInfoClass(); tileImageInfo.Format = sCacheFormat; // create a MapCooker in order to write all of the files to the new cache IMapCooker mapCooker = new MapCookerClass(); mapCooker.Format = sCacheFormat; mapCooker.Path = sCacheFullName; mapCooker.Extent = cacheExtent; mapCooker.TileCacheInfo = tileCacheInfo; ((IMapCooker2)mapCooker).TileImageInfo = tileImageInfo; // define the cache extent ICacheDatasetInfo cacheDatasetInfo = new CacheDatasetInfoClass(); cacheDatasetInfo.Extent = cacheExtent; // define the storage of the case ICacheStorageInfo cacheStorageInfo = new CacheStorageInfoClass(); cacheStorageInfo.PacketSize = 128; cacheStorageInfo.StorageFormat = esriMapCacheStorageFormat.esriMapCacheStorageModeCompact; // write the cache info and extent to the local cache dir ((IMapCooker3)mapCooker).WriteTilingSchemeEx(tileCacheInfo, tileImageInfo, cacheStorageInfo, cacheDatasetInfo, sCacheFullName, ""); ICacheInfo cacheInfo = new CacheInfoClass(); cacheInfo.TileCacheInfo = tileCacheInfo; cacheInfo.TileImageInfo = tileImageInfo; ((ICacheInfo2)cacheInfo).CacheStorageInfo = cacheStorageInfo; return cacheInfo; } /// <summary> /// re-hydrate a map cooker from the serialized objects defined in the cache /// </summary> /// <param name="map"></param> /// <param name="td"></param> /// <returns></returns> IMapCooker CreateMapCooker(ThreadData td) { // get the full path to the cache string sCacheFullName = System.IO.Path.Combine(td.CachePath, td.CacheName); IMapCooker mapCooker = null; // make sure to block the other threads while re-hydrating the map cooker in order // to avoid corrupting the cache. lock(td) { // re-hydrate the cache info first ICacheInfo cacheInfo = ReadCacheConfiguration(sCacheFullName); if (cacheInfo == null) return null; ITileCacheInfo tileCacheInfo = cacheInfo.TileCacheInfo; ITileImageInfo tileImageInfo = cacheInfo.TileImageInfo; ICacheInfo2 cacheInfo2 = cacheInfo as ICacheInfo2; if (cacheInfo2 == null) return null; ICacheStorageInfo cacheStorageInfo = cacheInfo2.CacheStorageInfo; // rehydrate the CacheDatasetInfo which defines the cache extent ICacheDatasetInfo cacheDatsetInfo = ReadCacheDatasetInfo(sCacheFullName); IEnvelope cacheExtent = cacheDatsetInfo.Extent; // create a MapCooker mapCooker = new MapCookerClass(); mapCooker.Path = sCacheFullName; mapCooker.TileCacheInfo = tileCacheInfo; mapCooker.Extent = cacheExtent; ((IMapCooker2)mapCooker).TileImageInfo = tileImageInfo; ((IMapCooker3)mapCooker).CacheStorageInfo = cacheStorageInfo; ((IMapCooker3)mapCooker).CacheDatasetInfo = cacheDatsetInfo; } return mapCooker; } /// <summary> /// generate the cache for the input LOD /// </summary> /// <param name="map"></param> /// <param name="mapCooker"></param> /// <param name="lod"></param> void GenerateLOD(IMap map, IMapCooker mapCooker, int lod) { System.Diagnostics.Trace.WriteLine("Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId + " is processing LOD: " + lod + "."); if (map == null || mapCooker == null || lod < 0) return; // get the screen display IScreenDisplay screenDisplay = ((IActiveView)map).ScreenDisplay; // populate a long array with the input LOD to generate ILongArray longArray = new LongArrayClass(); longArray.Add(lod); // use the map cooker to generate the required LOD mapCooker.Update(map, null, screenDisplay, mapCooker.Extent, longArray, esriMapCacheUpdateMode.esriMapCacheUpdateRecreateAll, null); System.Diagnostics.Trace.WriteLine("Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId + " finished processing LOD: " + lod + "."); } /// <summary> /// re-hydrate an ArcObject serialized to an xml file. /// </summary> /// <param name="cacheFullName"></param> /// <param name="objectFileName"></param> /// <returns></returns> object ReadObjectFromFile(string cacheFullName, string objectFileName) { // create the stream IXMLStream xmlStream = new XMLStreamClass(); // get the file name and load it to the xml stream string sConfigFileName = System.IO.Path.Combine(cacheFullName, objectFileName); xmlStream.LoadFromFile(sConfigFileName); xmlStream.Reset(); // read the object from the xml stream IXMLReader xmlReader = new XMLReaderClass(); xmlReader.ReadFrom(xmlStream as IStream); IXMLSerializer xmlSerializer = new XMLSerializerClass(); object unkObj = xmlSerializer.ReadObject(xmlReader, null, null); return unkObj; } /// <summary> /// re-hydrate the serialized cache info from the cache /// </summary> /// <param name="cacheFullName"></param> /// <returns></returns> ICacheInfo ReadCacheConfiguration(string cacheFullName) { //Read the configuration XML to figure out the tiling scheme. return ReadObjectFromFile(cacheFullName, "conf.xml") as ICacheInfo; } /// <summary> /// re-hydrate the serialized cache extent from the cache /// </summary> /// <param name="cacheFullName"></param> /// <returns></returns> ICacheDatasetInfo ReadCacheDatasetInfo(string cacheFullName) { object unkObj = ReadObjectFromFile(cacheFullName, "conf.cdi"); IEnvelope envelope = unkObj as IEnvelope; ICacheDatasetInfo cacheDatasetInfo = new CacheDatasetInfoClass(); cacheDatasetInfo.Extent = envelope; return cacheDatasetInfo; } /// <summary> /// get the active map given a map document path. /// </summary> /// <param name="mapDocFullPath"></param> /// <returns></returns> IMap GetMapFromDocument( string mapDocFullPath) { IMapDocument mapDocument = new MapDocumentClass(); mapDocument.Open(mapDocFullPath, null); IMap map = mapDocument.ActiveView as IMap; if (map == null) map = mapDocument.get_Map(0); mapDocument.Close(); return map; } // the thread procedure private void CookerProc(object threadParam) { ThreadData td = (ThreadData)threadParam; if (td == null) return; IMap map = GetMapFromDocument(td.MapDocumentFullName); IMapCooker mapCooker = CreateMapCooker(td); while (true) { // check the kill sequence first if (td.KillEvent.WaitOne(0)) break; int nLOD = -1; lock (td.LODs) { if (td.LODs.Count == 0) { System.Diagnostics.Trace.WriteLine("Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId + " is exiting."); break; } nLOD = td.LODs[0]; td.LODs.Remove(nLOD); } GenerateLOD(map, mapCooker, nLOD); if (CookerProgress != null) { MapCruncherEventArgs e = new MapCruncherEventArgs(nLOD, td.LODs.Count, System.Threading.Thread.CurrentThread.ManagedThreadId); CookerProgress(this, e); } } // the thread is exiting int nAliveThreadCount = td.DecrementThreadCounter(); if (nAliveThreadCount <= 0) { System.Diagnostics.Trace.WriteLine("Thread " + System.Threading.Thread.CurrentThread.ManagedThreadId + " signaling done event..."); td.DoneEvent.Set(); } } #endregion } }