/*
* $Id$
*
* Copyright (c) 2010, 2011 by Joel Uckelman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.launch;
import static VASSAL.tools.image.tilecache.ZipFileImageTilerState.STARTING_IMAGE;
import static VASSAL.tools.image.tilecache.ZipFileImageTilerState.TILE_WRITTEN;
import static VASSAL.tools.image.tilecache.ZipFileImageTilerState.TILING_FINISHED;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import VASSAL.Info;
import VASSAL.tools.DataArchive;
import VASSAL.tools.image.ImageUtils;
import VASSAL.tools.image.tilecache.ImageTileDiskCache;
import VASSAL.tools.image.tilecache.TileUtils;
import VASSAL.tools.io.FileArchive;
import VASSAL.tools.io.FileStore;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.io.InputOutputStreamPump;
import VASSAL.tools.io.InputStreamPump;
import VASSAL.tools.io.ProcessLauncher;
import VASSAL.tools.io.ProcessWrapper;
import VASSAL.tools.lang.Pair;
import VASSAL.tools.swing.EDT;
import VASSAL.tools.swing.ProgressDialog;
import VASSAL.tools.swing.Progressor;
/**
* A launcher for the process which tiles large images.
*
* @since 3.2.0
* @author Joel Uckelman
*/
public class TilingHandler {
private static final Logger logger =
LoggerFactory.getLogger(TilingHandler.class);
protected final String aname;
protected final File cdir;
protected final Dimension tdim;
protected final int maxheap_limit;
protected final int pid;
/**
* Creates a {@code TilingHandler}.
*
* @param aname the path to the ZIP archive
* @param cdir the tile cache diretory
* @param tdim the tile size
* @param pid the id of the child process
*/
public TilingHandler(String aname, File cdir,
Dimension tdim, int mhlim, int pid) {
this.aname = aname;
this.cdir = cdir;
this.tdim = tdim;
this.maxheap_limit = mhlim;
this.pid = pid;
}
protected boolean isFresh(FileArchive archive,
FileStore tcache, String ipath)
throws IOException {
// look at the first 1:1 tile
final String tpath = TileUtils.tileName(ipath, 0, 0, 1);
// check whether the image is older than the tile
final long imtime = archive.getMTime(ipath);
return imtime > 0 && // time in archive might be goofy
imtime <= tcache.getMTime(tpath);
}
protected Dimension getImageSize(DataArchive archive, String ipath)
throws IOException {
InputStream in = null;
try {
in = archive.getInputStream(ipath);
final Dimension id = ImageUtils.getImageSize(ipath, in);
in.close();
return id;
}
finally {
IOUtils.closeQuietly(in);
}
}
protected Pair<Integer,Integer> findImages(
DataArchive archive,
FileStore tcache,
List<String> multi,
List<Pair<String,IOException>> failed) throws IOException
{
// build a list of all multi-tile images and count tiles
final Set<String> images = archive.getImageNameSet();
int maxpix = 0; // number of pixels in the largest image
int tcount = 0; // tile count
final FileArchive fa = archive.getArchive();
for (String iname : images) {
final String ipath = DataArchive.IMAGE_DIR + iname;
// skip images with fresh tiles
if (isFresh(fa, tcache, ipath)) continue;
final Dimension idim;
try {
idim = getImageSize(archive, ipath);
}
catch (IOException e) {
// skip images we can't read
failed.add(Pair.of(ipath, e));
continue;
}
// count the tiles at all sizes if we have more than one tile at 1:1
final int t = TileUtils.tileCountAtScale(idim, tdim, 1) > 1 ?
TileUtils.tileCount(idim, tdim) : 0;
if (t == 0) continue;
tcount += t;
multi.add(ipath);
// check whether this image has the most pixels
if (idim.width * idim.height > maxpix) {
maxpix = idim.width * idim.height;
}
}
return new Pair<Integer,Integer>(tcount, maxpix);
}
protected void runSlicer(List<String> multi, final int tcount, int maxheap)
throws CancellationException, IOException {
final InetAddress lo = InetAddress.getByName(null);
final ServerSocket ssock = new ServerSocket(0, 0, lo);
final int port = ssock.getLocalPort();
final List<String> args = new ArrayList<String>();
args.addAll(Arrays.asList(new String[] {
Info.javaBinPath,
"-classpath",
System.getProperty("java.class.path"),
"-Xmx" + maxheap + "M",
"-DVASSAL.id=" + pid,
"-Duser.home=" + System.getProperty("user.home"),
"-DVASSAL.port=" + port,
"VASSAL.tools.image.tilecache.ZipFileImageTiler",
aname,
cdir.getAbsolutePath(),
String.valueOf(tdim.width),
String.valueOf(tdim.height)
}));
// get the progress dialog
final ProgressDialog pd = ProgressDialog.createOnEDT(
ModuleManagerWindow.getInstance(),
"Processing Image Tiles",
" "
);
// set up the process
final InputStreamPump outP = new InputOutputStreamPump(null, System.out);
final InputStreamPump errP = new InputOutputStreamPump(null, System.err);
final ProcessWrapper proc = new ProcessLauncher().launch(
null,
outP,
errP,
args.toArray(new String[args.size()])
);
// write the image paths to child's stdin, one per line
PrintWriter stdin = null;
try {
stdin = new PrintWriter(proc.stdin);
for (String m : multi) {
stdin.println(m);
}
}
finally {
IOUtils.closeQuietly(stdin);
}
Socket csock = null;
DataInputStream in = null;
try {
csock = ssock.accept();
csock.shutdownOutput();
in = new DataInputStream(csock.getInputStream());
final Progressor progressor = new Progressor(0, tcount) {
@Override
protected void run(Pair<Integer,Integer> prog) {
pd.setProgress((100*prog.second)/max);
}
};
// setup the cancel button in the progress dialog
EDT.execute(new Runnable() {
public void run() {
pd.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
pd.setVisible(false);
proc.future.cancel(true);
}
});
}
});
boolean done = false;
byte type;
while (!done) {
type = in.readByte();
switch (type) {
case STARTING_IMAGE:
final String ipath = in.readUTF();
EDT.execute(new Runnable() {
public void run() {
pd.setLabel("Tiling " + ipath);
if (!pd.isVisible()) pd.setVisible(true);
}
});
break;
case TILE_WRITTEN:
progressor.increment();
if (progressor.get() >= tcount) {
pd.setVisible(false);
}
break;
case TILING_FINISHED:
done = true;
break;
default:
throw new IllegalStateException("bad type: " + type);
}
}
in.close();
csock.close();
ssock.close();
}
catch (IOException e) {
}
finally {
IOUtils.closeQuietly(in);
IOUtils.closeQuietly(csock);
IOUtils.closeQuietly(ssock);
}
// wait for the tiling process to end
try {
final int retval = proc.future.get();
if (retval != 0) {
throw new IOException("return value == " + retval);
}
}
catch (ExecutionException e) {
// should never happen
throw new IllegalStateException(e);
}
catch (InterruptedException e) {
// should never happen
throw new IllegalStateException(e);
}
}
protected void makeHashDirs() throws IOException {
for (int i = 0; i < 16; ++i) {
for (int j = 0; j < 16; ++j) {
final File d = new File(String.format("%s/%1x/%1x%1x", cdir, i, i, j));
FileUtils.forceMkdir(d);
}
}
}
protected void cleanup() throws IOException {
FileUtils.forceDelete(cdir);
}
/**
* Slices the tiles.
*
* @throws IOException if one occurs
*/
public void sliceTiles() throws CancellationException, IOException {
final List<String> multi = new ArrayList<String>();
final List<Pair<String,IOException>> failed =
new ArrayList<Pair<String,IOException>>();
Pair<Integer,Integer> s;
DataArchive archive = null;
try {
archive = new DataArchive(aname);
final FileStore tcache = new ImageTileDiskCache(cdir.getAbsolutePath());
s = findImages(archive, tcache, multi, failed);
archive.close();
}
finally {
IOUtils.closeQuietly(archive);
}
// nothing to do if no images need tiling
if (multi.isEmpty()) {
logger.info("No images to tile.");
return;
}
// ensure that the tile directories exist
makeHashDirs();
final int tcount = s.first;
final int max_data_mbytes = (4*s.second) >> 20;
// fix the max heap
// This was determined empirically.
final int maxheap_estimated = (int) (1.66*max_data_mbytes + 150);
final int maxheap = Math.min(maxheap_estimated, maxheap_limit);
// slice, and cleanup on failure
try {
runSlicer(multi, s.first, maxheap);
}
catch (CancellationException e) {
cleanup();
throw e;
}
catch (IOException e) {
cleanup();
throw e;
}
}
}