package pl.djvuhtml5.client.ui; import static pl.djvuhtml5.client.TileRenderer.MAX_SUBSAMPLE; import static pl.djvuhtml5.client.TileRenderer.toSubsample; import static pl.djvuhtml5.client.TileRenderer.toZoom; import java.util.ArrayList; import com.google.gwt.canvas.client.Canvas; import com.google.gwt.canvas.dom.client.Context2d; import com.google.gwt.dom.client.CanvasElement; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.DomEvent; 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.dom.client.MouseDownEvent; import com.google.gwt.event.dom.client.MouseWheelEvent; import com.google.gwt.event.dom.client.MouseWheelHandler; import com.google.gwt.event.dom.client.TouchStartEvent; import com.google.gwt.http.client.UrlBuilder; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Widget; import com.lizardtech.djvu.DjVuInfo; import com.lizardtech.djvu.GRect; import pl.djvuhtml5.client.DataStore; import pl.djvuhtml5.client.DjvuContext; import pl.djvuhtml5.client.Djvu_html5; import pl.djvuhtml5.client.ui.Scrollbar.ScrollPanListener; public class SinglePageLayout { interface ChangeListener { void pageChanged(int currentPage); void zoomChanged(int currentZoom); } private final Djvu_html5 app; private double zoom = 0; private double zoom100; private int page; private final DataStore dataStore; private DjVuInfo pageInfo; /** * Location of the center of the screen in the coordinates of the scaled * page (one point is one pixel on the screen). */ private int centerX, centerY; private final Canvas canvas; private ChangeListener changeListener; private String background; private final int pageMargin; private CanvasElement[][] imagesArray; private GRect range = new GRect(); private final boolean locationUpdateEnabled; private Timer locationUpdater; private boolean enableScrollPageJump = true; public SinglePageLayout(Djvu_html5 app) { this.app = app; this.dataStore = app.getDataStore(); this.canvas = app.getCanvas(); this.background = DjvuContext.getBackground(); this.pageMargin = DjvuContext.getPageMargin(); new PanController(app.getTextLayer()); boolean pageParam = false; try { page = Integer.parseInt(Window.Location.getParameter("p")) - 1; pageParam = true; } catch (Exception e) { page = 0; } locationUpdateEnabled = pageParam || DjvuContext.getLocationUpdateEnabled(); DjvuContext.setPage(page); dataStore.addPageCountListener(pageCount -> { app.getToolbar().setPageCount(pageCount); int newPage = Math.max(0, Math.min(pageCount - 1, page)); setPage(newPage); }); dataStore.addInfoListener(pageNum -> { if (pageNum == page) setPage(pageNum); }); dataStore.addTileListener(pageNum -> { if (pageNum == page) redraw(); }); } public void setPage(int pageNum) { page = pageNum; DjVuInfo newInfo = app.getDataStore().getPageInfo(pageNum); if (newInfo != null) { pageInfo = newInfo; app.getToolbar().setZoomOptions(findZoomOptions()); } else { pageInfo = null; } viewChanged(); DjvuContext.setPage(pageNum); if (changeListener != null) changeListener.pageChanged(pageNum); scheduleURLUpdate(); } private void scheduleURLUpdate() { if (!locationUpdateEnabled) return; if (locationUpdater == null) { locationUpdater = new Timer() { @Override public void run() { UrlBuilder url = Window.Location.createUrlBuilder(); if (page > 0) { url.setParameter("p", Integer.toString(page + 1)); } else { url.removeParameter("p"); } updateURLWithoutReloading(url.buildString()); } private native void updateURLWithoutReloading(String newUrl) /*-{ $wnd.history.replaceState(newUrl, "", newUrl); }-*/; }; } locationUpdater.cancel(); locationUpdater.schedule(500); } public int getPage() { return page; } public void canvasResized() { viewChanged(); } public void zoomToFitPage() { if (pageInfo == null) return; doSetZoom(Math.min((1.0f * canvas.getCoordinateSpaceWidth() - pageMargin * 2) / pageInfo.width, (1.0f * canvas.getCoordinateSpaceHeight() - pageMargin * 2) / pageInfo.height)); } public void zoomToFitWidth() { if (pageInfo == null) return; doSetZoom((1.0f * canvas.getCoordinateSpaceWidth() - pageMargin * 2) / pageInfo.width); } public void setZoom(int percent) { if (pageInfo == null) return; doSetZoom(percent * zoom100 / 100); } public int getZoom() { return (int) (zoom / zoom100 * 100 + 0.5); } public double getPreciseZoom() { return zoom; } private void doSetZoom(double zoom) { if (this.zoom == zoom) return; centerX *= zoom / this.zoom; centerY *= zoom / this.zoom; this.zoom = zoom; viewChanged(); if (changeListener != null) changeListener.zoomChanged(getZoom()); } private ArrayList<Integer> findZoomOptions() { ArrayList<Integer> result = new ArrayList<>(); result.add(100); final int screenDPI = DjvuContext.getScreenDPI(); zoom100 = 1.0 * screenDPI / pageInfo.dpi; int subsample = toSubsample(zoom100); if (toZoom(subsample) / zoom100 > zoom100 / toZoom(subsample + 1)) subsample++; zoom100 = toZoom(subsample); if (zoom == 0) zoom = zoom100; double z = zoom100; for (int i = subsample + 1; i <= MAX_SUBSAMPLE; i++) { double z2 = toZoom(i); if (z / z2 > 1.2) { z = z2; result.add((int) (z / zoom100 * 100 + 0.5)); } } z = zoom100; for (int i = subsample - 1; i >= 1; i--) { double z2 = toZoom(i); if (z2 / z > 1.2) { z = z2; result.add(0, (int) (z / zoom100 * 100 + 0.5)); } } return result; } private void viewChanged() { if (pageInfo == null) { redraw(); return; } int w = canvas.getCoordinateSpaceWidth(), h = canvas.getCoordinateSpaceHeight(); int pw = (int) (pageInfo.width * zoom), ph = (int) (pageInfo.height * zoom); if (pw < w) { centerX = pw / 2; } else { centerX = Math.max(centerX, w / 2 - pageMargin); centerX = Math.min(centerX, pw - w / 2 + pageMargin); } if (ph < h) { centerY = ph / 2; } else { centerY = Math.max(centerY, h / 2 - pageMargin); centerY = Math.min(centerY, ph - h / 2 + pageMargin); } double pw2 = pw + 2 * pageMargin, ph2 = ph + 2 * pageMargin; app.getHorizontalScrollbar().setThumb((centerX + pageMargin) / pw2, w / pw2); app.getVerticalScrollbar().setThumb((centerY + pageMargin) / ph2, h / ph2); if (app.getTextLayer() != null) app.getTextLayer().setViewPosition(page, w / 2 - centerX, h / 2 - centerY, zoom); redraw(); } private void changePage(int targetPage, int horizontalPosition, int verticalPosition) { if (targetPage >= 0 && targetPage < dataStore.getPageCount()) { if (horizontalPosition < 0) centerX = 0; else if (horizontalPosition > 0) centerX = Integer.MAX_VALUE; if (verticalPosition < 0) centerY = 0; else if (verticalPosition > 0) centerY = Integer.MAX_VALUE; setPage(targetPage); } } /** * @param left position of page's left edge on the canvas * @param top position of page's top edge on the canvas */ public void externalScroll(int page, int left, int top) { int w = canvas.getCoordinateSpaceWidth(), h = canvas.getCoordinateSpaceHeight(); if (page == this.page && centerX == w / 2 - left && centerY == h / 2 - top) return; int oldX = centerX, oldY = centerY; int newX = w / 2 - left; int newY = h / 2 - top; centerX = newX; centerY = newY; if (page != this.page) { setPage(page); } else { viewChanged(); if (oldX == centerX && oldY == centerY && enableScrollPageJump) { if (newY < oldY) { changePage(page - 1, 0, 1); } else if (newY > oldY) { changePage(page + 1, 0, -1); } else if (newX < oldX) { changePage(page - 1, 1, 0); } else if (newX > oldX) { changePage(page + 1, -1, 0); } } } } public void setChangeListener(ChangeListener changeListener) { this.changeListener = changeListener; } public void redraw() { Context2d graphics2d = canvas.getContext2d(); int w = canvas.getCoordinateSpaceWidth(), h = canvas.getCoordinateSpaceHeight(); graphics2d.setFillStyle(background); graphics2d.fillRect(0, 0, w, h); if (pageInfo == null) return; int subsample = toSubsample(zoom); double scale = zoom / toZoom(subsample); graphics2d.save(); int startX = w / 2 - centerX, startY = h / 2 - centerY; graphics2d.translate(startX, startY); graphics2d.scale(scale, scale); graphics2d.translate(-startX, -startY); graphics2d.scale(1, -1); // DjVu images have y-axis inverted int tileSize = DjvuContext.getTileSize(); int pw = (int) (pageInfo.width * zoom), ph = (int) (pageInfo.height * zoom); range.xmin = (int) (Math.max(0, centerX - w * 0.5) / tileSize / scale); range.xmax = (int) Math.ceil(Math.min(pw, centerX + w * 0.5) / tileSize / scale); range.ymin = (int) (Math.max(0, centerY - h * 0.5) / tileSize / scale); range.ymax = (int) Math.ceil(Math.min(ph, centerY + h * 0.5) / tileSize / scale); imagesArray = dataStore.getTileImages(page, subsample, range , imagesArray); for (int y = range.ymin; y <= range.ymax; y++) { for (int x = range.xmin; x <= range.xmax; x++) { CanvasElement canvasElement = imagesArray[y - range.ymin][x - range.xmin]; for (int repeats = scale == 1 ? 1 : 3; repeats > 0; repeats--) { graphics2d.drawImage(canvasElement, startX + x * tileSize, -startY - y * tileSize - canvasElement.getHeight()); } } } graphics2d.restore(); // missing tile graphics may exceed the page boundary graphics2d.fillRect(startX + pw, 0, w, h); graphics2d.fillRect(0, startY + ph, w, h); DjvuContext.setTileRange(range, subsample); } private class PanController extends PanListener implements MouseWheelHandler, KeyDownHandler, ScrollPanListener { private static final int KEY_PLUS = 187; private static final int KEY_MINUS = 189; public PanController(Widget widget) { super(widget); widget.addDomHandler(this, MouseWheelEvent.getType()); widget.addDomHandler(this, KeyDownEvent.getType()); app.getHorizontalScrollbar().addScrollPanListener(this); app.getVerticalScrollbar().addScrollPanListener(this); } @Override public void onMouseDown(MouseDownEvent event) { widget.getElement().focus(); if (!isOnText(event)) super.onMouseDown(event); } @Override public void onTouchStart(TouchStartEvent event) { if (isOnText(event)) { enableScrollPageJump = false; } else { widget.getElement().focus(); super.onTouchStart(event); } } @Override public void onKeyDown(KeyDownEvent event) { int key = event.getNativeKeyCode(); if (event.isControlKeyDown()) { if (key == KEY_PLUS || key == KEY_MINUS) { app.getToolbar().zoomChangeClicked(key == KEY_PLUS ? 1 : -1); event.preventDefault(); } } else if (!event.isShiftKeyDown()) { boolean handled = true; switch (key) { case KeyCodes.KEY_HOME: changePage(0, -1, -1); break; case KeyCodes.KEY_END: changePage(dataStore.getPageCount() - 1, 1, 1); break; default: handled = false; } if (handled) event.preventDefault(); } } @Override public void onMouseWheel(MouseWheelEvent event) { if (event.isControlKeyDown()) { int delta = event.getDeltaY(); app.getToolbar().zoomChangeClicked(Integer.signum(-delta)); event.preventDefault(); } } @Override public void thumbDragged(double newCenter, boolean isHorizontal) { if (pageInfo == null) return; if (isHorizontal) { double pw2 = pageInfo.width * zoom + 2 * pageMargin; tryPan((int) (newCenter * pw2 - pageMargin + 0.5) - centerX, 0); } else { double ph2 = pageInfo.height * zoom + 2 * pageMargin; tryPan(0, (int) (newCenter * ph2 - pageMargin + 0.5) - centerY); } } @Override protected void pan(int dx, int dy) { tryPan(-dx, -dy); } private boolean isOnText(DomEvent<?> event) { return "SPAN".equals(Element.as(event.getNativeEvent().getEventTarget()).getNodeName()); } private boolean tryPan(int dx, int dy) { int oldX = centerX; int oldY = centerY; centerX += dx; centerY += dy; viewChanged(); // applies constraints to x,y if (centerX != oldX || centerY != oldY) { redraw(); return true; } return false; } } }