/******************************************************************************* * Copyright (c) 2016 Alex Shapiro - github.com/shpralex * This program and the accompanying materials * are made available under the terms of the The MIT License (MIT) * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. *******************************************************************************/ package com.sproutlife.panel.gamepanel; import java.awt.*; import java.awt.geom.*; import java.util.concurrent.locks.ReentrantLock; import com.sproutlife.panel.PanelController; import com.sproutlife.panel.gamepanel.ScrollPanel.ViewportMovedListener; import com.sproutlife.panel.gamepanel.ScrollPanel.ViewportResizedListener; public class ScrollPanelController { private static final int SUBTRACT_FROM_MARGIN = 100; private static final int SCROLL_TO_DELAY = 18; private static final int SCROLL_TO_STEP = 8; private PanelController gc; private Rectangle visibleRectangle; private Rectangle maxVisibleRectangle; private Rectangle2D.Double rendererBounds; private Point2D.Double centerLocation; private Thread scrollThread; private boolean autoScrolling; private ReentrantLock autoScrollLock; ScrollPanel scrollPanel; public ScrollPanelController(PanelController panelController) { this.gc = panelController; visibleRectangle = new Rectangle(0, 0, 1, 1); maxVisibleRectangle = new Rectangle(); autoScrollLock = new ReentrantLock(); scrollPanel = new ScrollPanel(panelController); createListeners(); } public ScrollPanel getScrollPanel() { return scrollPanel; } private void createListeners() { ViewportResizedListener viewportResizedListener = new ViewportResizedListener() { public void viewportResized(int viewportWidth, int viewportHeight) { ScrollPanelController.this.viewportResized(viewportWidth, viewportHeight); } }; ViewportMovedListener viewportMovedListener = new ViewportMovedListener() { public void viewportMoved(int viewportX, int viewportY) { stopAutoScroll(); ScrollPanelController.this.viewportMoved(viewportX, viewportY); } }; getScrollPanel().addViewportResizedListener(viewportResizedListener); getScrollPanel().addViewportMovedListener(viewportMovedListener); } public boolean isAutoScrolling() { return autoScrolling; } public synchronized void setScalingZoomFactor(double scalingZoomFactor) { //if ( getScalingZoomFactor() != scalingZoomFactor ) { // FIXME - this was always false for skeleton based i think try { gc.getInteractionLock().writeLock().lock(); double viewportX = getViewportOffsetX(); double viewportY = getViewportOffsetY(); gc.getBoardRenderer().getTransform().setToScale(scalingZoomFactor,scalingZoomFactor); gc.getBoardRenderer().getTransform().translate(viewportX, viewportY); updateScrollBarsWithCenter(getCenterDrawPosition()); } finally { gc.getInteractionLock().writeLock().unlock(); } gc.getImageManager().repaintNewImage(); //} } public double getScalingZoomFactor() { return gc.getBoardRenderer().getZoom(); } public Rectangle getMaxVisibleRectangle() { return maxVisibleRectangle; } public Rectangle2D.Double getRendererRectangle() { return getRendererRectangle(0); } public Rectangle2D.Double getRendererRectangle(int pad) { double x = (rendererBounds.getMinX()*getScalingZoomFactor()) - pad; double y = (rendererBounds.getMinY()*getScalingZoomFactor()) - pad; double width = (rendererBounds.getMaxX()*getScalingZoomFactor()) - x + pad; double height = (rendererBounds.getMaxY()*getScalingZoomFactor()) - y + pad; return new Rectangle2D.Double(x, y, width, height); } private static Rectangle2D.Double applyTransform(Rectangle2D.Double rect, AffineTransform transform) { Rectangle2D.Double newRect = new Rectangle2D.Double(); Rectangle2D transRect = transform.createTransformedShape(rect).getBounds2D(); newRect.setRect(transRect); return newRect; } public double getViewportOffsetX() { return gc.getBoardRenderer().getTransform().getTranslateX() / gc.getBoardRenderer().getTransform().getScaleX(); } public double getViewportOffsetY() { return gc.getBoardRenderer().getTransform().getTranslateY() / gc.getBoardRenderer().getTransform().getScaleY(); } public Rectangle2D.Double getViewportRendererBounds() { Rectangle2D.Double viewportBounds = null; try { gc.getInteractionLock().readLock().lock(); viewportBounds = new Rectangle2D.Double(-getViewportOffsetX(), -getViewportOffsetY(), visibleRectangle.width/getScalingZoomFactor(), visibleRectangle.height/getScalingZoomFactor()); } finally { gc.getInteractionLock().readLock().unlock(); } return viewportBounds; } public Rectangle getViewportRendererBounds(int scaledWidth, int scaledHeight) { Rectangle viewportBounds = null; try { gc.getInteractionLock().readLock().lock(); int x = (int)(((-getViewportOffsetX() - rendererBounds.x) / rendererBounds.width) * scaledWidth); int y = (int)(((-getViewportOffsetY() - rendererBounds.y) / rendererBounds.height) * scaledHeight); int width = (int)(((visibleRectangle.width/getScalingZoomFactor()) / rendererBounds.width) * scaledWidth); int height = (int)(((visibleRectangle.height/getScalingZoomFactor()) / rendererBounds.height) * scaledHeight); viewportBounds = new Rectangle(x, y, width, height); } finally { gc.getInteractionLock().readLock().unlock(); } return viewportBounds; } public boolean isDrawLocationOnScreen(Point2D.Double drawLocation) { drawLocation.x *= getScalingZoomFactor(); drawLocation.y *= getScalingZoomFactor(); return visibleRectangle.contains(drawLocation); } public Point getScreenLocationFromDrawLocation(Point2D.Double drawLocation) { Point screenLocation = null; try { gc.getInteractionLock().readLock().lock(); screenLocation = new Point((int)((drawLocation.x + getViewportOffsetX())*getScalingZoomFactor()), (int)((drawLocation.y + getViewportOffsetY())*getScalingZoomFactor())); } finally { gc.getInteractionLock().readLock().unlock(); } return screenLocation; } public Rectangle2D.Double getDrawRectangleFromMouseRectangle(Rectangle mouseRectangle) { Rectangle2D.Double drawRectangle = null; try { gc.getInteractionLock().readLock().lock(); drawRectangle = new Rectangle2D.Double(mouseRectangle.getMinX()/getScalingZoomFactor()-getViewportOffsetX(), mouseRectangle.getMinY()/getScalingZoomFactor()-getViewportOffsetY(), mouseRectangle.getWidth()/getScalingZoomFactor(), mouseRectangle.getHeight()/getScalingZoomFactor()); } finally { gc.getInteractionLock().readLock().unlock(); } return drawRectangle; } public Point2D.Double getDrawPositionFromMousePosition(Point mousePosition) { if ( mousePosition == null ) { return null; } Point2D.Double drawPosition = null; try { gc.getInteractionLock().readLock().lock(); drawPosition = new Point2D.Double(mousePosition.x/getScalingZoomFactor()-getViewportOffsetX(), mousePosition.y/getScalingZoomFactor()-getViewportOffsetY()); } finally { gc.getInteractionLock().readLock().unlock(); } return drawPosition; } public Rectangle getVisibleRectangle() { try { gc.getInteractionLock().readLock().lock(); return visibleRectangle; } finally { gc.getInteractionLock().readLock().unlock(); } } public Point2D.Double getCenterDrawPosition() { try { gc.getInteractionLock().readLock().lock(); return centerLocation; } finally { gc.getInteractionLock().readLock().unlock(); } } public Point getCenterDrawPoint() { Point centerDrawPoint = null; try { gc.getInteractionLock().readLock().lock(); centerDrawPoint = new Point((int)(visibleRectangle.width/(2*getScalingZoomFactor())-getViewportOffsetX()), (int)(visibleRectangle.height/(2*getScalingZoomFactor())-getViewportOffsetY())); } finally { gc.getInteractionLock().readLock().unlock(); } return centerDrawPoint; } public Point2D.Double shiftFromCenterToTopLeft(Point2D.Double centerLocation) { Point2D.Double topLeftLocation = new Point2D.Double(); topLeftLocation.x = centerLocation.x - visibleRectangle.width/2.0; topLeftLocation.y = centerLocation.y - visibleRectangle.height/2.0; return topLeftLocation; } public Point shiftFromCenterToTopLeft(Point centerLocation) { Point topLeftLocation = new Point(); topLeftLocation.x = centerLocation.x - visibleRectangle.width/2; topLeftLocation.y = centerLocation.y - visibleRectangle.height/2; return topLeftLocation; } private void viewportMoved(int viewportX, int viewportY) { if ( gc.getBoardRenderer() != null ) { try { gc.getInteractionLock().readLock().lock(); updateViewportOffset(viewportX, viewportY); } finally { gc.getInteractionLock().readLock().unlock(); } gc.getImageManager().repaintNewImage(); } } private Point2D.Double calculateCenterDrawPosition() { return new Point2D.Double(visibleRectangle.width/(2*getScalingZoomFactor())-getViewportOffsetX(), visibleRectangle.height/(2*getScalingZoomFactor())-getViewportOffsetY()); } public void viewportResized(int viewportWidth, int viewportHeight) { if ( viewportWidth > 0 && viewportHeight > 0 ) { try { gc.getInteractionLock().writeLock().lock(); visibleRectangle.width = viewportWidth; visibleRectangle.height = viewportHeight; gc.getImageManager().createImage(); updateScrollBarsWithCenter(centerLocation); gc.getImageManager().repaintNewImage(); } finally { gc.getInteractionLock().writeLock().unlock(); } } } private void updateViewportOffset(int x, int y) { double xoffset = x / getScalingZoomFactor(); double yoffset = y / getScalingZoomFactor(); gc.getBoardRenderer().getTransform().setToScale(getScalingZoomFactor(),getScalingZoomFactor()); gc.getBoardRenderer().getTransform().translate(-xoffset, -yoffset); visibleRectangle.x = x; visibleRectangle.y = y; centerLocation = calculateCenterDrawPosition(); gc.getBoardRenderer().updateVisibleRenderers(getViewportRendererBounds()); } private void updateViewportOffset(int x, int y, int hExtent, int vExtent, int minX, int maxX, int minY, int maxY) { double xoffset = x / getScalingZoomFactor(); double yoffset = y / getScalingZoomFactor(); gc.getBoardRenderer().getTransform().setToScale(getScalingZoomFactor(),getScalingZoomFactor()); gc.getBoardRenderer().getTransform().translate(-xoffset, -yoffset); visibleRectangle.x = x; visibleRectangle.y = y; visibleRectangle.width = hExtent; visibleRectangle.height = vExtent; maxVisibleRectangle.x = minX; maxVisibleRectangle.y = minY; maxVisibleRectangle.width = maxX - minX; maxVisibleRectangle.height = maxY - minY; centerLocation = calculateCenterDrawPosition(); gc.getBoardRenderer().updateVisibleRenderers(getViewportRendererBounds()); } public void updateScrollBars() { updateScrollBarsWithCenter(null); } public void updateScrollBarsWithCenter() { updateScrollBarsWithCenter(getCenterDrawPosition()); } public void updateScrollBarsWithCenter(Point2D.Double centerPosition) { try { gc.getInteractionLock().writeLock().lock(); rendererBounds = null; rendererBounds = gc.getBoardRenderer().getRendererBounds(); if (rendererBounds == null) { gc.updateBoardSizeFromPanelSize(getScrollPanel().getViewportSize()); rendererBounds = gc.getBoardRenderer().getRendererBounds(); } int xExtent = visibleRectangle.width; int yExtent = visibleRectangle.height; /* int minX = (int)(rendererBounds.getMinX()*getScalingZoomFactor()) - xExtent + SUBTRACT_FROM_MARGIN; int maxX = (int)(rendererBounds.getMaxX()*getScalingZoomFactor()) + xExtent - SUBTRACT_FROM_MARGIN; int minY = (int)(rendererBounds.getMinY()*getScalingZoomFactor()) - yExtent + SUBTRACT_FROM_MARGIN; int maxY = (int)(rendererBounds.getMaxY()*getScalingZoomFactor()) + yExtent - SUBTRACT_FROM_MARGIN; */ int minX = (int)(rendererBounds.getMinX()*getScalingZoomFactor()); int maxX = (int)(rendererBounds.getMaxX()*getScalingZoomFactor()); int minY = (int)(rendererBounds.getMinY()*getScalingZoomFactor()); int maxY = (int)(rendererBounds.getMaxY()*getScalingZoomFactor()); if (maxX-minX<xExtent) { int xDiff = xExtent-(maxX-minX); minX-=xDiff/2; maxX+=xDiff/2; } if (maxY-minY<yExtent) { int yDiff = yExtent-(maxY-minY); minY-=yDiff/2; maxY+=yDiff/2; } int xValue = validate(visibleRectangle.x, minX, maxX, xExtent); int yValue = validate(visibleRectangle.y, minY, maxY, yExtent); if ( centerPosition != null ) { Point2D.Double zoomCenterPosition = new Point2D.Double(); zoomCenterPosition.x = centerPosition.x * getScalingZoomFactor(); zoomCenterPosition.y = centerPosition.y * getScalingZoomFactor(); Point2D.Double topLeftPosition = shiftFromCenterToTopLeft(zoomCenterPosition); xValue = validate((int)Math.round(topLeftPosition.x), minX, maxX, xExtent); yValue = validate((int)Math.round(topLeftPosition.y), minY, maxY, yExtent); } getScrollPanel().update(minX, maxX, xValue, xExtent, minY, maxY, yValue, yExtent); updateViewportOffset(xValue, yValue, xExtent, yExtent, minX, maxX, minY, maxY); if ( !(rendererBounds.getMinX() == rendererBounds.getMaxX() && rendererBounds.getMinY() == rendererBounds.getMaxY()) && centerPosition != null ) { centerLocation = centerPosition; } } finally { gc.getInteractionLock().writeLock().unlock(); } } private int validate(int val, int min, int max, int extent) { if ( val < min ) { return min; } else if ( val > max - extent ) { return max - extent; } else { return val; } } public void scrollBy(int deltaX, int deltaY) { scrollBy(deltaX, deltaY, true); } private void scrollBy(int deltaX, int deltaY, boolean cancelAutoScroll) { if ( cancelAutoScroll ) { stopAutoScroll(); } if ( deltaX != 0 || deltaY != 0 ) { scrollTo(visibleRectangle.x + deltaX, visibleRectangle.y + deltaY); } } public void scrollTo(int x, int y) { try { gc.getInteractionLock().readLock().lock(); final int validX = validate(x, maxVisibleRectangle.x, maxVisibleRectangle.x + maxVisibleRectangle.width, visibleRectangle.width); final int validY = validate(y, maxVisibleRectangle.y, maxVisibleRectangle.y + maxVisibleRectangle.height, visibleRectangle.height); getScrollPanel().update(validX, validY); updateViewportOffset(validX, validY); centerLocation = calculateCenterDrawPosition(); } finally { gc.getInteractionLock().readLock().unlock(); } gc.getImageManager().repaintNewImage(); } private void scrollToCenter(int x, int y) { scrollTo(x-visibleRectangle.width/2, y-visibleRectangle.height/2); } public void scrollToCenterFromPercentBounds(double xPercent, double yPercent) { int xPad = visibleRectangle.width - SUBTRACT_FROM_MARGIN; int yPad = visibleRectangle.height - SUBTRACT_FROM_MARGIN; int x = (int)(xPad + maxVisibleRectangle.x + xPercent * (maxVisibleRectangle.width - 2*xPad)); int y = (int)(yPad + maxVisibleRectangle.y + yPercent * (maxVisibleRectangle.height - 2*yPad)); scrollToCenter(x, y); } private void scrollDrawLocationToCenter(Point2D.Double drawPoint) { scrollToCenter((int)(drawPoint.x * getScalingZoomFactor()), (int)(drawPoint.y * getScalingZoomFactor())); } private int ceilFromZero(double value) { if ( value > 0 ) { return (int)Math.ceil(value); } else { return (int)Math.floor(value); } } private void scale(Point2D.Double point, double scale) { point.x*= scale; point.y*= scale; } public void slowScrollToCenter(Point2D.Double scrollPoint) { stopAutoScroll(); if (scrollPoint!=null) { scrollThread = new AutoScrollThread(scrollPoint); scrollThread.start(); } } class AutoScrollThread extends Thread { private Point2D.Double scrollPoint; public AutoScrollThread(Point2D.Double scrollPoint) { this.scrollPoint = scrollPoint; } public void run() { try { autoScrollLock.lock(); autoScrolling = true; Point2D.Double centerLocation = new Point2D.Double(); centerLocation.setLocation(getCenterDrawPosition()); scale(centerLocation, getScalingZoomFactor()); Point2D.Double visibleScrollPoint = null; try { gc.getInteractionLock().readLock().lock(); visibleScrollPoint = new Point2D.Double(scrollPoint.x,scrollPoint.y);//.getDrawLocation(); scale(visibleScrollPoint, getScalingZoomFactor()); } finally { gc.getInteractionLock().readLock().unlock(); } double distance = visibleScrollPoint.distance(centerLocation); while ( autoScrolling && distance > SCROLL_TO_STEP ) { double distancePercent = 1.0 - (distance - SCROLL_TO_STEP) / distance; double deltaX = visibleScrollPoint.x - centerLocation.x; double deltaY = visibleScrollPoint.y - centerLocation.y; int moveX = ceilFromZero(deltaX * distancePercent); int moveY = ceilFromZero(deltaY * distancePercent); scrollBy(moveX, moveY, false); try { Thread.sleep(SCROLL_TO_DELAY); } catch (InterruptedException ex) { break; } centerLocation.setLocation(getCenterDrawPosition()); scale(centerLocation, getScalingZoomFactor()); try { gc.getInteractionLock().readLock().lock(); visibleScrollPoint = new Point2D.Double(scrollPoint.x,scrollPoint.y); scale(visibleScrollPoint, getScalingZoomFactor()); } finally { gc.getInteractionLock().readLock().unlock(); } distance = visibleScrollPoint.distance(centerLocation); //gc.getInteractionHandler().updateMouseOver(); } if ( autoScrolling ) { try { gc.getInteractionLock().readLock().lock(); scrollDrawLocationToCenter(new Point2D.Double(scrollPoint.x,scrollPoint.y)); } finally { gc.getInteractionLock().readLock().unlock(); } } //gc.getInteractionHandler().updateMouseOver(); autoScrolling = false; } finally { autoScrollLock.unlock(); } } } public void stopAutoScroll() { if ( scrollThread != null && scrollThread.isAlive() ) { autoScrolling = false; try { scrollThread.interrupt(); } catch (Exception ex) { //FIXME: Applets don't allow threads to be interrupted } //gc.getInteractionHandler().updateMouseOver(); } } }