예제 #1
0
        static void Main(string[] args)
        {
            string dbLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "tilecache.s3db");
            int minz = 7;
            int maxz = 10;
            double minx = -95.844727;
            double miny = 35.978006;
            double maxx = -88.989258;
            double maxy = 40.563895;
            string mapServiceUrl = "http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Demographics/ESRI_Census_USA/MapServer";
            int maxDegreeOfParallelism = 10;
            bool replaceExistingCacheDB = true;
            bool showHelp = false;
            bool verbose = false;
            string mapServiceType = "agsd";
            string settings = string.Empty;
            bool showInfo = false;
            ITileUrlSource tileSource = new OSMTileUrlSource()
            {
                MapServiceUrl = TileHelper.OSM_BASE_URL_TEMPLATE,
            };

            var options = new OptionSet()
            {
                {"h|?|help", "Show this message and exits", h => showHelp = h != null},
                {"v|verbose", "Display verbose information logs while running", v => verbose = v != null},
                {"t|type=", "Type of the map service to be cached", t => mapServiceType = t.ToLower()},
                {"m|mapservice=", "Url of the Map Service to be cached", m => mapServiceUrl = m},
                {"s|settings=", "Extra settings needed by the type of map service being used", s => settings = s},
                {"o|output=", "Complete file path and file name where the tile cache needs to be outputted", o => dbLocation = o},
                {"z|minz=", "Minimum zoom scale at which to begin caching", z => int.TryParse(z, out minz)},
                {"Z|maxz=", "Maximum zoom scale at which to end caching", Z => int.TryParse(Z, out maxz)},
                {"x|minx=", "Minimum X coordinate value of the extent to cache", x => double.TryParse(x, out minx)},
                {"y|miny=", "Minimum Y coordinate value of the extent to cache", y => double.TryParse(y, out miny)},
                {"X|maxx=", "Maximum X coordinate value of the extent to cache", X => double.TryParse(X, out maxx)},
                {"Y|maxy=", "Maximum Y coordinate value of the extent to cache", Y => double.TryParse(Y, out maxy)},
                {"p|parallelops=", "Limits the number of concurrent operations run by TileCutter", p => int.TryParse(p, out maxDegreeOfParallelism)},
                {"r|replace=", "Delete existing tile cache MBTiles database if already present and create a new one.", r => Boolean.TryParse(r, out replaceExistingCacheDB)},
                {"i|info=", "", i => showInfo = i != null}
            };
            options.Parse(args);

            if (showHelp)
            {
                ShowHelp(options);
                return;
            }

            var tiles = GetTiles(minz, maxz, minx, miny, maxx, maxy);

            if (showInfo)
            {
                var zoomLevels = Enumerable.Range(0, 21);
                var tileCountByZoomLevel = new Dictionary<int, int>();
                foreach (var level in zoomLevels)
                    tileCountByZoomLevel[level] = 0;
                foreach (var tile in tiles)
                    tileCountByZoomLevel[tile.Level]++;

                foreach (var item in tileCountByZoomLevel)
                    if (item.Value > 0)
                        Console.WriteLine(string.Format("Zoom level {0} - {1} tiles", item.Key, item.Value));
                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
                return;
            }

            if (!string.IsNullOrEmpty(mapServiceType))
                tileSource = GetTileSource(mapServiceType, mapServiceUrl, settings);

            string tileCacheDirectory = Path.GetDirectoryName(dbLocation);
            string tilecacheFileName = Path.GetFileNameWithoutExtension(dbLocation);
            if (!Directory.Exists(tileCacheDirectory))
            {
                Console.WriteLine("The tilecache path provided is not valid");
                return;
            }
            string errorLogFile = Path.Combine(tileCacheDirectory, tilecacheFileName + ".log");

            //if the user option to delete existing tile cache db is true delete it or else add to it
            if (replaceExistingCacheDB && File.Exists(dbLocation))
                File.Delete(dbLocation);

            //Connect to the sqlite database
            string connectionString = string.Format("Data Source={0}; FailIfMissing=False", dbLocation);
            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                connection.Open();

                //Check if the 'metadata' table exists, if not create it
                var rows = connection.GetSchema("Tables").Select("Table_Name = 'metadata'");
                if (!rows.Any())
                {
                    var command = connection.CreateCommand();
                    command.CommandText = "CREATE TABLE metadata (name text, value text);";
                    command.ExecuteNonQuery();
                    connection.Execute("CREATE UNIQUE INDEX name ON metadata ('name')");
                    connection.Execute(@"INSERT INTO metadata(name, value) values (@a, @b)",
                        new[] {
                        new { a = "name", b = "cache" },
                        new { a = "type", b = "overlay" },
                        new { a = "version", b = "1" },
                        new { a = "description", b = "some info here" },
                        new { a = "format", b = "png" },
                        new { a = "bounds", b = string.Format("{0},{1},{2},{3}", minx, miny, maxx, maxy)}
                    });
                }

                //Check if the 'images' table exists, if not create it
                rows = connection.GetSchema("Tables").Select("Table_Name = 'images'");
                if (!rows.Any())
                {
                    var command = connection.CreateCommand();
                    command.CommandText = "CREATE TABLE [images] ([tile_id] INTEGER  NOT NULL PRIMARY KEY, [tile_md5hash] VARCHAR(256) NOT NULL, [tile_data] BLOB  NULL);";
                    command.ExecuteNonQuery();
                    command = connection.CreateCommand();
                    command.CommandText = "CREATE UNIQUE INDEX images_hash on images (tile_md5hash)";
                    command.ExecuteNonQuery();
                }

                //Check if the 'map' table exists, if not create it
                rows = connection.GetSchema("Tables").Select("Table_Name = 'map'");
                if (!rows.Any())
                {
                    var command = connection.CreateCommand();
                    command.CommandText = "CREATE TABLE [map] ([map_id] INTEGER  NOT NULL PRIMARY KEY AUTOINCREMENT, [tile_id] INTEGER  NOT NULL, [zoom_level] INTEGER  NOT NULL, [tile_row] INTEGER  NOT NULL, [tile_column] INTEGER  NOT NULL);";
                    command.ExecuteNonQuery();
                }
            }

            FileStream errorLog = File.Create(errorLogFile);
            StreamWriter errorWriter = new StreamWriter(errorLog);
            Console.WriteLine("Output Cache file is: " + dbLocation);
            BlockingCollection<TileImage> images = new BlockingCollection<TileImage>();
            Task.Factory.StartNew(() =>
            {
                ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
                Parallel.ForEach(tiles, parallelOptions, (tile) =>
                {
                    string tileUrl = tileSource.GetTileUrl(tile);
                    WebClient client = new WebClient();
                    try
                    {
                        byte[] image = client.DownloadData(tileUrl);
                        images.Add(new TileImage()
                        {
                            Tile = tile,
                            Image = image
                        });

                        if (verbose)
                            Console.WriteLine(string.Format("Tile Level:{0}, Row:{1}, Column:{2} downloaded.", tile.Level, tile.Row, tile.Column));
                    }
                    catch (WebException ex)
                    {
                        errorWriter.WriteLine(String.Format("{0},{1},{2} - {3}", tile.Level, tile.Column, tile.Row, ex.Message));
                        errorLog.Flush();
                        Console.WriteLine(string.Format("Error while downloading tile Level:{0}, Row:{1}, Column:{2} - {3}.", tile.Level, tile.Row, tile.Column, ex.Message));
                        return;
                    }
                    finally { client.Dispose(); }
                });
            }).ContinueWith(t =>
            {
                images.CompleteAdding();
                if (verbose)
                    Console.WriteLine("All downloads complete.");
                errorLog.Flush();
                errorLog.Dispose();
            });

            int currentTileId = 1;
            Action<TileImage[]> processBatch = (batch) =>
            {
                using (SQLiteConnection connection = new SQLiteConnection(connectionString))
                {
                    connection.Open();
                    using (SQLiteTransaction transaction = connection.BeginTransaction())
                    {
                        using (SQLiteCommand mapCommand = connection.CreateCommand(),
                            imagesCommand = connection.CreateCommand(), tileCountCommand = connection.CreateCommand(),
                            mapDeleteCommand = connection.CreateCommand(),
                            imageDeleteCommand = connection.CreateCommand())
                        {
                            tileCountCommand.CommandText = "select ifnull(max(tile_id), 1) from images";
                            var tileCountObj = tileCountCommand.ExecuteScalar();
                            if (tileCountObj != null)
                            {
                                int.TryParse(tileCountObj.ToString(), out currentTileId);
                                currentTileId++;
                            }
                            //Create a dummy query for the [map] table and fill the adapter with it
                            //the purpose of this is to get the table structure in a DataTable
                            //the adapter also builds the insert command for it in when it is populated
                            mapCommand.CommandText = "SELECT * FROM [map] WHERE 1 = 2";
                            SQLiteDataAdapter mapAdapter = new SQLiteDataAdapter(mapCommand);

                            //Create a dummy query for the [images] table and fill the adapter with it
                            //the purpose of this is to get the table structure in a DataTable
                            //the adapter also builds the insert command for it in when it is populated
                            imagesCommand.CommandText = "SELECT * FROM [images] WHERE 1 = 2";
                            SQLiteDataAdapter imagesAdapter = new SQLiteDataAdapter(imagesCommand);

                            using (SQLiteCommandBuilder mapCmdBuilder = new SQLiteCommandBuilder(mapAdapter),
                                imagesCmdBuilder = new SQLiteCommandBuilder(imagesAdapter))
                            {
                                using (imagesAdapter.InsertCommand = (SQLiteCommand)((ICloneable)imagesCmdBuilder.GetInsertCommand()).Clone())
                                using (mapAdapter.InsertCommand = (SQLiteCommand)((ICloneable)mapCmdBuilder.GetInsertCommand()).Clone())
                                {
                                    imagesCmdBuilder.DataAdapter = null;
                                    mapCmdBuilder.DataAdapter = null;
                                    using (DataTable mapTable = new DataTable(),
                                        imagesTable = new DataTable())
                                    {
                                        if (batch.Any())
                                        {
                                            var conditions = batch.Select(b => string.Format("([zoom_level]={0} and [tile_row]={1} and [tile_column]={2})", b.Tile.Level, b.Tile.Row, b.Tile.Column));
                                            var whereClause = string.Join("or", conditions);
                                            var mapDeleteStatement = string.Format("delete from map where {0}", whereClause);
                                            var imageDeleteStatement = string.Format("delete from images where [tile_id] in (select [tile_id] from map where {0})", whereClause);

                                            imageDeleteCommand.CommandText = imageDeleteStatement;
                                            imageDeleteCommand.ExecuteNonQuery();
                                            mapDeleteCommand.CommandText = mapDeleteStatement;
                                            mapDeleteCommand.ExecuteNonQuery();
                                        }

                                        imagesAdapter.Fill(imagesTable);
                                        mapAdapter.Fill(mapTable);
                                        //Dictionary to eliminate duplicate images within batch
                                        Dictionary<string, int> added = new Dictionary<string, int>();
                                        //looping thru keys is safe to do here because
                                        //the Keys property of concurrentDictionary provides a snapshot of the keys
                                        //while enumerating
                                        //the TryGet & TryRemove inside the loop checks for items that were removed by another thread
                                        List<int> tileIdsInCurrentBatch = new List<int>();
                                        foreach (var tileimg in batch)
                                        {
                                            string hash = Convert.ToBase64String(new System.Security.Cryptography.MD5CryptoServiceProvider().ComputeHash(tileimg.Image));
                                            int tileid = -1;
                                            if (!added.ContainsKey(hash))
                                            {
                                                mapCommand.CommandText = "SELECT [tile_id] FROM [images] WHERE [tile_md5hash] = @hash";
                                                mapCommand.Parameters.Add(new SQLiteParameter("hash", hash));
                                                object tileObj = mapCommand.ExecuteScalar();
                                                if (tileObj != null && int.TryParse(tileObj.ToString(), out tileid))
                                                    added.Add(hash, tileid);
                                                else
                                                {
                                                    tileid = currentTileId++;
                                                    added.Add(hash, tileid);
                                                    DataRow idr = imagesTable.NewRow();
                                                    idr["tile_md5hash"] = hash;
                                                    idr["tile_data"] = tileimg.Image;
                                                    idr["tile_id"] = added[hash];
                                                    imagesTable.Rows.Add(idr);
                                                }
                                            }

                                            tileIdsInCurrentBatch.Add(added[hash]);

                                            DataRow mdr = mapTable.NewRow();
                                            mdr["zoom_level"] = tileimg.Tile.Level;
                                            mdr["tile_column"] = tileimg.Tile.Column;
                                            mdr["tile_row"] = tileimg.Tile.Row;
                                            mdr["tile_id"] = added[hash];
                                            mapTable.Rows.Add(mdr);
                                        }//for loop thru images

                                        tileIdsInCurrentBatch.Clear();

                                        imagesAdapter.Update(imagesTable);
                                        mapAdapter.Update(mapTable);
                                        transaction.Commit();

                                        if (verbose)
                                            Console.WriteLine(String.Format("Saving an image batch of {0}.", batch.Length));
                                    }//using for datatable
                                }//using for insert command
                            }//using for command builder
                        }//using for select command
                    }
                }//using for connection
            };

            List<TileImage> tilebatch = new List<TileImage>();
            foreach (var tileImage in images.GetConsumingEnumerable())
            {
                tilebatch.Add(tileImage);
                if (tilebatch.Count < 50)
                    continue;
                processBatch(tilebatch.ToArray());
                tilebatch.Clear();
            }

            if (verbose)
                Console.WriteLine("Saving remaining images that didn't fit into a batch.");

            processBatch(tilebatch.ToArray());
            tilebatch.Clear();

            if (verbose)
                Console.WriteLine("Creating Index on table [map] and Creating View [tiles].");
            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                connection.Open();
                connection.Execute("CREATE UNIQUE INDEX IF NOT EXISTS map_index on map (zoom_level, tile_column, tile_row)");
                connection.Execute("CREATE VIEW IF NOT EXISTS tiles as SELECT map.zoom_level as zoom_level, map.tile_column as tile_column, map.tile_row as tile_row, images.tile_data as tile_data FROM map JOIN images on images.tile_id = map.tile_id");
                connection.Close();
            }

            Console.WriteLine("All Done !!!");
        }
