#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 Disclaimer / License
using OSGeo.MapGuide.ObjectModels;
using OSGeo.MapGuide.ObjectModels.Common;
using OSGeo.MapGuide.ObjectModels.MapDefinition;
using OSGeo.MapGuide.ObjectModels.TileSetDefinition;
using System;
using System.Collections.Generic;
namespace OSGeo.MapGuide.MaestroAPI.Tile
{
public class TileProgressEventArgs : EventArgs
{
///
///
///
/// The state that invoked the callback
/// 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
public TileProgressEventArgs(CallbackStates state, MapTilingConfiguration map, string group, int scaleindex, int row, int column, bool cancel)
{
this.State = state;
this.Map = map;
this.Group = group;
this.ScaleIndex = scaleindex;
this.Row = row;
this.Column = column;
this.Cancel = cancel;
}
///
/// The state that invoked the callback
///
public CallbackStates State { get; }
///
/// The map currently being processed
///
public MapTilingConfiguration Map { get; }
///
/// The group being processed
///
public string Group { get; }
///
/// The scaleindex being processed
///
public int ScaleIndex { get; }
///
/// The row being processed
///
public int Row { get; }
///
/// The column being processed
///
public int Column { get; }
///
/// A control flag to stop the tile rendering
///
public bool Cancel { get; set; }
}
public class TileRenderingErrorEventArgs : EventArgs
{
///
///
///
/// The state that invoked the callback
/// The map currently being processed
/// The group being processed
/// The scaleindex being processed
/// The row being processed
/// The column being processed
///
public TileRenderingErrorEventArgs(CallbackStates state, MapTilingConfiguration map, string group, int scaleindex, int row, int column, Exception error)
{
this.State = state;
this.Map = map;
this.Group = group;
this.ScaleIndex = scaleindex;
this.Row = row;
this.Column = column;
this.Error = error;
}
///
/// The state that invoked the callback
///
public CallbackStates State { get; }
///
/// The map currently being processed
///
public MapTilingConfiguration Map { get; }
///
/// The group being processed
///
public string Group { get; }
///
/// The scaleindex being processed
///
public int ScaleIndex { get; }
///
/// The row being processed
///
public int Row { get; }
///
/// The column being processed
///
public int Column { get; }
///
///
///
public Exception Error { get; set; }
}
///
/// This delegate is used to monitor progress on tile rendering
///
///
///
public delegate void TileProgressEventHandler(object sender, TileProgressEventArgs args);
///
/// This delegate is used to monitor progress on tile rendering
///
///
///
public delegate void TileErrorEventHandler(object sender, TileRenderingErrorEventArgs args);
///
/// 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 : IDisposable
{
///
/// 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 TileProgressEventHandler BeginRenderingMaps;
///
/// A map is being rendered
///
public event TileProgressEventHandler BeginRenderingMap;
///
/// A group is being rendered
///
public event TileProgressEventHandler BeginRenderingGroup;
///
/// A scale is being rendered
///
public event TileProgressEventHandler BeginRenderingScale;
///
/// A tile is being rendered
///
public event TileProgressEventHandler BeginRenderingTile;
///
/// All maps have been rendered
///
public event TileProgressEventHandler FinishRenderingMaps;
///
/// A map has been rendered
///
public event TileProgressEventHandler FinishRenderingMap;
///
/// A group has been rendered
///
public event TileProgressEventHandler FinishRenderingGroup;
///
/// A scale has been rendered
///
public event TileProgressEventHandler FinishRenderingScale;
///
/// A tile has been rendered
///
public event TileProgressEventHandler FinishRenderingTile;
///
/// A tile has failed to render
///
public event TileErrorEventHandler FailedRenderingTile;
internal void InvokeBeginRendering(MapTilingConfiguration batchMap)
{
var args = new TileProgressEventArgs(CallbackStates.StartRenderMap, batchMap, null, -1, -1, -1, m_cancel);
this.BeginRenderingMap?.Invoke(this, args);
m_cancel = args.Cancel;
PauseEvent.WaitOne();
}
internal void InvokeFinishRendering(MapTilingConfiguration batchMap)
{
var args = new TileProgressEventArgs(CallbackStates.FinishRenderMap, batchMap, null, -1, -1, -1, m_cancel);
this.FinishRenderingMap?.Invoke(this, args);
m_cancel = args.Cancel;
}
internal void InvokeBeginRendering(MapTilingConfiguration batchMap, string group)
{
var args = new TileProgressEventArgs(CallbackStates.StartRenderGroup, batchMap, group, -1, -1, -1, m_cancel);
this.BeginRenderingGroup?.Invoke(this, args);
m_cancel = args.Cancel;
PauseEvent.WaitOne();
}
internal void InvokeFinishRendering(MapTilingConfiguration batchMap, string group)
{
var args = new TileProgressEventArgs(CallbackStates.FinishRenderGroup, batchMap, group, -1, -1, -1, m_cancel);
this.FinishRenderingGroup?.Invoke(this, args);
m_cancel = args.Cancel;
}
internal void InvokeBeginRendering(MapTilingConfiguration batchMap, string group, int scaleindex)
{
var args = new TileProgressEventArgs(CallbackStates.StartRenderScale, batchMap, group, scaleindex, -1, -1, m_cancel);
this.BeginRenderingScale?.Invoke(this, args);
m_cancel = args.Cancel;
PauseEvent.WaitOne();
}
internal void InvokeFinishRendering(MapTilingConfiguration batchMap, string group, int scaleindex)
{
var args = new TileProgressEventArgs(CallbackStates.FinishRenderScale, batchMap, group, scaleindex, -1, -1, m_cancel);
this.FinishRenderingScale?.Invoke(this, args);
m_cancel = args.Cancel;
}
internal void InvokeBeginRendering(MapTilingConfiguration batchMap, string group, int scaleindex, int row, int col)
{
var args = new TileProgressEventArgs(CallbackStates.StartRenderTile, batchMap, group, scaleindex, row, col, m_cancel);
this.BeginRenderingTile?.Invoke(this, args);
m_cancel = args.Cancel;
PauseEvent.WaitOne();
}
internal void InvokeFinishRendering(MapTilingConfiguration batchMap, string group, int scaleindex, int row, int col)
{
var args = new TileProgressEventArgs(CallbackStates.FinishRenderTile, batchMap, group, scaleindex, row, col, m_cancel);
this.FinishRenderingTile?.Invoke(this, args);
m_cancel = args.Cancel;
}
internal Exception InvokeError(MapTilingConfiguration batchMap, string group, int scaleindex, int row, int col, Exception exception)
{
var args = new TileRenderingErrorEventArgs(CallbackStates.FailedRenderingTile, batchMap, group, scaleindex, row, col, exception);
this.FailedRenderingTile?.Invoke(this, args);
return args.Error;
}
#endregion Events
///
/// 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);
}
~TilingRunCollection()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposing)
{
this.PauseEvent?.Dispose();
this.PauseEvent = null;
}
}
///
/// 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) => m_maps.ForEach(bm => bm.SetScales(scaleindexes));
///
/// Sets the list of groups
///
///
public void SetGroups(string[] groups) => m_maps.ForEach(bm => bm.SetGroups(groups));
///
/// Limits the number of rows
///
///
public void LimitRows(long limit) => m_maps.ForEach(bm => bm.LimitRows(limit));
///
/// Limits the number of columns
///
///
public void LimitCols(long limit) => m_maps.ForEach(bm => bm.LimitCols(limit));
private static void TriggerEvent(TileProgressEventHandler evt, object sender, CallbackStates state, MapTilingConfiguration map, string group, int scaleindex, int row, int column, ref bool cancel)
{
var args = new TileProgressEventArgs(state, map, group, scaleindex, row, column, cancel);
evt?.Invoke(sender, args);
cancel = args.Cancel;
}
///
/// Renders all tiles in all maps
///
public void RenderAll()
{
m_cancel = false;
TriggerEvent(this.BeginRenderingMaps, this, CallbackStates.StartRenderAllMaps, null, null, -1, -1, -1, ref m_cancel);
foreach (MapTilingConfiguration bm in this.Maps)
{
if (m_cancel)
break;
else
bm.Render();
}
TriggerEvent(this.FinishRenderingMaps, this, CallbackStates.FinishRenderAllMaps, null, null, -1, -1, -1, ref m_cancel);
}
///
/// The connection to the server
///
public IServerConnection Connection => m_connection;
///
/// The list of map configurations to proccess
///
public List Maps => m_maps;
///
/// The tile settings
///
public TileRunParameters Config => m_tileSettings;
///
/// Gets a flag indicating if the rendering process is cancelled
///
public bool Cancel => 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 readonly TilingRunCollection m_parent;
///
/// The resource ID of the tile set or map definition
///
private string m_tileSetResourceID;
///
/// The tile set extents
///
private IEnvelope m_tileSetExtents;
///
/// The map read from MapGuide
///
private ITileSetAbstract m_tileset;
///
/// 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 => m_groups;
///
/// The map's scales may have been modified, this array is a map of the new values
///
public int[] ScaleIndexMap => 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;
IResource res = parent.Connection.ResourceService.GetResource(map);
IMapDefinition mdf = res as IMapDefinition;
ITileSetDefinition tsd = res as ITileSetDefinition;
if (mdf != null)
{
m_tileSetResourceID = mdf.ResourceID;
m_tileSetExtents = mdf.Extents.Clone();
m_tileset = mdf.BaseMap;
}
else if (tsd != null && tsd.SupportsCustomFiniteDisplayScales)
{
m_tileSetResourceID = tsd.ResourceID;
m_tileSetExtents = tsd.Extents.Clone();
m_tileset = tsd;
}
if (m_tileset == null)
throw new InvalidOperationException(OSGeo.MapGuide.MaestroAPI.Strings.UnseedableTileSet);
var baseMap = m_tileset;
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.TileSet.ScaleCount];
for (int i = 0; i < tmp.Length; i++)
tmp[i] = i;
SetScales(tmp);
}
internal void CalculateDimensionsInternal()
{
if (m_tileset.ScaleCount == 0)
{
m_scaleindexmap = new int[0];
m_dimensions = new long[0][];
return;
}
IEnvelope extents = this.MaxExtent ?? m_tileSetExtents;
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_tileset.GetScaleAt(i);
// This is the official method, and the only one MgCooker will ever use
// 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_tileSetExtents.MinX;
double offsetY = m_tileSetExtents.MaxY - MaxExtent.MaxY;
rowTileOffset = (int)Math.Floor(offsetY / tileHeight);
colTileOffset = (int)Math.Floor(offsetX / tileWidth);
double offsetMaxX = MaxExtent.MaxX - m_tileSetExtents.MinX;
double offsetMinY = m_tileSetExtents.MaxY - MaxExtent.MinY;
int rowMinTileOffset = (int)Math.Floor(offsetMinY / tileHeight);
int colMaxTileOffset = (int)Math.Floor(offsetMaxX / tileWidth);
//GT 03/08/2014 - the right number of columns/rows it's the the end tile (maxtileoffset, ex 12) - the start tile (coltileoffset, ex 11) +1
//i.e. 12-11+1=2 so 2 columns
cols = (colMaxTileOffset - colTileOffset) + 1;
rows = (rowMinTileOffset - rowTileOffset) + 1;
}
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_tileset.ScaleCount - 1; i >= 0; i--)
if (!keys.Contains(i))
m_tileset.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_tileset == null || m_tileset.ScaleCount == 0)
return 0;
else
return m_tileset.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
using (var settings = new RenderThreads(this, m_parent, m_scaleindexmap[scaleindex], group, m_tileSetResourceID, 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 => m_tileSetResourceID;
///
/// Gets the MapDefintion
///
public ITileSetAbstract TileSet => m_tileset;
///
/// Gets a reference to the parent tiling run collection
///
public TilingRunCollection Parent => m_parent;
}
///
/// Defines global parameters for a tiling run
///
public class TileRunParameters
{
public const string MAPAGENT = "mapagent"; //NOXLATE
public const string USERNAME = "username"; //NOXLATE
public const string PASSWORD = "password"; //NOXLATE
public const string NATIVECONNECTION = "native-connection"; //NOXLATE
public const string MAPDEFINITIONS = "mapdefinitions"; //NOXLATE
public const string LIMITROWS = "limitrows"; //NOXLATE
public const string LIMITCOLS = "limitcols"; //NOXLATE
public const string EXTENTOVERRIDE = "extentoverride"; //NOXLATE
public const string METERSPERUNIT = "metersperunit"; //NOXLATE
public const string BASEGROUPS = "basegroups"; //NOXLATE
public const string SCALEINDEX = "scaleindex"; //NOXLATE
public const string PROVIDER = "provider"; //NOXLATE
public const string CONNECTIONPARAMS = "connection-params"; //NOXLATE
public const string TILEWIDTH = "tilewidth"; //NOXLATE
public const string TILEHEIGHT = "tileheight"; //NOXLATE
public const string DOTSPERINCH = "DPI"; //NOXLATE
public const string RANDOMTILEORDER = "random-tile-order"; //NOXLATE
public const string THREADCOUNT = "threadcount"; //NOXLATE
///
/// 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;
///
/// 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);
}
}