text:
package pizan.gwt.client;

import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;

import rosa.gwt.common.client.EmbeddedObjectViewer;
import rosa.gwt.common.client.FSIService;
import rosa.gwt.common.client.HttpGet;
import rosa.gwt.common.client.Searcher;
import rosa.gwt.common.client.Searcher.UserField;
import rosa.gwt.common.client.TranscriptionViewer;
import rosa.gwt.common.client.Util;
import rosa.gwt.common.client.codexview.CodexController;
import rosa.gwt.common.client.codexview.CodexImage;
import rosa.gwt.common.client.codexview.CodexModel;
import rosa.gwt.common.client.codexview.CodexOpening;
import rosa.gwt.common.client.codexview.CodexView;
import rosa.gwt.common.client.codexview.CodexView.Mode;
import rosa.gwt.common.client.codexview.RoseBook;
import rosa.gwt.common.client.codexview.SimpleCodexController;
import rosa.gwt.common.client.data.Book;
import rosa.gwt.common.client.data.ImageTagging;
import rosa.gwt.common.client.data.Repository;
import rosa.gwt.common.client.dynimg.FsiImageServer;
import rosa.gwt.common.client.dynimg.ImageServer;
import rosa.gwt.common.client.resource.Labels;
import rosa.search.SearchResult;
import rosa.search.SearchResult.SearchMatch;

import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.http.client.URL;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.resources.client.ExternalTextResource;
import com.google.gwt.resources.client.ResourceCallback;
import com.google.gwt.resources.client.ResourceException;
import com.google.gwt.resources.client.TextResource;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.cellview.client.CellList;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.DecoratedPopupPanel;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.DockLayoutPanel;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.InsertPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.MenuBar;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.RootLayoutPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SplitLayoutPanel;
import com.google.gwt.user.client.ui.TabLayoutPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SingleSelectionModel;

public class App implements EntryPoint {
    public static final String LC;
    private static final String DEFAULT_LC = "en";

    static {
        String s = LocaleInfo.getCurrentLocale().getLocaleName();
        LC = s.equals("default") ? DEFAULT_LC : s;
    }

    // TODO move all of this to properties file, take advantage of lc
    // TODO put in Config.properties

    private static final String PIZAN_FSI_SHARE = "pizan";
    private static final int MAX_SEARCH_RESULTS = 20;
    private static final String DATA_PATH = "data/";
    private static final int MIN_BOOK_READER_WIDTH = 600;
    private static final int MAX_BOOK_READER_WIDTH = 800;
    private static final int MIN_BOOK_BROWSER_WIDTH = 400;
    private static final int MAX_BOOK_BROWSER_WIDTH = 600;
    private static final int VIEWPORT_RESIZE_INCREMENT = 100;
    private static final String HELP_PATH = "/help/help_";

    private static final String BUG_SUBMIT_EMAIL = "contactus@romandelarose.org";

    private static final String SEARCH_BOOK_RESTRICT_KEY = "BOOK";

    private final rosa.gwt.common.client.SearchServiceAsync searchservice = GWT
            .create(rosa.gwt.common.client.SearchService.class);

    private Searcher searcher;
    private Repository col;
    private Book book; // currently viewed Book

    private MenuBar headermenu;
    private Panel content;
    private Panel sidebar;
    private int selectedImageIndex = -1; // currently selected image of book

    // Loaded on demand
    private String[][] charnames_variants = new String[][] {};

    private LoadingDialog loadingdialog = new LoadingDialog();

    private boolean use_flash;

    private DialogBox page_turner_annotation;

    private void browseBook(int image) {
        if (use_flash) {
            browseBookFSI(image);
        } else {
            useBuiltinImageViewer(CodexView.Mode.IMAGE_BROWSER, image);
        }
    }

    private void browseBookFSI(int image) {
        String title = Labels.INSTANCE.browseImages() + ": " + col.fullBookName(book.bookDataIndex());
        initDisplay(title, true);

        updateSelectedImage(image);

        EmbeddedObjectViewer.DisplayCallback cb = new EmbeddedObjectViewer.DisplayCallback() {
            public String display(String width, String height) {
                return FSIService.embedFSIShowcase(book.id(), translateBookIndexToShowcaseIndex(selectedImageIndex),
                        width, height, LC);
            }
        };
        EmbeddedObjectViewer viewer = new EmbeddedObjectViewer(cb, VIEWPORT_RESIZE_INCREMENT, MIN_BOOK_BROWSER_WIDTH,
                MAX_BOOK_BROWSER_WIDTH, 4.0 / 3.0, title);

        InsertPanel toolbar = viewer.toolbar();

        final TextBox gobox = new TextBox();
        gobox.setStylePrimaryName("GoTextBox");
        toolbar.insert(gobox, 0);

        gobox.addKeyDownHandler(new KeyDownHandler() {
            public void onKeyDown(KeyDownEvent event) {
                if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
                    int image = book.guessImage(gobox.getText());

                    if (image != -1) {
                        FSIService.fsishowcaseSelectImage(translateBookIndexToShowcaseIndex(image));
                    }
                }
            }
        });

        FSIService.setupFSIShowcaseCallback(new FSIService.FSIShowcaseCallback() {
            public void imageSelected(int index) {
                selectedImageIndex = index;
                String image = book.imageName(selectedImageIndex);
                image = Book.shortImageName(image);
                gobox.setText(image);
            }
        });