예제 #2
0
        static void Main(string[] args)
        {
            string         dbLocation             = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "tilecache.s3db");
            int            minz                   = 7;
            int            maxz                   = 10;
            double         minx                   = -95.844727;
            double         miny                   = 35.978006;
            double         maxx                   = -88.989258;
            double         maxy                   = 40.563895;
            string         mapServiceUrl          = "http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Demographics/ESRI_Census_USA/MapServer";
            int            maxDegreeOfParallelism = 10;
            bool           replaceExistingCacheDB = true;
            bool           showHelp               = false;
            bool           verbose                = false;
            string         mapServiceType         = "agsd";
            string         settings               = string.Empty;
            bool           showInfo               = false;
            ITileUrlSource tileSource             = new OSMTileUrlSource()
            {
                MapServiceUrl = TileHelper.OSM_BASE_URL_TEMPLATE,
            };

            var options = new OptionSet()
            {
                { "h|?|help", "Show this message and exits", h => showHelp = h != null },
                { "v|verbose", "Display verbose information logs while running", v => verbose = v != null },
                { "t|type=", "Type of the map service to be cached", t => mapServiceType = t.ToLower() },
                { "m|mapservice=", "Url of the Map Service to be cached", m => mapServiceUrl = m },
                { "s|settings=", "Extra settings needed by the type of map service being used", s => settings = s },
                { "o|output=", "Complete file path and file name where the tile cache needs to be outputted", o => dbLocation = o },
                { "z|minz=", "Minimum zoom scale at which to begin caching", z => int.TryParse(z, out minz) },
                { "Z|maxz=", "Maximum zoom scale at which to end caching", Z => int.TryParse(Z, out maxz) },
                { "x|minx=", "Minimum X coordinate value of the extent to cache", x => double.TryParse(x, out minx) },
                { "y|miny=", "Minimum Y coordinate value of the extent to cache", y => double.TryParse(y, out miny) },
                { "X|maxx=", "Maximum X coordinate value of the extent to cache", X => double.TryParse(X, out maxx) },
                { "Y|maxy=", "Maximum Y coordinate value of the extent to cache", Y => double.TryParse(Y, out maxy) },
                { "p|parallelops=", "Limits the number of concurrent operations run by TileCutter", p => int.TryParse(p, out maxDegreeOfParallelism) },
                { "r|replace=", "Delete existing tile cache MBTiles database if already present and create a new one.", r => Boolean.TryParse(r, out replaceExistingCacheDB) },
                { "i|info=", "", i => showInfo = i != null }
            };

            options.Parse(args);

            if (showHelp)
            {
                ShowHelp(options);
                return;
            }

            var tiles = GetTiles(minz, maxz, minx, miny, maxx, maxy);

            if (showInfo)
            {
                var zoomLevels           = Enumerable.Range(0, 21);
                var tileCountByZoomLevel = new Dictionary <int, int>();
                foreach (var level in zoomLevels)
                {
                    tileCountByZoomLevel[level] = 0;
                }
                foreach (var tile in tiles)
                {
                    tileCountByZoomLevel[tile.Level]++;
                }

                foreach (var item in tileCountByZoomLevel)
                {
                    if (item.Value > 0)
                    {
                        Console.WriteLine(string.Format("Zoom level {0} - {1} tiles", item.Key, item.Value));
                    }
                }
                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
                return;
            }

            if (!string.IsNullOrEmpty(mapServiceType))
            {
                tileSource = GetTileSource(mapServiceType, mapServiceUrl, settings);
            }

            string tileCacheDirectory = Path.GetDirectoryName(dbLocation);
            string tilecacheFileName  = Path.GetFileNameWithoutExtension(dbLocation);

            if (!Directory.Exists(tileCacheDirectory))
            {
                Console.WriteLine("The tilecache path provided is not valid");
                return;
            }
            string errorLogFile = Path.Combine(tileCacheDirectory, tilecacheFileName + ".log");

            //if the user option to delete existing tile cache db is true delete it or else add to it
            if (replaceExistingCacheDB && File.Exists(dbLocation))
            {
                File.Delete(dbLocation);
            }

            //Connect to the sqlite database
            string connectionString = string.Format("Data Source={0}; FailIfMissing=False", dbLocation);

            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                connection.Open();

                //Check if the 'metadata' table exists, if not create it
                var rows = connection.GetSchema("Tables").Select("Table_Name = 'metadata'");
                if (!rows.Any())
                {
                    var command = connection.CreateCommand();
                    command.CommandText = "CREATE TABLE metadata (name text, value text);";
                    command.ExecuteNonQuery();
                    connection.Execute("CREATE UNIQUE INDEX name ON metadata ('name')");
                    connection.Execute(@"INSERT INTO metadata(name, value) values (@a, @b)",
                                       new[] {
                        new { a = "name", b = "cache" },
                        new { a = "type", b = "overlay" },
                        new { a = "version", b = "1" },
                        new { a = "description", b = "some info here" },
                        new { a = "format", b = "png" },
                        new { a = "bounds", b = string.Format("{0},{1},{2},{3}", minx, miny, maxx, maxy) }
                    });
                }

                //Check if the 'images' table exists, if not create it
                rows = connection.GetSchema("Tables").Select("Table_Name = 'images'");
                if (!rows.Any())
                {
                    var command = connection.CreateCommand();
                    command.CommandText = "CREATE TABLE [images] ([tile_id] INTEGER  NOT NULL PRIMARY KEY, [tile_md5hash] VARCHAR(256) NOT NULL, [tile_data] BLOB  NULL);";
                    command.ExecuteNonQuery();
                    command             = connection.CreateCommand();
                    command.CommandText = "CREATE UNIQUE INDEX images_hash on images (tile_md5hash)";
                    command.ExecuteNonQuery();
                }

                //Check if the 'map' table exists, if not create it
                rows = connection.GetSchema("Tables").Select("Table_Name = 'map'");
                if (!rows.Any())
                {
                    var command = connection.CreateCommand();
                    command.CommandText = "CREATE TABLE [map] ([map_id] INTEGER  NOT NULL PRIMARY KEY AUTOINCREMENT, [tile_id] INTEGER  NOT NULL, [zoom_level] INTEGER  NOT NULL, [tile_row] INTEGER  NOT NULL, [tile_column] INTEGER  NOT NULL);";
                    command.ExecuteNonQuery();
                }
            }

            FileStream   errorLog    = File.Create(errorLogFile);
            StreamWriter errorWriter = new StreamWriter(errorLog);

            Console.WriteLine("Output Cache file is: " + dbLocation);
            BlockingCollection <TileImage> images = new BlockingCollection <TileImage>();

            Task.Factory.StartNew(() =>
            {
                ParallelOptions parallelOptions = new ParallelOptions()
                {
                    MaxDegreeOfParallelism = maxDegreeOfParallelism
                };
                Parallel.ForEach(tiles, parallelOptions, (tile) =>
                {
                    string tileUrl   = tileSource.GetTileUrl(tile);
                    WebClient client = new WebClient();
                    try
                    {
                        byte[] image = client.DownloadData(tileUrl);
                        images.Add(new TileImage()
                        {
                            Tile  = tile,
                            Image = image
                        });

                        if (verbose)
                        {
                            Console.WriteLine(string.Format("Tile Level:{0}, Row:{1}, Column:{2} downloaded.", tile.Level, tile.Row, tile.Column));
                        }
                    }
                    catch (WebException ex)
                    {
                        errorWriter.WriteLine(String.Format("{0},{1},{2} - {3}", tile.Level, tile.Column, tile.Row, ex.Message));
                        errorLog.Flush();
                        Console.WriteLine(string.Format("Error while downloading tile Level:{0}, Row:{1}, Column:{2} - {3}.", tile.Level, tile.Row, tile.Column, ex.Message));
                        return;
                    }
                    finally { client.Dispose(); }
                });
            }).ContinueWith(t =>
            {
                images.CompleteAdding();
                if (verbose)
                {
                    Console.WriteLine("All downloads complete.");
                }
                errorLog.Flush();
                errorLog.Dispose();
            });

            int currentTileId = 1;
            Action <TileImage[]> processBatch = (batch) =>
            {
                using (SQLiteConnection connection = new SQLiteConnection(connectionString))
                {
                    connection.Open();
                    using (SQLiteTransaction transaction = connection.BeginTransaction())
                    {
                        using (SQLiteCommand mapCommand = connection.CreateCommand(),
                               imagesCommand = connection.CreateCommand(), tileCountCommand = connection.CreateCommand(),
                               mapDeleteCommand = connection.CreateCommand(),
                               imageDeleteCommand = connection.CreateCommand())
                        {
                            tileCountCommand.CommandText = "select ifnull(max(tile_id), 1) from images";
                            var tileCountObj = tileCountCommand.ExecuteScalar();
                            if (tileCountObj != null)
                            {
                                int.TryParse(tileCountObj.ToString(), out currentTileId);
                                currentTileId++;
                            }
                            //Create a dummy query for the [map] table and fill the adapter with it
                            //the purpose of this is to get the table structure in a DataTable
                            //the adapter also builds the insert command for it in when it is populated
                            mapCommand.CommandText = "SELECT * FROM [map] WHERE 1 = 2";
                            SQLiteDataAdapter mapAdapter = new SQLiteDataAdapter(mapCommand);

                            //Create a dummy query for the [images] table and fill the adapter with it
                            //the purpose of this is to get the table structure in a DataTable
                            //the adapter also builds the insert command for it in when it is populated
                            imagesCommand.CommandText = "SELECT * FROM [images] WHERE 1 = 2";
                            SQLiteDataAdapter imagesAdapter = new SQLiteDataAdapter(imagesCommand);

                            using (SQLiteCommandBuilder mapCmdBuilder = new SQLiteCommandBuilder(mapAdapter),
                                   imagesCmdBuilder = new SQLiteCommandBuilder(imagesAdapter))
                            {
                                using (imagesAdapter.InsertCommand = (SQLiteCommand)((ICloneable)imagesCmdBuilder.GetInsertCommand()).Clone())
                                    using (mapAdapter.InsertCommand = (SQLiteCommand)((ICloneable)mapCmdBuilder.GetInsertCommand()).Clone())
                                    {
                                        imagesCmdBuilder.DataAdapter = null;
                                        mapCmdBuilder.DataAdapter    = null;
                                        using (DataTable mapTable = new DataTable(),
                                               imagesTable = new DataTable())
                                        {
                                            if (batch.Any())
                                            {
                                                var conditions           = batch.Select(b => string.Format("([zoom_level]={0} and [tile_row]={1} and [tile_column]={2})", b.Tile.Level, b.Tile.Row, b.Tile.Column));
                                                var whereClause          = string.Join("or", conditions);
                                                var mapDeleteStatement   = string.Format("delete from map where {0}", whereClause);
                                                var imageDeleteStatement = string.Format("delete from images where [tile_id] in (select [tile_id] from map where {0})", whereClause);

                                                imageDeleteCommand.CommandText = imageDeleteStatement;
                                                imageDeleteCommand.ExecuteNonQuery();
                                                mapDeleteCommand.CommandText = mapDeleteStatement;
                                                mapDeleteCommand.ExecuteNonQuery();
                                            }

                                            imagesAdapter.Fill(imagesTable);
                                            mapAdapter.Fill(mapTable);
                                            //Dictionary to eliminate duplicate images within batch
                                            Dictionary <string, int> added = new Dictionary <string, int>();
                                            //looping thru keys is safe to do here because
                                            //the Keys property of concurrentDictionary provides a snapshot of the keys
                                            //while enumerating
                                            //the TryGet & TryRemove inside the loop checks for items that were removed by another thread
                                            List <int> tileIdsInCurrentBatch = new List <int>();
                                            foreach (var tileimg in batch)
                                            {
                                                string hash   = Convert.ToBase64String(new System.Security.Cryptography.MD5CryptoServiceProvider().ComputeHash(tileimg.Image));
                                                int    tileid = -1;
                                                if (!added.ContainsKey(hash))
                                                {
                                                    mapCommand.CommandText = "SELECT [tile_id] FROM [images] WHERE [tile_md5hash] = @hash";
                                                    mapCommand.Parameters.Add(new SQLiteParameter("hash", hash));
                                                    object tileObj = mapCommand.ExecuteScalar();
                                                    if (tileObj != null && int.TryParse(tileObj.ToString(), out tileid))
                                                    {
                                                        added.Add(hash, tileid);
                                                    }
                                                    else
                                                    {
                                                        tileid = currentTileId++;
                                                        added.Add(hash, tileid);
                                                        DataRow idr = imagesTable.NewRow();
                                                        idr["tile_md5hash"] = hash;
                                                        idr["tile_data"]    = tileimg.Image;
                                                        idr["tile_id"]      = added[hash];
                                                        imagesTable.Rows.Add(idr);
                                                    }
                                                }

                                                tileIdsInCurrentBatch.Add(added[hash]);

                                                DataRow mdr = mapTable.NewRow();
                                                mdr["zoom_level"]  = tileimg.Tile.Level;
                                                mdr["tile_column"] = tileimg.Tile.Column;
                                                mdr["tile_row"]    = tileimg.Tile.Row;
                                                mdr["tile_id"]     = added[hash];
                                                mapTable.Rows.Add(mdr);
                                            }//for loop thru images

                                            tileIdsInCurrentBatch.Clear();

                                            imagesAdapter.Update(imagesTable);
                                            mapAdapter.Update(mapTable);
                                            transaction.Commit();

                                            if (verbose)
                                            {
                                                Console.WriteLine(String.Format("Saving an image batch of {0}.", batch.Length));
                                            }
                                        } //using for datatable
                                    }//using for insert command
                            }             //using for command builder
                        }                 //using for select command
                    }
                }                         //using for connection
            };

            List <TileImage> tilebatch = new List <TileImage>();

            foreach (var tileImage in images.GetConsumingEnumerable())
            {
                tilebatch.Add(tileImage);
                if (tilebatch.Count < 50)
                {
                    continue;
                }
                processBatch(tilebatch.ToArray());
                tilebatch.Clear();
            }

            if (verbose)
            {
                Console.WriteLine("Saving remaining images that didn't fit into a batch.");
            }

            processBatch(tilebatch.ToArray());
            tilebatch.Clear();

            if (verbose)
            {
                Console.WriteLine("Creating Index on table [map] and Creating View [tiles].");
            }
            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                connection.Open();
                connection.Execute("CREATE UNIQUE INDEX IF NOT EXISTS map_index on map (zoom_level, tile_column, tile_row)");
                connection.Execute("CREATE VIEW IF NOT EXISTS tiles as SELECT map.zoom_level as zoom_level, map.tile_column as tile_column, map.tile_row as tile_row, images.tile_data as tile_data FROM map JOIN images on images.tile_id = map.tile_id");
                connection.Close();
            }

            Console.WriteLine("All Done !!!");
        }