/* * Copyright (C) 2015 Alec Dhuse * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package co.foldingmap.map.tile; import co.foldingmap.Logger; import co.foldingmap.mapImportExport.TileExporter; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URL; import java.sql.*; import java.util.ArrayList; import java.util.HashMap; import javax.imageio.ImageIO; /** * * @author Alec */ public class TileDownloader extends Thread { private final ArrayList<TileReference> tilesToDownload; private final boolean urlReplace; private Connection conn; private HashMap<TileReference, BufferedImage> tileMap; private final String dbFile, tileServerAddress; public TileDownloader(String tileServerAddress, String sourceTitle) { this.tilesToDownload = new ArrayList<TileReference>(); this.urlReplace = tileServerAddress.contains("{x}"); this.tileMap = new HashMap<TileReference, BufferedImage>(); if (tileServerAddress.toLowerCase().startsWith("http")) { this.tileServerAddress = tileServerAddress; } else { this.tileServerAddress = "http://" + tileServerAddress; } this.dbFile = sourceTitle + ".mbtiles"; //load database //openDbConnection(); //createTables(); } public void closeConnection() { try { conn.close(); } catch (Exception e) { Logger.log(Logger.ERR, "Error in TileDownloader.closeConnection() - " + e); } } /** * Create tables for the cache database */ private void createTables() { PreparedStatement prep; Statement stat; try { stat = conn.createStatement(); prep = conn.prepareStatement(""); //create the tables stat.executeUpdate("create table if not exists metadata (name text, value text);"); stat.executeUpdate("create table if not exists map (zoom_level integer, tile_column integer, tile_row integer, tile_id text, grid_id text);"); stat.executeUpdate("create table if not exists grid_key (grid_id text, key_name text);"); stat.executeUpdate("create table if not exists keymap (key_name text, key_json text);"); stat.executeUpdate("create table if not exists grid_uftgrid (grid_id text, grid_uftgrid blob);"); stat.executeUpdate("create table if not exists images (tile_data blob, tile_id text);"); //create indexes stat.executeUpdate("CREATE UNIQUE INDEX if not exists map_index ON map (zoom_level, tile_column, tile_row);"); stat.executeUpdate("CREATE UNIQUE INDEX if not exists grid_key_lookup ON grid_key (grid_id, key_name);"); stat.executeUpdate("CREATE UNIQUE INDEX if not exists keymap_lookup ON keymap (key_name);"); stat.executeUpdate("CREATE UNIQUE INDEX if not exists grid_uftgrid_lookup ON grid_uftgrid (grid_id);"); stat.executeUpdate("CREATE UNIQUE INDEX if not exists images_id ON images (tile_id);"); stat.executeUpdate("CREATE UNIQUE INDEX if not exists name ON metadata (name);"); //create view to mimic the tiles table that appears in the spec stat.executeUpdate("CREATE VIEW if not exists tiles AS SELECT m.zoom_Level zoom_level, m.tile_column tile_column, m.tile_row tile_row, i.tile_data tile_data FROM map m, images i WHERE m.tile_id = i.tile_id;"); } catch (Exception e) { Logger.log(Logger.ERR, "Error in TileDownloader.createTables() - " + e); } } /** * Downloads a tile from the tile server. * @param tileRef The TileReference of the tile to download. * @return The downloaded tile as a BufferedImage */ public BufferedImage downloadTile(TileReference tileRef) { BufferedImage bufferedImage; int x, y, z; String constructedURL, lastMod; URL url; try { x = tileRef.getX(); y = tileRef.getY(); z = tileRef.getZoom(); if (y < 0) y = 0; if (urlReplace) { constructedURL = tileServerAddress; constructedURL = constructedURL.replace("{x}", Integer.toString(x)); constructedURL = constructedURL.replace("{y}", Integer.toString(y)); constructedURL = constructedURL.replace("{z}", Integer.toString(z)); } else { constructedURL = tileServerAddress + "/" + z + "/" + x + "/" + y + ".png"; } url = new URL(constructedURL); //urlConn = url.openConnection(); //lastMod = urlConn.getHeaderField("Expires"); bufferedImage = ImageIO.read(url); } catch (Exception e) { Logger.log(Logger.ERR, "Error TileDownloader.downloadTile(" + tileRef.toString() + ") - " + e); bufferedImage = null; } tileMap.put(tileRef, bufferedImage); return bufferedImage; } /** * Get a tile from the cache database. * * @param tileRef * @return */ public BufferedImage getTileFromDB(TileReference tileRef) { byte[] tileImage; BufferedImage bi; int numberOfTiles, x, y; ResultSet rs; Statement stat; String sql, tileID; bi = null; try { numberOfTiles = (int) Math.pow(2, tileRef.getZoom()); //allow for repeating of tiles if (tileRef.getX() >= numberOfTiles) { x = (tileRef.getX() - numberOfTiles); } else if (tileRef.getX() < 0) { x = numberOfTiles + tileRef.getX(); } else { x = tileRef.getX(); } return tileMap.get(tileRef); /* //MBTiles y is reversed y = (numberOfTiles - tileRef.getY()) - 1; sql = "SELECT tile_id FROM map WHERE zoom_level =" + tileRef.getZoom() + " AND tile_column =" + x + " AND tile_row =" + y + ";"; stat = conn.createStatement(); rs = stat.executeQuery(sql); if (rs.next()) { tileID = rs.getString("tile_id"); sql = "SELECT tile_data FROM images WHERE tile_id = '" + tileID + "';"; rs = stat.executeQuery(sql); if (rs.next()) { tileImage = rs.getBytes("tile_data"); bi = ImageIO.read(new ByteArrayInputStream(tileImage)); } } */ } catch (Exception e) { //} catch (SQLException | IOException e) { Logger.log(Logger.ERR, "Error in TileDownloader.getTileFromDB(TileReference) - " + e); } return bi; } /** * Gets a tile by retrieving it from the cache or downloading it from the * server if there is no cached version available. * * @param tileRef * @return */ public BufferedImage getTileImage(TileReference tileRef) { BufferedImage tileBI; if (tileRef.getY() >= 0) { tileBI = getTileFromDB(tileRef); if (tileBI == null) { if (!tilesToDownload.contains(tileRef)) { tilesToDownload.add(tileRef); if (!this.isAlive()) this.start(); } } } else { tileBI = null; } return tileBI; } /** * Open the SQLite database connection. * */ public final void openDbConnection() { try { Class.forName("org.sqlite.JDBC"); this.conn = DriverManager.getConnection("jdbc:sqlite:" + this.dbFile); } catch (ClassNotFoundException | SQLException e) { Logger.log(Logger.ERR, "Error in openDbConnection() - " + e); } } /** * Puts a tile into the tile cache database. * * @param tileRef * @param bi */ public void putTileImage(TileReference tileRef, BufferedImage bi) { byte[] tileImageBytes; ByteArrayOutputStream baos; int numberOfTiles; PreparedStatement prep; String imageHash; try { if (bi != null) { numberOfTiles = (int) Math.pow(2, tileRef.getZoom()); baos = new ByteArrayOutputStream(); ImageIO.write(bi, "png", baos); tileImageBytes = baos.toByteArray(); //hash the image imageHash = TileExporter.hashBytes(tileImageBytes); /* The actual Images are stored here, referenced by the * hash. If an image is just empty ocean there will be * a hash colision and thus saving space in the db. */ prep = conn.prepareStatement("INSERT OR IGNORE into images values (?, ?);"); prep.setBytes(1, tileImageBytes); prep.setString(2, imageHash); prep.addBatch(); conn.setAutoCommit(false); prep.executeBatch(); conn.setAutoCommit(true); prep = conn.prepareStatement("INSERT OR REPLACE into map values (?, ?, ?, ?, ?);"); prep.setInt(1, tileRef.getZoom()); prep.setInt(2, tileRef.getX()); prep.setInt(3, (numberOfTiles - tileRef.getY()) - 1); //MBTiles y is reversed prep.setString(4, imageHash); prep.setString(5, " "); prep.addBatch(); conn.setAutoCommit(false); prep.executeBatch(); conn.setAutoCommit(true); } } catch (IOException | SQLException e) { closeConnection(); openDbConnection(); Logger.log(Logger.ERR, "Error in TileDownloader.putTileImage(TileReference, BufferedImage) - " + e); } } @Override public void run() { BufferedImage bi; TileReference tileRef; try { while (!this.isInterrupted()) { try { while (tilesToDownload.size() > 0) { tileRef = tilesToDownload.remove(0); bi = downloadTile(tileRef); /* if (bi != null) putTileImage(tileRef, bi); */ } } catch (Exception e) {} sleep(250); } } catch (Exception e) {} } }