        content.add(viewer);
        content.add(new HTML(book.imagePermissionStatement()));
    }

    private int translateBookIndexToShowcaseIndex(int bookindex) {
        int i = 0;

        for (int j = 0; j < book.numImages(); j++) {
            String image = book.imageName(j);

            if (--bookindex < 0) {
                break;
            }

            if (!Book.isMissingImage(image)) {
                i++;
            }
        }

        return i;
    }

    private Panel createSearchResultsNav(int page, int numpages, String tokenqueryprefix) {
        HorizontalPanel nav = new HorizontalPanel();
        nav.setStylePrimaryName("ResultsNav");
        nav.setSpacing(2);

        if (numpages > 1) {
            if (page > 0) {
                int offset = (page - 1) * MAX_SEARCH_RESULTS;
                Hyperlink h = new Hyperlink(Labels.INSTANCE.previous(), tokenqueryprefix + ';' + offset);
                nav.add(h);
            }

            for (int i = 0; i < numpages; i++) {
                if (i == page) {
                    nav.add(contentHeader("" + (i + 1)));
                } else {
                    int offset = i * MAX_SEARCH_RESULTS;
                    Hyperlink h = new Hyperlink("" + (i + 1), tokenqueryprefix + ';' + offset);
                    nav.add(h);
                }
            }

            if (page < numpages - 1) {
                int offset = (page + 1) * MAX_SEARCH_RESULTS;
                Hyperlink h = new Hyperlink(Labels.INSTANCE.next(), tokenqueryprefix + ';' + offset);
                nav.add(h);
            }
        }

        return nav;
    }

    private void displaySearchResults(Panel panel, String tokenqueryprefix, SearchResult result) {
        int numpages = result.total / MAX_SEARCH_RESULTS;

        if (result.total % MAX_SEARCH_RESULTS != 0) {
            numpages++;
        }

        int page = result.offset / MAX_SEARCH_RESULTS;

        Panel nav = createSearchResultsNav(page, numpages, tokenqueryprefix);
        panel.add(nav);

        Grid grid = new Grid((result.matches.length / 2) + (result.matches.length % 2), 4);

        for (int i = 0; i < result.matches.length; i++) {
            SearchMatch m = result.matches[i];

            int resultrow = i / 2;
            int resultcol = (i % 2) * 2;

            String thumbhtml;
            String imagename;
            String bookid;

            if (m.loc.contains(".")) {
                bookid = Book.bookIDFromImage(m.loc);
                thumbhtml = FSIService.embedStaticImage(PIZAN_FSI_SHARE, m.loc + ".tif", 64, 64);
                imagename = m.loc.substring(m.loc.indexOf('.') + 1);
            } else {
                bookid = m.loc;
                imagename = null;
                thumbhtml = FSIService.embedStaticImage(PIZAN_FSI_SHARE, m.loc + ".binding.frontcover.tif", 64, 64);
            }

            grid.setWidget(resultrow, resultcol, new Hyperlink(thumbhtml, true, Action.READ_BOOK.toToken(m.loc)));

            FlowPanel desc = new FlowPanel();
            grid.setWidget(resultrow, resultcol + 1, desc);

            String resultname = (imagename == null ? "" : imagename + ": ")
                    + col.fullBookName(col.findBookByID(bookid));

            desc.add(new Anchor(resultname, "#" + Action.READ_BOOK.toToken(m.loc)));

            StringBuilder context = new StringBuilder();

            for (int j = 0; j < m.snippets.size();) {
                String field = m.snippets.get(j++);
                String snippet = m.snippets.get(j++);

                Searcher.UserField uf = Searcher.UserField.findByLuceneField(field);
                field = uf == null ? field : uf.display;

                context.append("" + field + ":");
                context.append(" " + snippet);
                if (j != m.snippets.size()) {
                    context.append(", ");
                }
                context.append("");
            }

            desc.add(new HTML(context.toString()));
        }

        panel.add(grid);
        panel.add(createSearchResultsNav(page, numpages, tokenqueryprefix));
    }

    // Fills in values if non-null

    private Widget createAdvancedSearchWidget(UserField[] userfields, String[] userqueries, String[] restrictedbookids) {
        Panel panel = new FlowPanel();

        Button search = new Button(Labels.INSTANCE.search());

        // panel.setSpacing(5);

        final FlexTable table = new FlexTable();

        Button add = new Button(Labels.INSTANCE.addSearchField());

        panel.add(table);
        panel.add(add);

        final ListBox chosenbooks = new ListBox();

        // Called to search filled in query

        final ClickHandler searchlistener = new ClickHandler() {
            public void onClick(ClickEvent event) {
                // Build up search history token

                String[] data = new String[(table.getRowCount() * 2) + 1 + (chosenbooks.getItemCount() > 0 ? 2 : 0)];
                int dataindex = 0;
                boolean emptyquery = true;

                for (int i = 0; i < table.getRowCount(); i++) {
                    ListBox lb = (ListBox) table.getWidget(i, 0);
                    TextBox tb = (TextBox) table.getWidget(i, 1);

                    int sel = lb.getSelectedIndex();

                    if (sel != -1) {
                        String userquery = tb.getText().trim();
                        String userfield = Searcher.UserField.values()[sel].name();

                        if (userquery.isEmpty()) {
                            userfield = null;
                            userquery = null;
                        } else {
                            emptyquery = false;
                        }

                        data[dataindex++] = userfield;
                        data[dataindex++] = userquery;
                    }
                }

                if (chosenbooks.getItemCount() > 0) {
                    data[dataindex++] = SEARCH_BOOK_RESTRICT_KEY;

                    StringBuilder sb = new StringBuilder();
                    int len = chosenbooks.getItemCount();
                    for (int i = 0; i < len; i++) {
                        sb.append(chosenbooks.getValue(i));

                        if (i != len - 1) {
                            sb.append(',');
                        }
                    }

                    data[dataindex++] = sb.toString();
                }

                data[dataindex] = "0";

                if (!emptyquery) {
                    History.newItem(Action.SEARCH.toToken(data));
                }
            }
        };

        ClickHandler addlistener = new ClickHandler() {
            public void onClick(ClickEvent event) {
                int row = table.getRowCount();

                table.setWidget(row, 0, createAdvancedSearchFieldSelector());

                TextBox tb = new TextBox();
                table.setWidget(row, 1, tb);

                tb.addKeyDownHandler(new KeyDownHandler() {
                    public void onKeyDown(KeyDownEvent event) {
                        if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
                            searchlistener.onClick(null);
                        }
                    }
                });

                final Button remove = new Button(Labels.INSTANCE.removeSearchField());

                table.setWidget(row, 2, remove);

                remove.addClickHandler(new ClickHandler() {
                    public void onClick(ClickEvent event) {
                        for (int row = 0; row < table.getRowCount(); row++) {
                            if (table.getWidget(row, 2) == remove) {
                                table.removeRow(row);
                            }
                        }
                    }
                });
            }
        };

        add.addClickHandler(addlistener);

        if (userfields != null) {
            for (int i = 0; i < userfields.length; i++) {
                if (userfields[i] == null) {
                    continue;
                }

                int row = table.getRowCount();
                addlistener.onClick(null);

                ListBox lb = (ListBox) table.getWidget(row, 0);
                lb.setItemSelected(userfields[i].ordinal(), true);
                TextBox tb = (TextBox) table.getWidget(row, 1);
                tb.setText(userqueries[i]);
            }
        } else {
            addlistener.onClick(null);
            addlistener.onClick(null);
            addlistener.onClick(null);
        }

        final ListBox availbooks = new ListBox();
        final Button clearbutton = new Button(Labels.INSTANCE.clearTextBox());

        HorizontalPanel hp = new HorizontalPanel();
        hp.setSpacing(2);

        hp.add(chosenbooks);
        hp.add(clearbutton);
        hp.setCellVerticalAlignment(clearbutton, HasVerticalAlignment.ALIGN_BOTTOM);

        panel.add(availbooks);
        panel.add(hp);
        panel.add(search);

        chosenbooks.setVisibleItemCount(5);
        availbooks.setVisibleItemCount(1);

        search.addClickHandler(searchlistener);

        availbooks.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                int sel = availbooks.getSelectedIndex();

                if (sel > 0) {
                    chosenbooks.setVisible(true);
                    clearbutton.setVisible(true);

                    chosenbooks.addItem(availbooks.getItemText(sel), availbooks.getValue(sel));
                    availbooks.removeItem(sel);
                }
            }
        });

        chosenbooks.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                int sel = chosenbooks.getSelectedIndex();
                availbooks.addItem(chosenbooks.getItemText(sel), chosenbooks.getValue(sel));
                chosenbooks.removeItem(sel);

                if (chosenbooks.getItemCount() == 0) {
                    clearbutton.click();
                }
            }
        });

        ClickHandler clearlistener = new ClickHandler() {
            public void onClick(ClickEvent event) {
                chosenbooks.clear();
                chosenbooks.setVisible(false);
                clearbutton.setVisible(false);

                availbooks.clear();
                availbooks.addItem(Labels.INSTANCE.restrictByBook());

                for (int i = 0; i < col.numBooks(); i++) {
                    availbooks.addItem(col.fullBookName(i), col.bookData(i, Repository.Category.ID));
                }
            }
        };

        clearbutton.addClickHandler(clearlistener);
        clearlistener.onClick(null);

        if (restrictedbookids != null) {
            for (String bookid : restrictedbookids) {
                for (int i = 0; i < availbooks.getItemCount(); i++) {
                    if (availbooks.getValue(i).equals(bookid)) {
                        chosenbooks.setVisible(true);
                        clearbutton.setVisible(true);
                        chosenbooks.addItem(availbooks.getItemText(i), bookid);
                        availbooks.removeItem(i);
                        break;
                    }
                }
            }
        }

        return panel;
    }

    private ListBox createAdvancedSearchFieldSelector() {
        ListBox lb = new ListBox();

        for (Searcher.UserField uf : Searcher.UserField.values()) {
            lb.addItem(uf.display);
        }

        return lb;
    }

    private Widget createSearchWidget() {
        FlowPanel top = new FlowPanel();

        top.setStylePrimaryName("Search");

        final TextBox querybox = new TextBox();

        Button search = new Button(Labels.INSTANCE.search());
        top.add(querybox);
        top.add(search);

        Hyperlink adv = new Hyperlink(Labels.INSTANCE.advancedSearch(), Action.SEARCH.toToken());
        top.add(adv);

        search.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                String s = querybox.getText().trim();

                if (s.length() > 0) {
                    History.newItem(Action.SEARCH.toToken(Searcher.UserField.ALL.name(), s, "0"));
                }
            }
        });

        querybox.addKeyDownHandler(new KeyDownHandler() {
            public void onKeyDown(KeyDownEvent event) {
                if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
                    String s = querybox.getText().trim();

                    if (s.length() > 0) {
                        History.newItem(Action.SEARCH.toToken(Searcher.UserField.ALL.name(), s, "0"));
                    }
                }
            }
        });

        return top;
    }

    private void loadBook(String bookid, final String nexttoken) {
        int bookindex = col.findBookByID(bookid);

        loadingdialog.display();

        HttpGet.Callback<Book> cb = new HttpGet.Callback<Book>() {
            public void failure(String error) {
                loadingdialog.error(error);
            }

            public void success(Book result) {
                loadingdialog.hide();
                book = result;
                col.loadedBooks().put(book.id(), book);
                historyChanged(nexttoken);
            }
        };

        Book.load(GWT.getHostPageBaseURL() + DATA_PATH, bookid, bookindex, LC, cb);
    }

    private void handleHistoryTokenError(String token) {
        Window.alert("Invalid location: " + token);
        viewHome();
    }

    private void historyChanged(String token) {
        // TODO hack
        page_turner_annotation.hide();

        if (token.isEmpty()) {
            viewHome();
            return;
        }

        Action state = Action.fromToken(token);
        List<String> args = Action.tokenArguments(token);

        if (state == null) {
            handleHistoryTokenError(token);
            return;
        }

        if (state == Action.HOME) {
            viewHome();
        } else if (state == Action.VIEW_PARTNERS) {
            viewPage(Labels.INSTANCE.partners(), Resources.INSTANCE.partnersHtml());
        } else if (state == Action.VIEW_TERMS) {
            viewPage(Labels.INSTANCE.termsAndConditions(), Resources.INSTANCE.termsAndConditionsHtml());
        } else if (state == Action.VIEW_CONTACT) {
            viewPage(Labels.INSTANCE.contactUs(), Resources.INSTANCE.contactHtml());
        } else if (state == Action.VIEW_PROPER_NAMES) {
            viewPage("Proper names", Resources.INSTANCE.properNamesHtml());
        } else if (state == Action.VIEW_WORKS) {
            viewPage("Works", Resources.INSTANCE.worksHtml());
        } else if (state == Action.VIEW_PIZAN) {
            viewPage("Christine de Pizan", Resources.INSTANCE.christineDePizanHtml());
        } else if (state == Action.VIEW_BOOK) {
            if (args.size() != 1) {
                handleHistoryTokenError(token);
                return;
            }

            String bookid = args.get(0);

            if (col.findBookByID(bookid) == -1) {
                handleHistoryTokenError(token);
                return;
            }

            if (selectBook(bookid, token)) {
                return;
            } else {
                viewBook();
            }
        } else if (state == Action.READ_BOOK) {
            if (args.size() != 1) {
                handleHistoryTokenError(token);
                return;
            }

            String bookid = args.get(0);
            String image = null;

            int i = bookid.indexOf('.');

            if (i != -1) {
                image = bookid.substring(i + 1);
                bookid = bookid.substring(0, i);
            }

            if (col.findBookByID(bookid) == -1) {
                handleHistoryTokenError(token);
                return;
            }

            if (selectBook(bookid, token)) {
                return;
            } else {
                readBook(image == null ? -1 : book.guessImage(image));
            }
        } else if (state == Action.BROWSE_BOOK) {
            if (args.size() != 1) {
                handleHistoryTokenError(token);
                return;
            }

            String bookid = args.get(0);
            String image = null;

            int i = bookid.indexOf('.');

            if (i != -1) {
                image = bookid.substring(i + 1);
                bookid = bookid.substring(0, i);
            }

            if (col.findBookByID(bookid) == -1) {
                handleHistoryTokenError(token);
            }

            if (selectBook(bookid, token)) {
                return;
            } else {
                browseBook(image == null ? -1 : book.guessImage(image));
            }
        } else if (state == Action.SELECT_BOOK) {
            if (args.size() != 1) {
                handleHistoryTokenError(token);
                return;
            }

            Repository.Category cat = null;

            for (Repository.Category d : Repository.Category.values()) {
                if (d.name().equals(args.get(0))) {
                    cat = d;
                }
            }

            if (cat == null) {
                handleHistoryTokenError(token);
                return;
            }

            viewBookPicker(cat);
        } else if (state == Action.SEARCH) {
            if (args.size() == 0) {
                viewSearch(null, null, null, 0, null);
                return;
            }

            if (args.size() < 3 || (args.size() & 1) == 0) {
                handleHistoryTokenError(token);
                return;
            }

            int offset;

            try {
                offset = Integer.parseInt(args.get(args.size() - 1));
            } catch (NumberFormatException e) {
                handleHistoryTokenError(token);
                return;
            }

            String[] userqueries = new String[(args.size() - 1) / 2];
            Searcher.UserField[] userfields = new Searcher.UserField[userqueries.length];
            String[] bookrestrict = null;

            int argindex = 0;
            for (int i = 0; i < userqueries.length; i++) {
                userfields[i] = null;

                if (SEARCH_BOOK_RESTRICT_KEY.equals(args.get(argindex))) {
                    argindex++;
                    bookrestrict = args.get(argindex++).split(",");

                    for (String bookid : bookrestrict) {
                        if (col.findBookByID(bookid) == -1) {
                            handleHistoryTokenError(token);
                        }
                    }
                } else {
                    for (Searcher.UserField uf : Searcher.UserField.values()) {
                        if (uf.name().equals(args.get(argindex))) {
                            userfields[i] = uf;
                        }
                    }

                    if (userfields[i] == null) {
                        handleHistoryTokenError(token);
                        return;
                    }

                    argindex++;
                    userqueries[i] = args.get(argindex++);
                }
            }

            String searchtokenprefix = token.substring(0, token.lastIndexOf(';'));

            viewSearch(userfields, userqueries, bookrestrict, offset, searchtokenprefix);
        } else {
            handleHistoryTokenError(token);
            return;
        }

        Analytics.track(state, book == null ? null : book.id(), args);
    }

    private void viewPage(String label, ExternalTextResource html) {
        book = null;
        initDisplay(label, false);

        addHtml(content, html);
    }

    /**
     * Add external html resource to a panel. On success also add the given
     * widgets.
     *
     * @param panel
     * @param html
     * @param widgets
     */
    private static void addHtml(final Panel panel, ExternalTextResource html, final Widget... widgets) {
        try {
            html.getText(new ResourceCallback<TextResource>() {
                public void onSuccess(TextResource resource) {
                    panel.add(new HTML(resource.getText()));

                    for (Widget w : widgets) {
                        panel.add(w);
                    }
                }

                public void onError(ResourceException e) {
                    reportInternalError("Failed to load external resource", e);
                }
            });
        } catch (ResourceException e) {
            reportInternalError("Failed to load external resource", e);
        }
    }

    private static void reportInternalError(String message, Throwable e) {
        Window.alert("This is likely a bug. Please report to "
                + BUG_SUBMIT_EMAIL
                + ". Include your operating system and version, browser and version, url you were visiting, and what you did to trigger the problem. \nInternal error: "
                + message + "\n" + e.getMessage() + (e.getCause() == null ? "" : "\nCaused by: " + e.getCause()));
    }

    private static Image getImage(String name, String alt) {
        Image img = new Image(GWT.getModuleBaseURL() + name);

        img.setAltText(alt);

        return img;
    }

    public void onModuleLoad() {
        GWT.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
            public void onUncaughtException(Throwable e) {
                reportInternalError("Uncaught exception", e);
                viewHome();
            }
        });

        Resources.INSTANCE.css().ensureInjected();

        DockLayoutPanel dock = new DockLayoutPanel(Unit.PX);

        FlowPanel header = new FlowPanel();

        headermenu = new MenuBar();
        headermenu.setAutoOpen(true);
        headermenu.setAnimationEnabled(true);

        Image headerimg = getImage("header-5.jpg", "Christine de Pizan Digital Scriptorium");

        headerimg.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                History.newItem(Action.HOME.toToken());
            }
        });

        header.add(headerimg);
        header.add(headermenu);

        // TODO labels

        headermenu.addItem("Home", new ScheduledCommand() {
            public void execute() {
                History.newItem(Action.HOME.toToken());
            }
        });

        headermenu.addItem("Who is Christine de Pizan?", new ScheduledCommand() {
            public void execute() {
                History.newItem(Action.VIEW_PIZAN.toToken());
            }
        });

        headermenu.addItem("List of Works", new ScheduledCommand() {
            public void execute() {
                History.newItem(Action.VIEW_WORKS.toToken());
            }
        });

        headermenu.addItem("Proper Names", new ScheduledCommand() {
            public void execute() {
                History.newItem(Action.VIEW_PROPER_NAMES.toToken());
            }
        });

        MenuBar projectmenu = new MenuBar(true);

        projectmenu.addItem("Partners", new ScheduledCommand() {
            public void execute() {
                History.newItem(Action.VIEW_PARTNERS.toToken());
            }
        });

        projectmenu.addItem("Terms & Conditions", new ScheduledCommand() {
            public void execute() {
                History.newItem(Action.VIEW_TERMS.toToken());
            }
        });

        projectmenu.addItem("Contact us", new ScheduledCommand() {
            public void execute() {
                History.newItem(Action.VIEW_CONTACT.toToken());
            }
        });

        headermenu.addItem("Project", projectmenu);

        headermenu.addItem("Help", new ScheduledCommand() {
            public void execute() {
                Util.popupWindowURL("help", 700, 600, HELP_PATH + LC + ".html",
                        "toolbar=yes,menubar=no,scrollbars=yes,resizable=yes");
                Analytics.trackEvent("Page", "view", "help");
            }
        });

        content = new FlowPanel();
        sidebar = new FlowPanel();

        header.setStylePrimaryName("Header");
        headermenu.setStylePrimaryName("HeaderMenu");
        projectmenu.setStylePrimaryName("HeaderProjectMenu");
        content.setStylePrimaryName("Content");
        sidebar.setStylePrimaryName("Sidebar");
        dock.setStylePrimaryName("Main");

        dock.addNorth(header, 150);
        dock.addEast(new ScrollPanel(sidebar), 181);

        ScrollPanel contentscroll = new ScrollPanel(content);
        contentscroll.setStylePrimaryName("MainContentScroll");

        dock.add(contentscroll);

        FlowPanel footer = new FlowPanel();
        footer.setStylePrimaryName("Footer");
        footer.add(createSearchWidget());

        dock.addSouth(footer, 40);

        RootLayoutPanel.get().add(dock);

        History.addValueChangeHandler(new ValueChangeHandler<String>() {
            public void onValueChange(ValueChangeEvent<String> event) {
                historyChanged(event.getValue());
            }
        });

        page_turner_annotation = new DialogBox(false, false);
        page_turner_annotation.setAnimationEnabled(true);

        searcher = new Searcher(searchservice);
        col = new Repository(Util.parseCSVTable(Resources.INSTANCE.bookBrowseTable().getText()));
        use_flash = browserSupportsFlash();

        History.fireCurrentHistoryState();
    }

    private boolean browserSupportsFlash() {
        String agent = Window.Navigator.getUserAgent();

        if (agent.contains("iPad") || agent.contains("iPhone") || agent.contains("iPod")) {
            return false;
        }

        return true;
    }

    private void updateSelectedImage(int image) {
        if (image == -1) {
            for (int i = 0; i < book.numImages(); i++) {
                if (!Book.isMissingImage(book.imageName(i))) {
                    selectedImageIndex = i;
                    break;
                }
            }
        } else {
            selectedImageIndex = image;
        }
    }

    private void readBook(int image) {
        if (use_flash) {
            readBookFSIViewer(image);
        } else {
            useBuiltinImageViewer(CodexView.Mode.PAGE_TURNER, image);
        }
    }

    private void useBuiltinImageViewer(CodexView.Mode mode, int image) {
        String title = Labels.INSTANCE.pageTurner() + ": " + col.fullBookName(book.bookDataIndex());
        initDisplay(title, true);

        updateSelectedImage(image);

        Panel toolbar = new FlowPanel();

        final Panel reader_toolbar = new FlowPanel();

        final TextBox gobox = new TextBox();
        gobox.setStylePrimaryName("GoTextBox");

        Button reader_button = new Button(Labels.INSTANCE.pageTurner());
        Button browser_button = new Button(Labels.INSTANCE.browseImages());

        toolbar.add(reader_button);
        toolbar.add(browser_button);

        RoseBook rose_book = new RoseBook(PIZAN_FSI_SHARE, book.imagesTable());
        final CodexModel rose_book_model = rose_book.model();
        final CodexController rose_book_ctrl = new SimpleCodexController(rose_book_model);
        ImageServer img_server = new FsiImageServer("http://fsiserver.library.jhu.edu/server");

        ScrollPanel content_scroll = (ScrollPanel) content.getParent();

        final CodexView rose_book_view = new CodexView(img_server, rose_book_model, rose_book_ctrl, content_scroll);

        Button next = new Button(Labels.INSTANCE.next());
        Button first = new Button(Labels.INSTANCE.first());
        Button last = new Button(Labels.INSTANCE.last());
        Button prev = new Button(Labels.INSTANCE.previous());

        next.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                rose_book_ctrl.gotoNextOpening();
            }
        });

        prev.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                rose_book_ctrl.gotoPreviousOpening();
            }
        });

        first.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                rose_book_ctrl.gotoOpening(rose_book_model.opening(0));
            }
        });

        last.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                rose_book_ctrl.gotoOpening(rose_book_model.opening(rose_book_model.numOpenings() - 1));
            }
        });

        reader_toolbar.add(first);
        reader_toolbar.add(prev);
        reader_toolbar.add(gobox);
        reader_toolbar.add(next);
        reader_toolbar.add(last);

        reader_button.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                reader_toolbar.setVisible(true);
                rose_book_view.setMode(Mode.PAGE_TURNER);
            }
        });

        browser_button.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                reader_toolbar.setVisible(false);
                page_turner_annotation.hide();
                rose_book_view.setMode(Mode.IMAGE_BROWSER);
            }
        });

        gobox.addKeyDownHandler(new KeyDownHandler() {
            public void onKeyDown(KeyDownEvent event) {
                if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
                    int index = book.guessImage(gobox.getText());

                    if (index != -1) {
                        index /= 2;

                        if (index < rose_book_model.numOpenings()) {
                            rose_book_ctrl.gotoOpening(rose_book_model.opening(index));
                        }
                    }
                }
            }
        });

        final ListBox sidechoice = new ListBox();
        sidechoice.setVisibleItemCount(1);

        sidechoice.addItem(Labels.INSTANCE.show());

        if (!col.bookData(book.bookDataIndex(), Repository.Category.TRANSCRIPTION).equals(Labels.INSTANCE.none())) {
            sidechoice.addItem(Labels.INSTANCE.transcription());
            sidechoice.addItem(Labels.INSTANCE.transcription() + " [" + Labels.INSTANCE.lecoy() + "]");
        }

        if (!col.bookData(book.bookDataIndex(), Repository.Category.ILLUSTRATION_TAGGING)
                .equals(Labels.INSTANCE.none())) {
            sidechoice.addItem(Labels.INSTANCE.illustrationDescription());
        }

        if (!col.bookData(book.bookDataIndex(), Repository.Category.NARRATIVE_TAGGING).equals(Labels.INSTANCE.none())) {
            sidechoice.addItem(Labels.INSTANCE.narrativeSections());
        }

        final ChangeHandler sidechoicelistener = new ChangeHandler() {
            public void onChange(ChangeEvent event) {
                String choice = sidechoice.getItemText(sidechoice.getSelectedIndex());

                if (!page_turner_annotation.isShowing()) {
                    page_turner_annotation.setPopupPosition(content.getAbsoluteLeft() + rose_book_view.getUsedWidth()
                            + 2, content.getAbsoluteTop());
                }

                if (choice.startsWith(Labels.INSTANCE.transcription())) {
                    boolean lecoy = choice.contains(Labels.INSTANCE.lecoy());

                    page_turner_annotation.setText(Labels.INSTANCE.transcription());
                    page_turner_annotation.setWidget(displayTranscription(lecoy));
                    page_turner_annotation.show();

                    Analytics.trackEvent("Book", "display-trans", book.id());
                } else if (choice.equals(Labels.INSTANCE.illustrationDescription())) {
                    page_turner_annotation.setText(Labels.INSTANCE.illustrationDescription());
                    page_turner_annotation.clear();
                    displayIllustrationKeywordsOnRight(page_turner_annotation);
                    page_turner_annotation.show();

                    Analytics.trackEvent("Book", "display-illus-tags", book.id());
                } else {
                    page_turner_annotation.hide();
                }
            }
        };

        sidechoice.addChangeHandler(sidechoicelistener);

        if (sidechoice.getItemCount() > 1) {
            reader_toolbar.add(sidechoice);
        }

        rose_book_ctrl.addChangeHandler(new CodexController.ChangeHandler() {
            public void viewChanged(List<CodexImage> view) {
                reader_toolbar.setVisible(false);
                page_turner_annotation.hide();

                if (view.size() == 1) {
                    CodexImage img = view.get(0);
                    selectedImageIndex = book.findImage(img.label());
                }
            }

            public void openingChanged(CodexOpening opening) {
                reader_toolbar.setVisible(true);

                selectedImageIndex = opening.position() * 2;
                gobox.setText(opening.label());

                // update side display
                sidechoicelistener.onChange(null);
            }
        });

        content.add(toolbar);
        content.add(rose_book_view);
        content.add(reader_toolbar);
        content.add(new HTML(book.imagePermissionStatement()));

        if (mode == Mode.IMAGE_BROWSER) {
            reader_toolbar.setVisible(false);
        } else if (mode == Mode.PAGE_TURNER) {
            reader_toolbar.setVisible(true);

            int index = selectedImageIndex / 2;

            if (index < rose_book_model.numOpenings()) {
                rose_book_ctrl.gotoOpening(rose_book_model.opening(index));
            } else {
                rose_book_ctrl.gotoOpening(rose_book_model.opening(0));
            }
        } else if (mode == Mode.IMAGE_VIEWER) {
            if (selectedImageIndex < rose_book_model.numImages()) {
                rose_book_ctrl.setView(rose_book_model.image(selectedImageIndex));
            } else {
                rose_book_ctrl
                        .setView(rose_book_model.nonOpeningImage(selectedImageIndex - rose_book_model.numImages()));
            }
        }

        rose_book_view.setMode(mode);
    }

    // TODO stack layout panel for menu choices

    private void readBookFSIViewer(int image) {
        String title = Labels.INSTANCE.pageTurner() + ": " + col.fullBookName(book.bookDataIndex());
        initDisplay(title, true);

        updateSelectedImage(image);

        EmbeddedObjectViewer.DisplayCallback cb = new EmbeddedObjectViewer.DisplayCallback() {
            public String display(String width, String height) {
                return FSIService.embedFSIPages(book.id(), selectedImageIndex, width, height, LC);
            }
        };

        EmbeddedObjectViewer viewer = new EmbeddedObjectViewer(cb, VIEWPORT_RESIZE_INCREMENT, MIN_BOOK_READER_WIDTH,
                MAX_BOOK_READER_WIDTH, 3.0 / 4.0, title);
        InsertPanel toolbar = viewer.toolbar();

        final TextBox gobox = new TextBox();
        gobox.setStylePrimaryName("GoTextBox");

        toolbar.insert(gobox, 0);

        gobox.addKeyDownHandler(new KeyDownHandler() {
            public void onKeyDown(KeyDownEvent event) {
                if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
                    int index = book.guessImage(gobox.getText());

                    if (index != -1) {
                        FSIService.fsipagesGotoImage(index + 1);
                    }
                }
            }
        });

        final ComplexPanel display = new HorizontalPanel();
        display.add(viewer);

        final ListBox sidechoice = new ListBox();
        sidechoice.setVisibleItemCount(1);

        sidechoice.addItem(Labels.INSTANCE.show());

        if (!col.bookData(book.bookDataIndex(), Repository.Category.TRANSCRIPTION).equals(Labels.INSTANCE.none())) {
            sidechoice.addItem(Labels.INSTANCE.transcription());
            sidechoice.addItem(Labels.INSTANCE.transcription() + " [" + Labels.INSTANCE.lecoy() + "]");
        }

        if (!col.bookData(book.bookDataIndex(), Repository.Category.ILLUSTRATION_TAGGING)
                .equals(Labels.INSTANCE.none())) {
            sidechoice.addItem(Labels.INSTANCE.illustrationDescription());
        }

        if (!col.bookData(book.bookDataIndex(), Repository.Category.NARRATIVE_TAGGING).equals(Labels.INSTANCE.none())) {
            sidechoice.addItem(Labels.INSTANCE.narrativeSections());
        }

        final ChangeHandler sidechoicelistener = new ChangeHandler() {
            public void onChange(ChangeEvent event) {
                // Should only be page turner and metadata display in hp
                while (display.getWidgetCount() > 1) {
                    display.remove(1);
                }

                String choice = sidechoice.getItemText(sidechoice.getSelectedIndex());

                if (choice.startsWith(Labels.INSTANCE.transcription())) {
                    boolean lecoy = choice.contains(Labels.INSTANCE.lecoy());

                    display.add(displayTranscription(lecoy));
                    Analytics.trackEvent("Book", "display-trans", book.id());
                } else if (choice.equals(Labels.INSTANCE.illustrationDescription())) {
                    displayIllustrationKeywordsOnRight(display);
                    Analytics.trackEvent("Book", "display-illus-tags", book.id());
                }
            }
        };

        sidechoice.addChangeHandler(sidechoicelistener);

        FSIService.setupFSIPagesCallback(new FSIService.FSIPagesCallback() {
            public void pageChanged(int page) {
                selectedImageIndex = page;

                if (page == book.numImages()) {
                    String image = Book.shortImageName(book.imageName(page - 1));
                    gobox.setText(image);
                } else {
                    String image = Book.shortImageName(book.imageName(page));

                    if (page > 0) {
                        String imagev = Book.shortImageName(book.imageName(page - 1));
                        gobox.setText(imagev + ", " + image);
                    } else {
                        gobox.setText(image);
                    }
                }

                // update transcription display
                sidechoicelistener.onChange(null);
            }

            public void imageInfo(String info) {
                selectedImageIndex = FSIService.getImageIndexFromPagesInfo(info);
                gobox.setText(Book.shortImageName(book.imageName(selectedImageIndex)));
            }
        });

        if (sidechoice.getItemCount() > 1) {
            toolbar.add(sidechoice);
        }

        content.add(display);
        content.add(new HTML(book.imagePermissionStatement()));
    }

    private Widget displayTranscription(final boolean lecoy) {
        final ComplexPanel container = new FlowPanel();

        String recto = null;
        String verso = null;
        String image = book.imageName(selectedImageIndex);

        if (Book.isRectoImage(selectedImageIndex)) {
            recto = image;
            verso = book.imageName(selectedImageIndex - 1);
        } else {
            if (selectedImageIndex + 1 < book.numImages()) {
                recto = book.imageName(selectedImageIndex + 1);
            }

            verso = image;
        }

        if (recto != null) {
            recto = Book.isFolioImage(recto) && !Book.isMissingImage(recto) ? recto : null;
        }

        if (verso != null) {
            verso = Book.isFolioImage(verso) && !Book.isMissingImage(verso) ? verso : null;
        }

        String[] urls;
        final String[] imagenames;

        if (recto == null && verso == null) {
            Label l = new Label(Labels.INSTANCE.transcriptionUnavailable());
            l.setStylePrimaryName("TranscriptionUnavailableError");
            container.add(l);

            return container;
        } else if (recto == null) {
            urls = new String[] { GWT.getHostPageBaseURL() + DATA_PATH + Book.transcriptionPath(verso) };
            imagenames = new String[] { Book.shortImageName(verso) };
        } else if (verso == null) {
            urls = new String[] { GWT.getHostPageBaseURL() + DATA_PATH + Book.transcriptionPath(recto) };
            imagenames = new String[] { Book.shortImageName(recto) };
        } else {
            urls = new String[] { GWT.getHostPageBaseURL() + DATA_PATH + Book.transcriptionPath(verso),
                    GWT.getHostPageBaseURL() + DATA_PATH + Book.transcriptionPath(recto) };
            imagenames = new String[] { Book.shortImageName(verso), Book.shortImageName(recto) };
        }

        HttpGet.request(urls, new HttpGet.Callback<String[]>() {
            public void failure(String error) {
                if (container.getWidgetCount() == 1) {
                    Label l = new Label(Labels.INSTANCE.transcriptionUnavailable());
                    l.setStylePrimaryName("TranscriptionUnavailableError");
                    container.add(l);
                }
            }

            public void success(String[] results) {
                for (int i = 0; i < results.length; i++) {
                    if (results[i].isEmpty()) {
                        results[i] = null;
                    }
                }

                Widget trans = TranscriptionViewer.createTranscriptionViewer(results, imagenames,
                        container.getOffsetHeight() - 50, lecoy);
                container.add(trans);
            }
        });

        return container;
    }

    private void displayIllustrationKeywordsOnRight(final Panel container) {
        int recto = -1;
        int verso = -1;

        ImageTagging illus = book.illustrations();

        if (illus == null) {
            String illusurl = GWT.getHostPageBaseURL() + DATA_PATH + book.illustrationsPath();

            loadingdialog.display();

            HttpGet.request(illusurl, new HttpGet.Callback<String>() {
                public void failure(String error) {
                    loadingdialog.error(error);
                }

                public void success(String result) {
                    loadingdialog.hide();
                    book.setIllustrations(result);

                    if (book.illustrations() != null) {
                        displayIllustrationKeywordsOnRight(container);
                    }
                }
            });
        } else {
            if (Book.isRectoImage(selectedImageIndex)) {
                recto = selectedImageIndex;

                if (recto > 0) {
                    verso = selectedImageIndex - 1;
                }
            } else {
                if (selectedImageIndex + 1 < book.numImages()) {
                    recto = selectedImageIndex + 1;
                }

                verso = selectedImageIndex;
            }

            TabLayoutPanel tabpanel = new TabLayoutPanel(1.5, Unit.EM);
            tabpanel.addStyleName("ImageDescription");

            if (verso != -1) {
                displayIllustrationKeywords(tabpanel, verso);
            }

            if (recto != -1) {
                displayIllustrationKeywords(tabpanel, recto);
            }

            if (tabpanel.getWidgetCount() > 0) {
                tabpanel.selectTab(0);
                container.add(tabpanel);
            }
        }
    }

    private void displayIllustrationKeywords(TabLayoutPanel tabpanel, int image) {
        ImageTagging illus = book.illustrations();

        int count = 1;
        List<Integer> indexes = illus.findImageIndexes(image);

        for (int i : indexes) {
            String name = Book.shortImageName(book.imageName(image)) + (indexes.size() > 1 ? " " + count++ : "");
            tabpanel.add(new ScrollPanel(illus.displayImage(i)), name);
        }
    }

    // TODO use DialogBox

    private class LoadingDialog extends DecoratedPopupPanel {
        private final Panel panel;

        public LoadingDialog() {
            super(false);
            this.panel = new FlowPanel();
            panel.setStylePrimaryName("LoadingDialog");
            add(panel);
        }

        public void display() {
            panel.clear();
            panel.add(new Label(Labels.INSTANCE.usingWebService()));
            panel.add(new Label("..."));
            center();
        }

        public void error(String error) {
            // make sure dialog is visible
            content.clear();

            panel.clear();
            panel.add(new Label(Labels.INSTANCE.error()));
            panel.add(new Label(error));
            addCloseButton(true);
            center();
        }

        private void addCloseButton(final boolean gohomeonclose) {
            Button close = new Button(Labels.INSTANCE.close(), new ClickHandler() {
                public void onClick(ClickEvent event) {
                    hide();

                    if (gohomeonclose) {
                        History.newItem(Action.HOME.toToken());
                    }
                }
            });

            panel.add(close);
        }
    }

    // If search arguments are null, just displays search dialog

    private void viewSearch(final Searcher.UserField[] userfields, final String[] userqueries,
            final String[] restrictedbookids, final int offset, final String searchtokenprefix) {
        book = null;

        if (userfields == null || userqueries == null) {
            initDisplay(Labels.INSTANCE.search(), true);
            content.add(createAdvancedSearchWidget(userfields, userqueries, restrictedbookids));
            return;
        }

        loadingdialog.display();

        // Load character names as needed

        AsyncCallback<SearchResult> cb = new AsyncCallback<SearchResult>() {
            public void onFailure(Throwable caught) {
                loadingdialog.error(caught.getMessage());
            }

            public void onSuccess(SearchResult result) {
                loadingdialog.hide();
                initDisplay(result.total + " " + Labels.INSTANCE.hits(), true);

                content.add(createAdvancedSearchWidget(userfields, userqueries, restrictedbookids));
                displaySearchResults(content, searchtokenprefix, result);
            }
        };

        searcher.searchCollection(userfields, userqueries, restrictedbookids, offset, MAX_SEARCH_RESULTS,
                charnames_variants, cb);
    }

    /**
     * Select and load book as needed.
     *
     * @param id
     * @param nexttoken
     * @return whether or not book is being loaded
     */
    private boolean selectBook(String id, String nexttoken) {
        Book newbook = col.loadedBooks().get(id);

        if (newbook == null) {
            loadBook(id, nexttoken);
            return true;
        }

        book = newbook;

        return false;
    }

    private void addSidebarHeader(String header) {
        Label label = new Label(header);
        label.setStylePrimaryName("SidebarHeader");
        sidebar.add(label);
    }

    private void addSidebarItem(String text, String token) {
        addSidebarItem(createSidebarHyperlink(text, token));
    }

    private void addSidebarItem(Widget w) {
        w.setStylePrimaryName("SidebarItem");
        sidebar.add(w);
    }

    private static Widget createSidebarHyperlink(String name, String token) {
        if (History.getToken().startsWith(token)) {
            Label l = new Label(name);
            l.addStyleName("SidebarSelected");
            return l;
        } else {
            return new Hyperlink(name, token);
        }
    }

    private void setupSidebar() {
        sidebar.clear();

        if (book != null) {
            addSidebarHeader(Labels.INSTANCE.book());
            addSidebarItem(Labels.INSTANCE.description(), Action.VIEW_BOOK.toToken(book.id()));
            addSidebarItem(Labels.INSTANCE.pageTurner(), Action.READ_BOOK.toToken(book.id()));
            addSidebarItem(Labels.INSTANCE.browseImages(), Action.BROWSE_BOOK.toToken(book.id()));
        }

        addSidebarHeader(Labels.INSTANCE.selectBookBy());

        for (Repository.Category d : Repository.Category.values()) {
            if (d == Repository.Category.SHELFMARK || d == Repository.Category.ILLUSTRATION_TAGGING
                    || d == Repository.Category.NARRATIVE_TAGGING) {
                continue;
            }

            addSidebarItem(d.display(), Action.SELECT_BOOK.toToken(d.name()));
        }

        // addSidebarHeader(Labels.INSTANCE.language());
        // addSidebarLocaleSelector();

        // TODO translate
        addSidebarHeader("Features");
        final CheckBox flash = new CheckBox("Flash");
        flash.setValue(use_flash);
        addSidebarItem(flash);

        flash.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                use_flash = flash.getValue();
                History.fireCurrentHistoryState();
            }
        });

        //Label lastupdated = new Label(Labels.INSTANCE.updated() + ": " + Util.appLastModified());
        //lastupdated.setStylePrimaryName("SidebarLastUpdated");
        //sidebar.add(lastupdated);
    }

    private void initDisplay(String title, boolean displaytitleincontent) {
        Window.setTitle("Christine de Pizan: " + title);

        content.clear();

        if (displaytitleincontent) {
            Label label = new Label(title);
            label.setStylePrimaryName("ContentTitle");
            content.add(label);
        }

        setupSidebar();
    }

    private void viewBook() {
        initDisplay(col.fullBookName(book.bookDataIndex()), true);
        content.add(book.displayDescription(col));
    }

    private void viewBookPicker(Repository.Category cat) {
        book = null;
        initDisplay(Labels.INSTANCE.selectBook(), true);

        content.add(createBookPicker(col, cat));
    }

    private static Widget contentHeader(String s) {
        return label(s, "ContentHeader");
    }

    private static Widget label(String s, String style) {
        Widget w = new Label(s);
        w.setStylePrimaryName(style);
        return w;
    }

    private static Widget createBookPicker(final Repository col, Repository.Category category) {
        final TreeMap<String, List<Integer>> entries = col.browse(category);

        Cell<String> datacell = new AbstractCell<String>() {
            public void render(Cell.Context context, String value, SafeHtmlBuilder sb) {

                if (value != null) {
                    sb.appendEscaped(value);

                    int count = entries.get(value).size();

                    if (count > 1) {
                        sb.appendEscaped(" (" + count + ")");
                    }
                }
            }
        };

        final CellList<String> datalist = new CellList<String>(datacell);
        datalist.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.BOUND_TO_SELECTION);

        List<String> keys = new ArrayList<String>(entries.keySet());
        datalist.setRowData(keys);

        Cell<Integer> booknamecell = new AbstractCell<Integer>() {
            public void render(Cell.Context context, Integer bookid, SafeHtmlBuilder sb) {

                if (bookid != null) {
                    String id = col.bookData(bookid, Repository.Category.ID);
                    String fullname = col.fullBookName(bookid);
                    String a = "";

                    // TODO use template
                    sb.append(SafeHtmlUtils.fromTrustedString(a));
                    sb.appendEscaped(fullname);
                    sb.append(SafeHtmlUtils.fromTrustedString(""));
                }
            }
        };

        final CellList<Integer> booklist = new CellList<Integer>(booknamecell);
        booklist.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);

        final SingleSelectionModel<String> datalistsel = new SingleSelectionModel<String>();
        datalist.setSelectionModel(datalistsel);

        datalistsel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
            public void onSelectionChange(SelectionChangeEvent event) {
                String selected = datalistsel.getSelectedObject();

                if (selected != null) {
                    booklist.setRowData(entries.get(selected));
                }
            }
        });

        SplitLayoutPanel split = new SplitLayoutPanel();
        split.setStylePrimaryName("BookPicker");

        FlowPanel left = new FlowPanel();
        left.add(label(category.display(), "BookPicker-header"));
        left.add(new ScrollPanel(datalist));

        FlowPanel right = new FlowPanel();
        right.add(label(Labels.INSTANCE.book(), "BookPicker-header"));
        right.add(new ScrollPanel(booklist));

        // TODO would be nice to do this by percentage...
        split.addWest(left, 300);
        split.add(right);

        datalist.getParent().setStylePrimaryName("BookPicker-list");
        booklist.getParent().setStylePrimaryName("BookPicker-list");

        // Setting focus always needs to be a deferred command
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
            public void execute() {
                if (datalist.getVisibleItemCount() > 0) {
                    datalist.getSelectionModel().setSelected(datalist.getVisibleItem(0), true);
                    datalist.setFocus(true);
                }
            }
        });

        return split;
    }

    private void viewHome() {
        book = null;
        initDisplay(Labels.INSTANCE.mainPage(), false);

        HTML w = new HTML(Resources.INSTANCE.homeHtml().getText());
        w.setStylePrimaryName("Home");
        w.setWordWrap(true);
        content.add(w);
    }
}


