#region Disclaimer / License // Copyright (C) 2009, Kenneth Skovhede // http://www.hexad.dk, opensource@hexad.dk // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // #endregion using System; using System.Collections.Generic; using System.Text; using OSGeo.MapGuide.MaestroAPI; using OSGeo.MapGuide.ObjectModels.MapDefinition; using OSGeo.MapGuide.ObjectModels.Common; namespace OSGeo.MapGuide.MaestroAPI.Tile { /// /// This delegate is used to monitor progress on tile rendering /// /// The map currently being processed /// The group being processed /// The scaleindex being processed /// The row being processed /// The column being processed /// A control flag to stop the tile rendering /// The state that invoked the callback public delegate void ProgressCallback(CallbackStates state, MapTilingConfiguration map, string group, int scaleindex, int row, int column, ref bool cancel); /// /// This delegate is used to monitor progress on tile rendering /// /// The map currently being processed /// The group being processed /// The scaleindex being processed /// The row being processed /// The column being processed /// The state that invoked the callback /// The exception from the last attempt, set this to null to ignore the exception public delegate void ErrorCallback(CallbackStates state, MapTilingConfiguration map, string group, int scaleindex, int row, int column, ref Exception exception); /// /// These are the avalible states for callbacks /// public enum CallbackStates { /// /// All maps are being rendered /// StartRenderAllMaps, /// /// A map is being rendered /// StartRenderMap, /// /// A group is being rendered /// StartRenderGroup, /// /// A scale is being rendered /// StartRenderScale, /// /// A tile is being rendered /// StartRenderTile, /// /// A tile has been rendered /// FinishRenderTile, /// /// A scale has been rendered /// FinishRenderScale, /// /// A group has been rendered /// FinishRenderGroup, /// /// A map has been rendered /// FinishRenderMap, /// /// All maps have been rendered /// FinishRenderAllMaps, /// /// A tile has failed to render /// FailedRenderingTile, } /// /// Class to hold settings for a batch run of tile building /// public class TilingRunCollection { /// /// A reference to the connection /// private IServerConnection m_connection; /// /// The list of maps /// private List m_maps; /// /// A default set of tile settings /// private TileRunParameters m_tileSettings = new TileRunParameters(); /// /// A flag that indicates the rendering should stop /// private bool m_cancel; /// /// An event that can be used to pause MgCooker /// public System.Threading.ManualResetEvent PauseEvent = new System.Threading.ManualResetEvent(true); #region Events /// /// All maps are being rendered /// public event ProgressCallback BeginRenderingMaps; /// /// A map is being rendered /// public event ProgressCallback BeginRenderingMap; /// /// A group is being rendered /// public event ProgressCallback BeginRenderingGroup; /// /// A scale is being rendered /// public event ProgressCallback BeginRenderingScale; /// /// A tile is being rendered /// public event ProgressCallback BeginRenderingTile; /// /// All maps have been rendered /// public event ProgressCallback FinishRenderingMaps; /// /// A map has been rendered /// public event ProgressCallback FinishRenderingMap; /// /// A group has been rendered /// public event ProgressCallback FinishRenderingGroup; /// /// A scale has been rendered /// public event ProgressCallback FinishRenderingScale; /// /// A tile has been rendered /// public event ProgressCallback FinishRenderingTile; /// /// A tile has failed to render /// public event ErrorCallback FailedRenderingTile; internal void InvokeBeginRendering(MapTilingConfiguration batchMap) { if (this.BeginRenderingMap != null) this.BeginRenderingMap(CallbackStates.StartRenderMap, batchMap, null, -1, -1, -1, ref m_cancel); PauseEvent.WaitOne(); } internal void InvokeFinishRendering(MapTilingConfiguration batchMap) { if (this.FinishRenderingMap != null) this.FinishRenderingMap(CallbackStates.FinishRenderMap, batchMap, null, -1, -1, -1, ref m_cancel); } internal void InvokeBeginRendering(MapTilingConfiguration batchMap, string group) { if (this.BeginRenderingGroup != null) this.BeginRenderingGroup(CallbackStates.StartRenderGroup, batchMap, group, -1, -1, -1, ref m_cancel); PauseEvent.WaitOne(); } internal void InvokeFinishRendering(MapTilingConfiguration batchMap, string group) { if (this.FinishRenderingGroup != null) this.FinishRenderingGroup(CallbackStates.FinishRenderGroup, batchMap, group, -1, -1, -1, ref m_cancel); } internal void InvokeBeginRendering(MapTilingConfiguration batchMap, string group, int scaleindex) { if (this.BeginRenderingScale != null) this.BeginRenderingScale(CallbackStates.StartRenderScale, batchMap, group, scaleindex, -1, -1, ref m_cancel); PauseEvent.WaitOne(); } internal void InvokeFinishRendering(MapTilingConfiguration batchMap, string group, int scaleindex) { if (this.FinishRenderingScale != null) this.FinishRenderingScale(CallbackStates.FinishRenderScale, batchMap, group, scaleindex, -1, -1, ref m_cancel); } internal void InvokeBeginRendering(MapTilingConfiguration batchMap, string group, int scaleindex, int row, int col) { if (this.BeginRenderingTile != null) this.BeginRenderingTile(CallbackStates.StartRenderTile, batchMap, group, scaleindex, row, col, ref m_cancel); PauseEvent.WaitOne(); } internal void InvokeFinishRendering(MapTilingConfiguration batchMap, string group, int scaleindex, int row, int col) { if (this.FinishRenderingTile != null) this.FinishRenderingTile(CallbackStates.FinishRenderTile, batchMap, group, scaleindex, row, col, ref m_cancel); } internal Exception InvokeError(MapTilingConfiguration batchMap, string group, int scaleindex, int row, int col, ref Exception exception) { if (this.FailedRenderingTile != null) this.FailedRenderingTile(CallbackStates.FailedRenderingTile, batchMap, group, scaleindex, row, col, ref exception); return exception; } #endregion /// /// Constructs a new batch setup. If no maps are supplied, all maps in the repository is assumed. /// /// The url to the mapagent.fcgi /// The username to connect with /// The password to connect with /// A list of maps to process, leave empty to process all layers public TilingRunCollection(string mapagent, string username, string password, params string[] maps) : this(ConnectionProviderRegistry.CreateConnection("Maestro.Http", "Url", mapagent, "Username", username, "Password", password, "AllowUntestedVersions", "true"), maps) //NOXLATE { } /// /// Constructs a new batch setup /// /// public TilingRunCollection(IServerConnection connection) { m_connection = connection; m_maps = new List(); } /// /// Constructs a new batch setup /// /// /// public TilingRunCollection(IServerConnection connection, params string[] maps) { m_connection = connection; m_maps = new List(); AddMapDefinitions(maps); } /// /// Adds the specified map definition ids /// /// public void AddMapDefinitions(string[] maps) { if (maps == null || maps.Length == 0 || (maps.Length == 1 && maps[0].Trim().Length == 0)) { List tmp = new List(); foreach (var doc in m_connection.ResourceService.GetRepositoryResources(StringConstants.RootIdentifier, ResourceTypes.MapDefinition.ToString()).Children) tmp.Add(doc.ResourceId); maps = tmp.ToArray(); } foreach (string s in maps) { MapTilingConfiguration bm = new MapTilingConfiguration(this, s); if (bm.Resolutions > 0) m_maps.Add(bm); } } /// /// Sets the list of scale indexes /// /// public void SetScales(int[] scaleindexes) { foreach (MapTilingConfiguration bm in m_maps) bm.SetScales(scaleindexes); } /// /// Sets the list of groups /// /// public void SetGroups(string[] groups) { foreach (MapTilingConfiguration bm in m_maps) bm.SetGroups(groups); } /// /// Limits the number of rows /// /// public void LimitRows(long limit) { foreach (MapTilingConfiguration bm in m_maps) bm.LimitRows(limit); } /// /// Limits the number of columns /// /// public void LimitCols(long limit) { foreach (MapTilingConfiguration bm in m_maps) bm.LimitCols(limit); } /// /// Renders all tiles in all maps /// public void RenderAll() { m_cancel = false; if (this.BeginRenderingMaps != null) this.BeginRenderingMaps(CallbackStates.StartRenderAllMaps, null, null, -1, -1, -1, ref m_cancel); foreach (MapTilingConfiguration bm in this.Maps) if (m_cancel) break; else bm.Render(); if (this.FinishRenderingMaps != null) this.FinishRenderingMaps(CallbackStates.FinishRenderAllMaps, null, null, -1, -1, -1, ref m_cancel); } /// /// The connection to the server /// public IServerConnection Connection { get { return m_connection; } } /// /// The list of map configurations to proccess /// public List Maps { get { return m_maps; } } /// /// The tile settings /// public TileRunParameters Config { get { return m_tileSettings; } } /// /// Gets a flag indicating if the rendering process is cancelled /// public bool Cancel { get { return m_cancel; } } } /// /// Class that represents a single map to build tiles for /// public class MapTilingConfiguration { /// /// A reference to the parent, and thus the connection /// private TilingRunCollection m_parent; /// /// The map read from MapGuide /// private IMapDefinition m_mapdefinition; /// /// The max extent of the map /// private IEnvelope m_maxExtent; /// /// The list of baselayer group names /// private string[] m_groups; /// /// For each entry there is two longs, row and column /// private long[][] m_dimensions; /// /// The max scale for the map /// private double m_maxscale; /// /// Conversion from supplied scaleindex to actual scaleindex /// private int[] m_scaleindexmap; /// /// The number of meters in an inch /// private const double INCH_TO_METER = 0.0254; /// /// Gets the list of groups /// public string[] Groups { get { return m_groups; } } /// /// The map's scales may have been modified, this array is a map of the new values /// public int[] ScaleIndexMap { get { return m_scaleindexmap; } } /// /// Constructs a new map to be processed /// /// The parent entry /// The resource id for the mapdefinition public MapTilingConfiguration(TilingRunCollection parent, string map) { m_parent = parent; m_mapdefinition = (IMapDefinition)parent.Connection.ResourceService.GetResource(map); var baseMap = m_mapdefinition.BaseMap; if (baseMap != null && baseMap.ScaleCount > 0) { m_groups = new string[baseMap.GroupCount]; for (int i = 0; i < baseMap.GroupCount; i++) m_groups[i] = baseMap.GetGroupAt(i).Name; m_maxscale = baseMap.GetMaxScale(); CalculateDimensions(); } } internal void CalculateDimensions() { int[] tmp = new int[this.MapDefinition.BaseMap.ScaleCount]; for (int i = 0; i < tmp.Length; i++) tmp[i] = i; SetScales(tmp); } internal void CalculateDimensionsInternal() { if (m_mapdefinition.BaseMap.ScaleCount == 0) { m_scaleindexmap = new int[0]; m_dimensions = new long[0][]; return; } IEnvelope extents = this.MaxExtent ?? m_mapdefinition.Extents; double maxscale = m_maxscale; m_dimensions = new long[this.Resolutions][]; m_scaleindexmap = new int[m_dimensions.Length]; double width_in_meters = Math.Abs(m_parent.Config.MetersPerUnit * (extents.MaxX - extents.MinX)); double height_in_meters = Math.Abs(m_parent.Config.MetersPerUnit * (extents.MaxY - extents.MinY)); m_dimensions = new long[this.Resolutions][]; for (int i = this.Resolutions - 1; i >= 0; i--) { long rows, cols, rowTileOffset = 0 , colTileOffset = 0; double scale = m_mapdefinition.BaseMap.GetScaleAt(i); if (m_parent.Config.UseOfficialMethod) { //This is the algorithm proposed by the MapGuide team: //http://www.nabble.com/Pre-Genererate--tiles-for-the-entire-map-at-all-pre-defined-zoom-scales-to6074037.html#a6078663 // //Method description inline (in case nabble link disappears): // // The upper left corner of the extents of the map corresponds to tile (0,0). Then tile (1,0) is to the right of that and tile (0,1) is under tile (0,0). // So assuming you know the extents of your map, you can calculate how many tiles it spans at the given scale, using the following // // number of tiles x = map width in meters / ( 0.079375 * map_scale) // number of tiles y = map height in meters / ( 0.079375 * map_scale) // // where 0.079375 = [inch to meter] / image DPI * tile size = 0.0254 / 96 * 300. // // This assumes you know the scale factor that converts your map width and height to meters. You can get this from the coordinate system of the map if you don't know it, but it's much easier to just plug in the number into this equation. // // Also have in mind that you can also request tiles beyond the map extent (for example tile (-1, -1), however, there is probably no point to cache them unless you have valid data outside your initial map extents. //The tile extent in meters double tileWidth =((INCH_TO_METER / m_parent.Config.DPI * m_parent.Config.TileWidth) * (scale)); double tileHeight = ((INCH_TO_METER / m_parent.Config.DPI * m_parent.Config.TileHeight) * (scale)); //Using this algorithm, yields a negative number of columns/rows, if the max scale is larger than the max extent of the map. rows = Math.Max(1, (int)Math.Ceiling((height_in_meters / tileHeight))); cols = Math.Max(1, (int)Math.Ceiling((width_in_meters / tileWidth))); if (m_maxExtent != null) { //The extent is overridden, so we need to adjust the start offsets double offsetX = MaxExtent.MinX - m_mapdefinition.Extents.MinX; double offsetY = m_mapdefinition.Extents.MaxY - MaxExtent.MaxY ; rowTileOffset = (int)Math.Floor(offsetY / tileHeight); colTileOffset = (int)Math.Floor(offsetX / tileWidth); double offsetMaxX = MaxExtent.MaxX - m_mapdefinition.Extents.MinX; double offsetMinY = m_mapdefinition.Extents.MaxY - MaxExtent.MinY; int rowMinTileOffset = (int)Math.Floor(offsetMinY / tileHeight); int colMaxTileOffset = (int)Math.Floor(offsetMaxX / tileWidth); cols += (colMaxTileOffset - colTileOffset); rows += (rowMinTileOffset - rowTileOffset); } } else { //This method assumes that the max scale is displayed on a screen with resolution 1920x1280. //This display width/height is then multiplied up to calculate the pixelwidth of all subsequent //scale ranges. Eg. if max scale range is 1:200, then scale range 1:100 is twice the size, //meaning the full map at 1:100 fills 3840x2560 pixels. //The width/height is then used to calculate the number of rows and columns of 300x300 pixel tiles. //The purpose of this method is to enabled tile generation without access to //coordinate system properties long pw = (long)(m_parent.Config.DisplayResolutionWidth * (1 / (scale / maxscale))); long ph = (long)(m_parent.Config.DisplayResolutionHeight * (1 / (scale / maxscale))); rows = (ph + (m_parent.Config.TileHeight - 1)) / m_parent.Config.TileHeight; cols = (pw + (m_parent.Config.TileWidth - 1)) / m_parent.Config.TileWidth; rows += rows % 2; cols += cols % 2; } m_dimensions[i] = new long[] { rows, cols, rowTileOffset , colTileOffset}; } } /// /// Sets the list of groups /// /// public void SetGroups(string[] groups) { List g = new List(); for(int i = 0; i < m_groups.Length; i++) if (Array.IndexOf(groups, m_groups[i]) >= 0) g.Add(m_groups[i]); m_groups = g.ToArray(); } /// /// Sets the list of scale indexes and sets the maximum extent to the given envelope /// /// /// public void SetScalesAndExtend(int[] scales, IEnvelope envelope) { this.m_maxExtent = envelope; SetScales(scales); } /// /// Sets the list of scale indexes /// /// public void SetScales(int[] scaleindexes) { //TODO: Re-read scales from mapdef? SortedList s = new SortedList(); foreach (int i in scaleindexes) if (!s.ContainsKey(i)) s.Add(i, i); List keys = new List(s.Keys); keys.Reverse(); for (int i = m_mapdefinition.BaseMap.ScaleCount - 1; i >= 0; i--) if (!keys.Contains(i)) m_mapdefinition.BaseMap.RemoveScaleAt(i); CalculateDimensionsInternal(); keys.Reverse(); //Preserve the original scales m_scaleindexmap = new int[keys.Count]; for (int i = 0; i < keys.Count; i++) m_scaleindexmap[i] = keys[i]; } internal void LimitCols(long limit) { foreach (long[] d in m_dimensions) d[1] = Math.Min(limit, d[1]); } internal void LimitRows(long limit) { foreach (long[] d in m_dimensions) d[0] = Math.Min(limit, d[0]); } /// /// Gets the total number of tiles to be rendered /// public long TotalTiles { get { long t = 0; foreach (long[] d in m_dimensions) t += d[0] * d[1]; return t; } } /// /// Gets the number of resolutions for the map /// public int Resolutions { get { if (m_mapdefinition.BaseMap == null || m_mapdefinition.BaseMap.ScaleCount == 0) return 0; else return m_mapdefinition.BaseMap.ScaleCount; } } /// /// Renders all tiles in a given scale /// /// The scale to render /// The name of the baselayer group public void RenderScale(int scaleindex, string group) { m_parent.InvokeBeginRendering(this, group, scaleindex); if (!m_parent.Cancel) { int rows = (int)m_dimensions[scaleindex][0]; int cols = (int)m_dimensions[scaleindex][1]; int rowTileOffset = (int)m_dimensions[scaleindex][2]; int colTileOffset = (int)m_dimensions[scaleindex][3]; //If the MaxExtents are different from the actual bounds, we need a start offset offset RenderThreads settings = new RenderThreads(this, m_parent, m_scaleindexmap[scaleindex], group, m_mapdefinition.ResourceID, rows, cols, rowTileOffset, colTileOffset, m_parent.Config.RandomizeTileSequence); settings.RunAndWait(); if (settings.TileSet.Count != 0 && !m_parent.Cancel) throw new Exception(Strings.TS_ThreadFailureError); } m_parent.InvokeFinishRendering(this, group, scaleindex); } /// /// Renders all tiles in all scales /// /// The name of the baselayer group public void RenderGroup(string group) { m_parent.InvokeBeginRendering(this, group); if (!m_parent.Cancel) { for (int i = this.Resolutions - 1; i >= 0; i--) if (m_parent.Cancel) break; else RenderScale(i, group); } m_parent.InvokeFinishRendering(this, group); } /// /// Renders all tiles in all groups in all scales /// public void Render() { m_parent.InvokeBeginRendering(this); if (!m_parent.Cancel) foreach (string s in m_groups) if (m_parent.Cancel) break; else RenderGroup(s); m_parent.InvokeFinishRendering(this); } /// /// Gets or sets the maximum extent used to calculate the tiles /// public IEnvelope MaxExtent { get { return m_maxExtent; } set { m_maxExtent = value; CalculateDimensions(); } } /// /// Gets the resourceId for the map /// public string ResourceId { get { return m_mapdefinition.ResourceID; } } /// /// Gets the MapDefintion /// public IMapDefinition MapDefinition { get { return m_mapdefinition; } } /// /// Gets a reference to the parent tiling run collection /// public TilingRunCollection Parent { get { return m_parent; } } } /// /// Defines global parameters for a tiling run /// public class TileRunParameters { /// /// The meters per unit /// public double MetersPerUnit = 1; /// /// The display DPI /// public double DPI = 96; /// /// The tile width /// public int TileWidth = 300; /// /// The tile height /// public int TileHeight = 300; /// /// The number of times to retry /// public int RetryCount = 5; /// /// The display resolution width /// public int DisplayResolutionWidth = 1920; /// /// The display resolution height /// public int DisplayResolutionHeight = 1280; /// /// Gets or sets whether to use the official method of tile generation. Requires an accurate meters per unit value to work /// public bool UseOfficialMethod = false; /// /// Gets or sets whether to randomize the tile generation sequence /// public bool RandomizeTileSequence = false; private int m_threadCount = 1; /// /// Gets or sets the thread count /// public int ThreadCount { get { return m_threadCount; } set { m_threadCount = Math.Max(1, value); } } /// /// The render method /// public RenderMethodDelegate RenderMethod; /// /// Defines a tile render method /// /// /// /// /// /// public delegate void RenderMethodDelegate(string map, string group, int col, int row, int scale); } }