/*
* Copyright (C) 2011 Alex Kuiper
*
* This file is part of PageTurner
*
* PageTurner 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.
*
* PageTurner 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 PageTurner. If not, see <http://www.gnu.org/licenses/>.*
*/
package net.nightwhistler.pageturner.epub;
import android.util.Log;
import jedi.option.Option;
import nl.siegmann.epublib.domain.Author;
import nl.siegmann.epublib.domain.Book;
import nl.siegmann.epublib.domain.Resource;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import static jedi.functional.FunctionalPrimitives.isEmpty;
import static jedi.option.Options.none;
import static jedi.option.Options.option;
import static jedi.option.Options.some;
/**
* Special spine class which handles navigation
* and provides a custom cover.
*
* @author Alex Kuiper
*
*/
public class PageTurnerSpine implements Iterable<PageTurnerSpine.SpineEntry> {
private List<SpineEntry> entries;
private List<List<Integer>> pageOffsets = new ArrayList<>();
private int position;
public static final String COVER_HREF = "PageTurnerCover";
/** How long should a cover page be to be included **/
private static final int COVER_PAGE_THRESHOLD = 1024;
private String tocHref;
/**
* Creates a new Spine from this book.
*
* @param book
*/
public PageTurnerSpine(Book book) {
this.entries = new ArrayList<>();
this.position = 0;
addResource(createCoverResource(book));
String href = null;
if ( entries.size() > 0 && ! entries.get(0).href.equals( COVER_HREF ) ) {
href = book.getCoverPage().getHref();
}
for ( int i=0; i < book.getSpine().size(); i++ ) {
Resource res = book.getSpine().getResource(i);
if ( href == null || ! (href.equals(res.getHref()))) {
addResource(res);
}
}
if ( book.getNcxResource() != null ) {
this.tocHref = book.getNcxResource().getHref();
}
}
public void setPageOffsets(List<List<Integer>> pageOffsets) {
if ( pageOffsets != null ) {
this.pageOffsets = pageOffsets;
} else {
this.pageOffsets = new ArrayList<>();
}
}
public int getTotalNumberOfPages() {
int total = 0;
for ( List<Integer> pagesPerSection: pageOffsets ) {
total += pagesPerSection.size();
}
return Math.max(0, total - 1);
}
@Override
public Iterator<SpineEntry> iterator() {
return this.entries.iterator();
}
public List<List<Integer>> getPageOffsets() {
return pageOffsets;
}
/**
* Adds a new resource.
* @param resource
*/
private void addResource( Resource resource ) {
SpineEntry newEntry = new SpineEntry();
newEntry.title = resource.getTitle();
newEntry.resource = resource;
newEntry.href = resource.getHref();
newEntry.size = (int) resource.getSize();
entries.add(newEntry);
}
/**
* Returns the number of entries in this spine.
* This includes the generated cover.
*
* @return
*/
public int size() {
return this.entries.size();
}
/**
* Navigates one entry forward.
*
* @return false if we're already at the end.
*/
public boolean navigateForward() {
if ( this.position == size() -1 ) {
return false;
}
this.position++;
return true;
}
/**
* Navigates one entry back.
*
* @return false if we're already at the start
*/
public boolean navigateBack() {
if ( this.position == 0 ) {
return false;
}
this.position--;
return true;
}
/**
* Checks if the current entry is the cover page.
*
* @return
*/
public boolean isCover() {
return this.position == 0;
}
/**
* Returns the title of the current entry,
* or null if it could not be determined.
*
* @return
*/
public Option<String> getCurrentTitle() {
if ( entries.size() > 0 ) {
return option(entries.get(position).title);
} else {
return none();
}
}
/**
* Returns the current resource, or null
* if there is none.
*
* @return
*/
public Option<Resource> getCurrentResource() {
return getResourceForIndex(position);
}
/**
* Returns the resource after the current one
* @return
*/
public Option<Resource> getNextResource() {
return getResourceForIndex(position + 1);
}
public Option<Resource> getResourceForIndex( int index ) {
if ( entries.isEmpty() || index < 0 || index >= entries.size() ) {
return none();
}
return option(entries.get(index).resource);
}
/**
* Resolves a href relative to the current resource.
*
* @param href
* @return
*/
public String resolveHref( String href ) {
Option<Resource> res = getCurrentResource();
if (! isEmpty(res) ) {
Resource actualResource = res.unsafeGet();
if ( actualResource.getHref() != null ) {
return resolveHref(href, actualResource.getHref());
}
}
return href;
}
/**
* Resolves a HREF relative to the Table of Contents
*
* @param href
* @return
*/
public String resolveTocHref( String href ) {
if ( this.tocHref != null ) {
return resolveHref(href, tocHref);
}
return href;
}
private static String resolveHref( String href, String against ) {
try {
String result = new URI(encode(against)).resolve(encode(href)).getPath();
return result;
}
catch (URISyntaxException u) {
return href;
} catch (IllegalArgumentException i) {
return href;
}
}
private static String encode(String input) {
StringBuilder resultStr = new StringBuilder();
for (char ch : input.toCharArray()) {
if ( ch == '\\' ) { //Some books use \ as a separator... invalid, but we'll try to fix it
resultStr.append('/');
} else if (isUnsafe(ch)) {
resultStr.append('%');
resultStr.append(toHex(ch / 16));
resultStr.append(toHex(ch % 16));
} else {
resultStr.append(ch);
}
}
return resultStr.toString();
}
private static char toHex(int ch) {
return (char) (ch < 10 ? '0' + ch : 'A' + ch - 10);
}
/**
* This is slightly unsafe: it lets / and % pass, making
* multiple encodes safe.
* @param ch
* @return
*/
private static boolean isUnsafe(char ch) {
if (ch > 128 || ch < 0)
return true;
return " %$&+,:;=?@<>#[]".indexOf(ch) >= 0;
}
/**
* Returns the href of the current resource.
* @return
*/
public Option<String> getCurrentHref() {
if ( entries.size() > 0 ) {
return option(entries.get(position).href);
} else {
return none();
}
}
/**
* Navigates to a specific point in the spine.
*
* @param index
* @return false if the point did not exist.
*/
public boolean navigateByIndex( int index ) {
if ( index < 0 || index >= size() ) {
return false;
}
this.position = index;
return true;
}
/**
* Returns the current position in the spine.
*
* @return
*/
public int getPosition() {
return position;
}
/**
* Navigates to the point with the given href.
*
* @param href
* @return false if that point did not exist.
*/
public boolean navigateByHref( String href ) {
String encodedHref = encode(href);
for ( int i=0; i < size(); i++ ) {
String entryHref = encode(entries.get(i).href);
if ( entryHref.equals(encodedHref)){
this.position = i;
return true;
}
}
return false;
}
/**
* Returns a percentage, which indicates how
* far the given point in the current entry is
* compared to the whole book.
*
* @param progressInPart
* @return
*/
public int getProgressPercentage(double progressInPart) {
return getProgressPercentage(getPosition(), progressInPart);
}
private int getProgressPercentage(int index, double progressInPart) {
if ( this.entries == null ) {
return -1;
}
double uptoHere = 0;
List<Double> percentages = getRelativeSizes();
for ( int i=0; i < percentages.size() && i < index; i++ ) {
uptoHere += percentages.get( i );
}
double thisPart = percentages.get(index);
double progress = uptoHere + (progressInPart * thisPart);
return (int) (progress * 100);
}
/**
* Returns the progress percentage for the given text position
* in the given index.
*
* @param index
* @param position
* @return
*/
public int getProgressPercentage(int index, int position) {
if ( this.entries == null || index >= entries.size() ) {
return -1;
}
double progressInPart = ( (double)position / (double) entries.get(index).size);
return getProgressPercentage(index, progressInPart);
}
/**
* Returns a list of doubles representing the relative size of each spine index.
* @return
*/
public List<Double> getRelativeSizes() {
int total = 0;
List<Integer> sizes = new ArrayList<>();
for ( int i=0; i < entries.size(); i++ ) {
int size = entries.get(i).size;
sizes.add(size);
total += size;
}
List<Double> result = new ArrayList<>();
for ( int i=0; i < sizes.size(); i++ ) {
double part = (double) sizes.get(i) / (double) total;
result.add( part );
}
return result;
}
private Resource createCoverResource(Book book) {
if ( book.getCoverPage() != null && book.getCoverPage().getSize() > 0
&& book.getCoverPage().getSize() < COVER_PAGE_THRESHOLD ) {
Log.d("PageTurnerSpine", "Using cover resource " + book.getCoverPage().getHref() );
return book.getCoverPage();
}
Log.d("PageTurnerSpine", "Constructing a cover page" );
Resource res = new Resource(generateCoverPage(book).getBytes(), COVER_HREF);
res.setTitle("Cover");
return res;
}
private String generateCoverPage(Book book) {
String centerpiece;
//Else we construct a basic front page with title and author.
if ( book.getCoverImage() == null ) {
centerpiece = "<center><h1>" + (book.getTitle() != null ? book.getTitle(): "Book without a title") + "</h1>";
if ( ! book.getMetadata().getAuthors().isEmpty() ) {
for ( Author author: book.getMetadata().getAuthors() ) {
centerpiece += "<h3>" + author.getFirstname() + " " + author.getLastname() + "</h3>";
}
} else {
centerpiece += "<h3>Unknown author</h3>";
}
centerpiece += "</center>";
} else {
//If the book has a cover image, we display that
centerpiece = "<img src='" + book.getCoverImage().getHref() + "'>";
}
return "<html><body>" + centerpiece + "</body></html>";
}
public class SpineEntry {
private String title;
private Resource resource;
private String href;
private int size;
public String getTitle() {
return title;
}
public int getSize() {
return size;
}
public Resource getResource() {
return resource;
}
public String getHref() {
return href;
}
}
}