text:
10.1016_j.jadohealth.2016.11.026

<button title="table2" class="tablinks" onClick="openTab(event, 'table2', 'tabcontent')">table2</button><button title="table1" class="tablinks" onClick="openTab(event, 'table1', 'tabcontent')">table1</button>

corpus-oa-validation/10.1016_j.jadohealth.2016.11.026/tables/table2/table.svg.png

     12e14
     15e17
     18e19
     Total
     12e14
     15e17
     18e19
     Total

     Number of well-child and family planning clients (two visit types)
     7
     12
     2
     21
     10
     23
     16
     49

     Number of well-child clients receiving sexual health screening/counseling
     131
     59
     5
     195
     179
     136
     42
     357

     Number of family planning clients
     63
     550
     702
     1,315
     69
     512
     786
     1,367

     Total clients unduplicated across services
     187
     597
     705
     1,489
     238
     625
     812
     1,675

corpus-oa-validation/10.1016_j.jadohealth.2016.11.026/tables/table2/table.svg.html

corpus-oa-validation/10.1016_j.jadohealth.2016.11.026/tables/table1/table.svg.png

     Community mobilization Stakeholder education Evidence-based programs Increase youth access to contraceptive and reproductive health services
     • Assess community support for ARHS access • Build community leaders'capacity to support ARHS access • Normalize support for ARHS access through presentations to community groups on community attitudes and project approaches • Implement supplemental lesson on ARHS access to youth enrolled in evidence-based programs • Assess youth attitudes about ARHS access • Open Teen Wellness Center • Expand ARHS referral network of community organizations

     ARHS = adolescent reproductive health services.

