/* * Copyright (C) 2014 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.map.vector.Coordinate; import co.foldingmap.map.vector.LatLonAltBox; import co.foldingmap.mapImportExport.TileExporter; import co.foldingmap.xml.XmlOutput; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.sql.*; import java.util.StringTokenizer; import javax.imageio.ImageIO; /** * * @author Alec */ public class MbTileSource extends TileSource { private boolean tilesTable; private Connection conn; private Coordinate center; private int initZoom; private String mapBounds, mapVersion, filePathName; /** * Creates a connection to a SQLite database in the MBTiles format. * If no file exists a new one is created. * * @param filePathName */ public MbTileSource(String filePathName) { this.filePathName = filePathName; try { File f = new File(filePathName); Class.forName("org.sqlite.JDBC"); conn = DriverManager.getConnection("jdbc:sqlite:" + filePathName); if (f.exists()) { createTables(); } loadMetaData(); } catch (Exception e) { Logger.log(Logger.ERR, "Error in MbTileSource.constructor(String) - " + e); } } /** * Closes the connection to the Tile Source. */ @Override public void closeSource() { try { conn.close(); } catch (Exception e) { Logger.log(Logger.ERR, "Error in MbTilsSource.closeSource() - " + e); } } /** * Creates the tables needed for the MBTiles database format. */ 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) { System.err.println("Error in MbTileSource.createTables() - " + e); } } /** * Returns the bounds for a give MbTiles bounds string formated: * West, South, East, North; * * @param bounds * @return */ public static LatLonAltBox getBounds(String bounds) { LatLonAltBox boundingBox; String north, south, east, west; StringTokenizer st; st = new StringTokenizer(bounds, ","); west = st.nextToken(); south = st.nextToken(); east = st.nextToken(); north = st.nextToken(); boundingBox = new LatLonAltBox(north, south, east, west); return boundingBox; } /** * Returns the center of this map, if it has one. * * @return */ public Coordinate getCenter() { return center; } /** * Returns a BufferedImage from a SQLite database matching the given * TileReference. If no tile is present in the database then null is * returned. * * @param tr * @return */ @Override public BufferedImage getTileImage(TileReference tr) { byte[] tileImage; BufferedImage bi; int numberOfTiles, x, y; ResultSet rs; Statement stat; String sql, tileID; bi = null; try { numberOfTiles = (int) Math.pow(2, tr.getZoom()); //allow for repeating of tiles if (tr.getX() >= numberOfTiles) { x = (tr.getX() - numberOfTiles); } else if (tr.getX() < 0) { x = numberOfTiles + tr.getX(); } else { x = tr.getX(); } //MBTiles y is reversed y = (numberOfTiles - tr.getY()) - 1; if (tilesTable) { //uses tile table instead of map sql = "SELECT tile_data FROM tiles WHERE zoom_level =" + tr.getZoom() + " AND tile_column =" + x + " AND tile_row =" + y + ";"; } else { sql = "SELECT tile_id FROM map WHERE zoom_level =" + tr.getZoom() + " AND tile_column =" + x + " AND tile_row =" + y + ";"; } stat = conn.createStatement(); rs = stat.executeQuery(sql); if (rs.next()) { if (tilesTable) { tileImage = rs.getBytes("tile_data"); bi = ImageIO.read(new ByteArrayInputStream(tileImage)); } else { 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)); } } } else { //Tile not found, return null. bi = null; } } catch (Exception e) { Logger.log(Logger.ERR, "Error in MbTileSource.getTileImage(TileReference) - " + e); } return bi; } /** * Returns the path and file name of the MbTiles file used as the source for this Tile Source. * * @return */ @Override public String getSource() { return this.filePathName; } /** * Returns the zoom loaded from the meta data of this TileSource. * * @return */ public int getZoom() { return initZoom; } /** * Loads the tile map's meta data from the MbTile file. */ private void loadMetaData() { boolean hasMax, hasMin; ResultSet rs; Statement stat; String property, value; try { hasMax = false; hasMin = false; stat = conn.createStatement(); rs = stat.executeQuery("SELECT * FROM metadata;"); while (rs.next()) { property = rs.getString("name"); value = rs.getString("value"); if (property.equalsIgnoreCase("bounds")) { this.mapBounds = value; this.boundingBox = getBounds(mapBounds); } else if (property.equalsIgnoreCase("center")) { StringTokenizer centerST = new StringTokenizer(value, ","); float lng = Float.parseFloat(centerST.nextToken()); float lat = Float.parseFloat(centerST.nextToken()); if (centerST.hasMoreTokens()) initZoom = Integer.parseInt(centerST.nextToken()); center = new Coordinate(0, lat, lng); } else if (property.equalsIgnoreCase("minzoom")) { this.minZoom = Integer.parseInt(value); hasMin = true; } else if (property.equalsIgnoreCase("maxzoom")) { this.maxZoom = Integer.parseInt(value); hasMax = true; } if (property.equalsIgnoreCase("description")) this.description = value; if (property.equalsIgnoreCase("name")) this.name = value; if (property.equalsIgnoreCase("version")) this.mapVersion = value; } //check to see if this mbTile db has a tiles table stat = conn.createStatement(); rs = stat.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='tiles';"); if (rs.next()) { tilesTable = true; } else { tilesTable = false; } if (tilesTable) { if (hasMax == false) { rs = stat.executeQuery("SELECT Max(zoom_level) FROM tiles;"); if (rs.next()) { this.maxZoom = rs.getInt(1); } } if (hasMin == false) { rs = stat.executeQuery("SELECT Min(zoom_level) FROM tiles;"); if (rs.next()) { this.minZoom = rs.getInt(1); } } } else { if (hasMax == false) { rs = stat.executeQuery("SELECT Max(zoom_level) FROM map;"); if (rs.next()) { this.maxZoom = rs.getInt(1); } } if (hasMin == false) { rs = stat.executeQuery("SELECT Min(zoom_level) FROM map;"); if (rs.next()) { this.minZoom = rs.getInt(1); } } } } catch (Exception e) { Logger.log(Logger.ERR, "Error in MbTileSource.loadMetaData() - " + e); } } /** * Puts a tile into the tile MbTiles database. * * @param tileRef * @param bi */ public void putTileImage(TileReference tileRef, BufferedImage bi) { byte[] tileImageBytes; ByteArrayOutputStream baos; int numberOfTiles; PreparedStatement prep; Statement stat; 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 (Exception e) { Logger.log(Logger.ERR, "Error in MbTileSource.putTileImage(TileReference, BufferedImage) - " + e); } } /** * Writes this TileSource to FmXML. * * @param xmlWriter */ @Override public void toXML(XmlOutput xmlWriter) { xmlWriter.openTag("TileSource"); xmlWriter.writeTag("href", filePathName); xmlWriter.closeTag("TileSource"); } }