/*******************************************************************************
* Copyright (c) 2011, 2013, 2014 Torkild U. Resheim.
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Torkild U. Resheim - initial API and implementation
*******************************************************************************/
package no.resheim.elibrarium.epub.ui.reader;
import java.io.File;
import java.text.MessageFormat;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import no.resheim.elibrarium.epub.core.EpubUtil;
import no.resheim.elibrarium.library.Book;
import no.resheim.elibrarium.library.Bookmark;
import no.resheim.elibrarium.library.TextAnnotation;
import no.resheim.elibrarium.library.core.ILibraryCatalog;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.emf.common.util.EList;
import org.eclipse.mylyn.docs.epub.core.Publication;
import org.eclipse.mylyn.docs.epub.opf.Item;
import org.eclipse.mylyn.docs.epub.opf.Itemref;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.browser.ProgressListener;
import org.eclipse.swt.widgets.Shell;
/**
* This type is for paginating EPUB books. It employs two threads - one
* iterating over the book chapters (or XHTML files) the other is counting the
* pages of each chapter. The latter will also inject all bookmarks and
* annotations and update the page number of these.
*
* @author Torkild U. Resheim
*/
public class PaginationJob extends Job {
private class Paginator implements Runnable, ProgressListener {
/**
* Used to synchronise the two threads working on paginating a chapter.
*/
private final CyclicBarrier barrier = new CyclicBarrier(2);
private String currentHref;
private final IProgressMonitor monitor;
private final Itemref ref;
public Paginator(Itemref ref, IProgressMonitor monitor) {
this.ref = ref;
this.monitor = monitor;
}
@Override
public void changed(ProgressEvent event) {
// ignore
}
/**
* This event is triggered every time a full HTML file has been loaded
* into the browser component. We use it to determine the number of
* pages in the chapter.
*/
@Override
public void completed(ProgressEvent event) {
if (monitor.isCanceled()) {
return;
}
browser.removeProgressListener(this);
int[] newSizes = new int[chapterSizes.length + 1];
System.arraycopy(chapterSizes, 0, newSizes, 0, chapterSizes.length);
newSizes[chapterSizes.length] = paginateChapter();
chapterSizes = newSizes;
try {
// The thread running this code is done and the other barrier
// is waiting for it.
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
/**
* Executes JavaScript that will reformat the chapter and obtain
* information that is required for browsing it.
*
* @return the number of pages in the chapter
*/
private int paginateChapter() {
int pageCount = -1;
try {
boolean ok = utility.injectJavaScript(browser);
if (ok) {
pageCount = (int) Math.round((Double) browser.evaluate("return pageCount"));
// Iterate over all bookmarks and annotations in order to
// determine their page numbers. The data
// model will be updated at this stage.
EList<Bookmark> bookmarks = book.getBookmarks();
for (Bookmark bookmark : bookmarks) {
if (bookmark.getHref() != null && bookmark.getHref().equals(currentHref)) {
String id = bookmark.getId();
if (bookmark instanceof TextAnnotation) {
int page = (int) Math.round((Double) browser.evaluate("page=markRange('"
+ bookmark.getLocation() + "','" + id + "');return page;"));
updatePageNumber(bookmark, page);
} else {
int page = (int) Math.round((Double) browser.evaluate("page=injectIdentifier('"
+ bookmark.getLocation() + "','" + id + "');return page;"));
updatePageNumber(bookmark, page);
}
}
} // for
}
} catch (Exception e) {
e.printStackTrace();
}
return pageCount;
}
private void updatePageNumber(Bookmark bookmark, int pageNumber) {
for (int chapterSize : chapterSizes) {
pageNumber += chapterSize;
}
final int page = pageNumber + 1;
ILibraryCatalog.INSTANCE.modify(bookmark, new ILibraryCatalog.ITransactionalOperation<Bookmark>() {
@Override
public Object execute(Bookmark object) {
object.cdoWriteLock();
object.setPage(page);
object.cdoWriteLock().unlock();
return null;
}
});
}
@Override
public void run() {
browser.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
browser.addProgressListener(Paginator.this);
Item item = ops.getItemById(ref.getIdref());
currentHref = item.getHref();
String filePath = ops.getRootFolder().getAbsolutePath() + File.separator + currentHref;
File file = new File(filePath);
if (file.exists()) {
String url = "file:" + filePath;
browser.setUrl(url);
} else {
monitor.setCanceled(true);
}
}
});
try {
// This thread is done, wait until the other one is done.
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
private final EpubUiUtility utility;
/**
* The book currently open in the editor.
*/
private final Book book;
/**
* A private browser instance used for paginating the EPUB.
*/
private final Browser browser;
/**
* A list of individual chapter sizes.
*/
private int[] chapterSizes = new int[0];
/**
* The OPS publication being paginated.
*/
private final Publication ops;
private final Shell shell;
/**
* The width of the page
*/
private int width;
public PaginationJob(Book book, Publication ops) {
super(MessageFormat.format("Paginating \"{0}\"", EpubUtil.getFirstTitle(ops)));
this.ops = ops;
this.book = book;
shell = new Shell();
browser = new Browser(shell, SWT.NONE);
utility = new EpubUiUtility();
}
public int[] getChapterSizes() {
// Make copy to help avoid threading issues.
int[] sizes = new int[chapterSizes.length];
System.arraycopy(chapterSizes, 0, sizes, 0, sizes.length);
return sizes;
}
/**
* Returns the total number of pages in the book.
*
* @return
*/
public int getTotalpages() {
int total = 0;
for (int size : chapterSizes) {
total += size;
}
return total;
}
/**
* Returns the width of the book page.
*
* @return the page width
*/
public int getWidth() {
return width;
}
@Override
protected IStatus run(IProgressMonitor monitor) {
long start = System.currentTimeMillis();
chapterSizes = new int[0];
EList<Itemref> refs = ops.getPackage().getSpine().getSpineItems();
ExecutorService es = Executors.newFixedThreadPool(1);
for (Itemref ref : refs) {
if (!monitor.isCanceled()) {
Paginator paginator = new Paginator(ref, monitor);
es.execute(paginator);
} else {
System.out.println("Pagination cancelled");
break;
}
}
es.shutdown();
try {
es.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Pagination done in " + (System.currentTimeMillis() - start) + "ms");
monitor.done();
return Status.OK_STATUS;
}
public synchronized void update(int width, int height) {
if (width == 0 || height == 0) {
throw new IllegalArgumentException("Height or width cannot be 0");
}
if (getState() == RUNNING) {
return;
}
browser.setSize(width, height);
this.schedule();
}
}