corpus-oa-validation/10.1016_j.jadohealth.2016.11.026/tables/table1/table.svg.html

redpajama_set_name: RedPajamaGithub


text:
package com.jfastnet.messages;

import com.esotericsoftware.kryo.Kryo;
import com.jfastnet.AbstractTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.util.Random;
import java.util.function.Supplier;

/** @author Klaus Pfeiffer - klaus@allpiper.com */
@Slf4j
public class CompressedMessageTest extends AbstractTest {

	private static int received;

	public static class Message1 extends Message {
		long value = 0;
		public Message1(long value) { this.value = value; }

		@Override
		public void process(Object context) {
			received++;
		}
	}

	public static class Message2 extends Message {
		long[] longArray;

		public Message2(long[] longArray) {
			this.longArray = longArray;
		}

		@Override
		public void process(Object context) {
			received++;
		}
	}

	@Test
	public void testCompressedMessages() {
		start(1);
		Message msg;

		log.info(" * Test with one additional long field value 0");
		testWithMessage(() -> new Message1(1));

		log.info(" * Test with one additional long field value Long.MAX_VALUE");
		testWithMessage(() -> new Message1(Long.MAX_VALUE));

		log.info(" * Test with long array field and random values");
		testWithMessage(() -> new Message2(createLongArrayWithRandomValues(200)));
	}

	private void testWithMessage(Supplier<Message> messageSupplier) {
		received = 0;
		Message msg;
		msg = messageSupplier.get();
		server.send(msg);
		log.info("Payload length uncompressed message: {}", msg.payloadLength());
		waitForCondition("message not received.", 1, () -> received == 1);

		msg = CompressedMessage.createFrom(server.getState(), messageSupplier.get());
		server.send(msg);
		log.info("Payload length compressed message: {}", msg.payloadLength());
		waitForCondition("message not received.", 1, () -> received == 2);
	}

	private long[] createLongArrayWithRandomValues(int size) {
		Random random = new Random(System.currentTimeMillis());
		long[] arr = new long[size];
		for (int i = 0; i < size; i++) {
			arr[i] = random.nextLong();
		}
		return arr;
	}

	@Override
	public void customizeKryo(Kryo kryo) {
		kryo.register(CompressedMessage.class);
		kryo.register(Message1.class);
		kryo.register(Message2.class);
	}

}


text:
var win = Titanium.UI.currentWindow;

var images = [];
var prefix = 'http://grin.hq.nasa.gov/IMAGES/SMALL/GPN-2000-0000';
var start = 38;
for (var c=0;c<30;c++)
{
	var name = prefix + (start+c) + '.jpg';
	images[c]= {image:name, width:225, height:225};
}

// create coverflow view with images
var view = Titanium.UI.createCoverFlowView({
	images:images,
	backgroundColor:'#000'
});

// click listener - when image is clicked
view.addEventListener('click',function(e)
{
	Titanium.API.info("image clicked: "+e.index+', selected is '+view.selected);
});

// change listener when active image changes
view.addEventListener('change',function(e)
{
	Titanium.API.info("image changed: "+e.index+', selected is '+view.selected);
});
win.add(view);

// change button to dynamically change the image
var change = Titanium.UI.createButton({
	title:'Change Image',
	style:Titanium.UI.iPhone.SystemButtonStyle.BORDERED
});
change.addEventListener('click',function()
{
	Titanium.API.info("selected is = "+view.selected);
	view.setImage(view.selected,'../images/imageview/28.jpg');
});

// move scroll view left
var left = Titanium.UI.createButton({
	image:'../images/icon_arrow_left.png'
});
left.addEventListener('click', function(e)
{
	var i = view.selected - 1;
	if (i < 0)
	{
		i = 0;
	}
	view.selected = i;
});

// move scroll view right
var right = Titanium.UI.createButton({
	image:'../images/icon_arrow_right.png'
});
right.addEventListener('click', function(e)
{
	var i = view.selected + 1;
	if (i >= images.length)
	{
		i = images.length - 1;
	}
	view.selected = i;
});
var flexSpace = Titanium.UI.createButton({
	systemButton:Titanium.UI.iPhone.SystemButton.FLEXIBLE_SPACE
});
win.setToolbar([flexSpace,left,change,right,flexSpace]);


text:
module Aspector
  # Class used as a wrapper to get logging instances
  # It provides logger detection (from ENV) and creates a logger instance with a proper context
  module Logging
    class << self
      # @param context [Aspector::Base, Class] context in which we want to log information
      # @note it will log more details if it is an Aspector internal context
      # @return [Aspector::Logger, Logger] aspector logger that logs in a given context
      #   or provided logger
      # @example Create a logger instance inside Aspector::Base
      #   @logger ||= Aspector::Logging.build(self)
      def build(context)
        (deconstantize(ENV['ASPECTOR_LOGGER'] || 'Aspector::Logger')).new(context)
      end

      private

      # @param klass_name [String, Symbol] stringified class/module name that we want to convert
      #   to a real working class/module instance
      # @return [Class] class/module that was fetched from its string version
      # @note If it cannot detect a valid class based on the name,
      #   it will return an Aspector::Logger instance as a fallback
      # @example Get a class DummyClass based on its 'DummyClass' string
      #   deconstantize('DummyClass') #=> 'DummyClass'
      # @example Get a namespaced class reference
      #   deconstantize('Users::UserRole') #=> User::UserRole
      # @example Try to get class that doesn't exist
      #   deconstantize('NonExistingClass') #=> Aspector::Logger
      def deconstantize(klass_name)
        Object.const_get(klass_name.to_s)
      rescue NameError
        $stderr.puts "#{klass_name} is not a valid constant name!"
        Aspector::Logger
      end
    end
  end
end


text:
import Check from "./Check.js";

/**
 * Initiates a terrain height query for an array of {@link Cartographic} positions by
 * requesting tiles from a terrain provider, sampling, and interpolating.  The interpolation
 * matches the triangles used to render the terrain at the specified level.  The query
 * happens asynchronously, so this function returns a promise that is resolved when
 * the query completes.  Each point height is modified in place.  If a height can not be
 * determined because no terrain data is available for the specified level at that location,
 * or another error occurs, the height is set to undefined.  As is typical of the
 * {@link Cartographic} type, the supplied height is a height above the reference ellipsoid
 * (such as {@link Ellipsoid.WGS84}) rather than an altitude above mean sea level.  In other
 * words, it will not necessarily be 0.0 if sampled in the ocean. This function needs the
 * terrain level of detail as input, if you need to get the altitude of the terrain as precisely
 * as possible (i.e. with maximum level of detail) use {@link sampleTerrainMostDetailed}.
 *
 * @function sampleTerrain
 *
 * @param {TerrainProvider} terrainProvider The terrain provider from which to query heights.
 * @param {Number} level The terrain level-of-detail from which to query terrain heights.
 * @param {Cartographic[]} positions The positions to update with terrain heights.
 * @returns {Promise.<Cartographic[]>} A promise that resolves to the provided list of positions when terrain the query has completed.
 *
 * @see sampleTerrainMostDetailed
 *
 * @example
 * // Query the terrain height of two Cartographic positions
 * const terrainProvider = Cesium.createWorldTerrain();
 * const positions = [
 *     Cesium.Cartographic.fromDegrees(86.925145, 27.988257),
 *     Cesium.Cartographic.fromDegrees(87.0, 28.0)
 * ];
 * const promise = Cesium.sampleTerrain(terrainProvider, 11, positions);
 * Promise.resolve(promise).then(function(updatedPositions) {
 *     // positions[0].height and positions[1].height have been updated.
 *     // updatedPositions is just a reference to positions.
 * });
 */
function sampleTerrain(terrainProvider, level, positions) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("terrainProvider", terrainProvider);
  Check.typeOf.number("level", level);
  Check.defined("positions", positions);
  //>>includeEnd('debug');

  return terrainProvider.readyPromise.then(function () {
    return doSampling(terrainProvider, level, positions);
  });
}

/**
 * @param {Array.<Object>} tileRequests The mutated list of requests, the first one will be attempted
 * @param {Array.<Promise<void>>} results The list to put the result promises into
 * @returns {boolean} true if the request was made, and we are okay to attempt the next item immediately,
 *  or false if we were throttled and should wait awhile before retrying.
 *
 * @private
 */
function attemptConsumeNextQueueItem(tileRequests, results) {
  const tileRequest = tileRequests[0];
  const requestPromise = tileRequest.terrainProvider.requestTileGeometry(
    tileRequest.x,
    tileRequest.y,
    tileRequest.level
  );

  if (!requestPromise) {
    // getting back undefined instead of a promise indicates we should retry a bit later
    return false;
  }

  const promise = requestPromise
    .then(createInterpolateFunction(tileRequest))
    .catch(createMarkFailedFunction(tileRequest));

  // remove the request we've just done from the queue
  //  and add its promise result to the result list
  tileRequests.shift();
  results.push(promise);

  // indicate we should synchronously attempt the next request as well
  return true;
}

/**
 * Wrap window.setTimeout in a Promise
 * @param {number} ms
 * @private
 */
function delay(ms) {
  return new Promise(function (res) {
    setTimeout(res, ms);
  });
}

/**
 * Recursively consumes all the tileRequests until the list has been emptied
 *  and a Promise of each result has been put into the results list
 * @param {Array.<Object>} tileRequests The list of requests desired to be made
 * @param {Array.<Promise<void>>} results The list to put all the result promises into
 * @returns {Promise<void>} A promise which resolves once all requests have been started
 *
 * @private
 */
function drainTileRequestQueue(tileRequests, results) {
  // nothing left to do
  if (!tileRequests.length) {
    return Promise.resolve();
  }

  // consume an item from the queue, which will
  //  mutate the request and result lists, and return true if we should
  //  immediately attempt to consume the next item as well
  const success = attemptConsumeNextQueueItem(tileRequests, results);
  if (success) {
    return drainTileRequestQueue(tileRequests, results);
  }

  // wait a small fixed amount of time first, before retrying the same request again
  return delay(100).then(() => {
    return drainTileRequestQueue(tileRequests, results);
  });
}

function doSampling(terrainProvider, level, positions) {
  const tilingScheme = terrainProvider.tilingScheme;

  let i;

  // Sort points into a set of tiles
  const tileRequests = []; // Result will be an Array as it's easier to work with
  const tileRequestSet = {}; // A unique set
  for (i = 0; i < positions.length; ++i) {
    const xy = tilingScheme.positionToTileXY(positions[i], level);
    const key = xy.toString();

    if (!tileRequestSet.hasOwnProperty(key)) {
      // When tile is requested for the first time
      const value = {
        x: xy.x,
        y: xy.y,
        level: level,
        tilingScheme: tilingScheme,
        terrainProvider: terrainProvider,
        positions: [],
      };
      tileRequestSet[key] = value;
      tileRequests.push(value);
    }

    // Now append to array of points for the tile
    tileRequestSet[key].positions.push(positions[i]);
  }

  // create our list of result promises to be filled
  const tilePromises = [];
  return drainTileRequestQueue(tileRequests, tilePromises).then(function () {
    // now all the required requests have been started
    //  we just wait for them all to finish
    return Promise.all(tilePromises).then(function () {
      return positions;
    });
  });
}

/**
 * Calls {@link TerrainData#interpolateHeight} on a given {@link TerrainData} for a given {@link Cartographic} and
 *  will assign the height property if the return value is not undefined.
 *
 * If the return value is false; it's suggesting that you should call {@link TerrainData#createMesh} first.
 * @param {Cartographic} position The position to interpolate for and assign the height value to
 * @param {TerrainData} terrainData
 * @param {Rectangle} rectangle
 * @returns {Boolean} If the height was actually interpolated and assigned
 * @private
 */
function interpolateAndAssignHeight(position, terrainData, rectangle) {
  const height = terrainData.interpolateHeight(
    rectangle,
    position.longitude,
    position.latitude
  );
  if (height === undefined) {
    // if height comes back as undefined, it may implicitly mean the terrain data
    //  requires us to call TerrainData.createMesh() first (ArcGIS requires this in particular)
    //  so we'll return false and do that next!
    return false;
  }
  position.height = height;
  return true;
}

function createInterpolateFunction(tileRequest) {
  const tilePositions = tileRequest.positions;
  const rectangle = tileRequest.tilingScheme.tileXYToRectangle(
    tileRequest.x,
    tileRequest.y,
    tileRequest.level
  );
  return function (terrainData) {
    let isMeshRequired = false;
    for (let i = 0; i < tilePositions.length; ++i) {
      const position = tilePositions[i];
      const isHeightAssigned = interpolateAndAssignHeight(
        position,
        terrainData,
        rectangle
      );
      // we've found a position which returned undefined - hinting to us
      //  that we probably need to create a mesh for this terrain data.
      // so break out of this loop and create the mesh - then we'll interpolate all the heights again
      if (!isHeightAssigned) {
        isMeshRequired = true;
        break;
      }
    }

    if (!isMeshRequired) {
      // all position heights were interpolated - we don't need the mesh
      return Promise.resolve();
    }

    // create the mesh - and interpolate all the positions again
    // note: terrain exaggeration is not passed in - we are only interested in the raw data
    return terrainData
      .createMesh({
        tilingScheme: tileRequest.tilingScheme,
        x: tileRequest.x,
        y: tileRequest.y,
        level: tileRequest.level,
        // don't throttle this mesh creation because we've asked to sample these points;
        //  so sample them! We don't care how many tiles that is!
        throttle: false,
      })
      .then(function () {
        // mesh has been created - so go through every position (maybe again)
        //  and re-interpolate the heights - presumably using the mesh this time
        for (let i = 0; i < tilePositions.length; ++i) {
          const position = tilePositions[i];
          // if it doesn't work this time - that's fine, we tried.
          interpolateAndAssignHeight(position, terrainData, rectangle);
        }
      });
  };
}

function createMarkFailedFunction(tileRequest) {
  const tilePositions = tileRequest.positions;
  return function () {
    for (let i = 0; i < tilePositions.length; ++i) {
      const position = tilePositions[i];
      position.height = undefined;
    }
  };
}

export default sampleTerrain;


text:
using namespace std;

namespace eversim { namespace core { namespace world { namespace {
	uint32_t load_big_endian(uint32_t in)
	{
		auto* dat = reinterpret_cast<unsigned char*>(&in);
		return (dat[0] << 24) + (dat[1] << 16) + (dat[2] << 8) + dat[3];
	}
}

level_loader::level_loader(tile_loader_ptr tl) : tile_loader_(tl)
{
	register_tile_descriptor(&blank_tile);
}

shared_ptr<level> level_loader::load_file(string const& filename)
{
	using namespace utility::fs;
	const auto dir = path(filename);
	const auto layout_path = dir / "layout.png";
	const auto table_path = dir / "ids.txt";
	// TODO check extra data

	if (!exists(layout_path))
	{
		throw runtime_error{"Layout file missing in " + filename};
	}
	if (!exists(table_path))
	{
		throw runtime_error{"ID table file missing in " + filename};
	}

	auto idtablestream = ifstream(table_path);
	auto idtable = load_id_table(idtablestream);

	glm::ivec2 res;
	auto image_data = SOIL_load_image(layout_path.string().c_str(), &res.x, &res.y, nullptr, SOIL_LOAD_RGBA);

	auto _ = utility::finally([&image_data]
	{
		SOIL_free_image_data(image_data);
	});

	auto layout = boost::const_multi_array_ref<uint32_t, 2>(
		reinterpret_cast<uint32_t*>(image_data),
		boost::extents[res.x][res.y]
	);

	auto lvl = make_shared<level>(res);

	for (int x = 0; x < res.x; ++x)
	{
		for (int y = 0; y < res.y; ++y)
		{
			auto& t = lvl->get_tile_by_index({x,y});
			auto id = load_big_endian(layout[res.y - y - 1][x]);
			if (idtable.find(id) == idtable.end())
			{
				EVERSIM_THROW(level_error::UnknownID,
					(boost::format("Unknown id %d at tile (%d,%d)") % id % x % y).str()
				);
			}
			const auto desc_name = idtable[id];
			const auto desc_it = descriptors.find(desc_name);

			tile_descriptor const* desc = nullptr;
			if(desc_it != descriptors.end())
			{
				desc = desc_it->second;
			}else if(tile_loader_)
			{
				const auto ptr = tile_loader_->load(desc_name);
				if(ptr.use_count() == 1)
				{
					// loader currently can't set the name of the tile descriptor by itself
					ptr->name = desc_name;
				}
				desc = ptr.get();
				lvl->add_tile_descriptor(ptr);
			}

			if (!desc)
			{
				EVERSIM_THROW(level_error::UnknownTile, desc_name);
			}
			t.set_descriptor(desc);
		}
	}
	lvl->calculate_collision_shapes();
	return lvl;
}

map<uint32_t, string> level_loader::load_id_table(istream& data)
{
	auto ret = map<uint32_t, string>();
	auto line = string();
	while (getline(data, line))
	{
		stringstream in(line);
		in >> ws;
		if (in.peek() == '#')
			continue;
		string num_txt;
		in >> num_txt;

		// use base '0' to let stoi automatically detect hex numbers
		auto num = stoll(num_txt, nullptr, 0);

		string text;
		in >> text;
		ret[num] = text;
	}
	return ret;
}

void level_loader::register_tile_descriptor(tile_descriptor const* desc)
{
	EVERSIM_ASSERT(desc);
	descriptors[desc->name] = desc;
}

vector<string> level_loader::get_level_tiles(string const& name) const
{
	const auto dir = search(name);
	const auto table_path = dir / "ids.txt";
	auto idtablestream = ifstream(table_path);
	const auto idtable = load_id_table(idtablestream);

	vector<std::string> names;
	names.reserve(idtable.size());
	for(auto& tile : idtable | boost::adaptors::map_values)
	{
		if(tile == "blank")
		{
			continue;
		}
		names.push_back(tile);
	}
	return names;
}

}}}