text:
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert, assertNotReached} from 'chrome://resources/js/assert_ts.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {hasKeyModifiers, isRTL} from 'chrome://resources/js/util.m.js';

import {ExtendedKeyEvent, FittingType, Point} from './constants.js';
import {Gesture, GestureDetector, PinchEventDetail} from './gesture_detector.js';
import {PdfPluginElement} from './internal_plugin.js';
import {SwipeDetector, SwipeDirection} from './swipe_detector.js';
import {ViewportInterface} from './viewport_scroller.js';
import {InactiveZoomManager, ZoomManager} from './zoom_manager.js';

export interface ViewportRect {
  x: number;
  y: number;
  width: number;
  height: number;
}

export interface DocumentDimensions {
  width: number;
  height: number;
  pageDimensions: ViewportRect[];
  layoutOptions?: LayoutOptions;
}

export interface LayoutOptions {
  direction: number;
  defaultPageOrientation: number;
  twoUpViewEnabled: boolean;
}

export interface Size {
  width: number;
  height: number;
}

/** @return The area of the intersection of the rects */
function getIntersectionArea(rect1: ViewportRect, rect2: ViewportRect): number {
  const left = Math.max(rect1.x, rect2.x);
  const top = Math.max(rect1.y, rect2.y);
  const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
  const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);

  if (left >= right || top >= bottom) {
    return 0;
  }

  return (right - left) * (bottom - top);
}

/** @return The vector between the two points. */
function vectorDelta(p1: Point, p2: Point): Point {
  return {x: p2.x - p1.x, y: p2.y - p1.y};
}

type HTMLElementWithExtras = HTMLElement&{
  scrollCallback(): void,
  resizeCallback(): void,
};

// TODO(crbug.com/1276456): Would Viewport be better as a Polymer element?
export class Viewport implements ViewportInterface {
  private window_: HTMLElement;
  private scrollContent_: ScrollContent;
  private defaultZoom_: number;

  private viewportChangedCallback_: () => void;
  private beforeZoomCallback_: () => void;
  private afterZoomCallback_: () => void;
  private userInitiatedCallback_: (userInitiated: boolean) => void;

  private allowedToChangeZoom_: boolean = false;
  private internalZoom_: number = 1;

  /**
   * Predefined zoom factors to be used when zooming in/out. These are in
   * ascending order.
   */
  private presetZoomFactors_: number[] = [];

  private zoomManager_: ZoomManager|null = null;
  private documentDimensions_: DocumentDimensions|null = null;
  private pageDimensions_: ViewportRect[] = [];
  private fittingType_: FittingType = FittingType.NONE;
  private prevScale_: number = 1;
  private smoothScrolling_: boolean = false;
  private pinchPhase_: PinchPhase = PinchPhase.NONE;
  private pinchPanVector_: Point|null = null;
  private pinchCenter_: Point|null = null;
  private firstPinchCenterInFrame_: Point|null = null;
  private oldCenterInContent_: Point|null = null;
  private keepContentCentered_: boolean = false;
  private tracker_: EventTracker = new EventTracker();
  private gestureDetector_: GestureDetector;
  private swipeDetector_: SwipeDetector;
  private sentPinchEvent_: boolean = false;
  private fullscreenForTesting_: boolean = false;

  /**
   * @param container The element which contains the scrollable content.
   * @param sizer The element which represents the size of the scrollable
   *     content in the viewport
   * @param content The element which is the parent of the plugin in the viewer.
   * @param scrollbarWidth The width of scrollbars on the page
   * @param defaultZoom The default zoom level.
   */
  constructor(
      container: HTMLElement, sizer: HTMLElement, content: HTMLElement,
      scrollbarWidth: number, defaultZoom: number) {
    this.window_ = container;

    this.scrollContent_ =
        new ScrollContent(this.window_, sizer, content, scrollbarWidth);

    this.defaultZoom_ = defaultZoom;

    this.viewportChangedCallback_ = function() {};
    this.beforeZoomCallback_ = function() {};
    this.afterZoomCallback_ = function() {};
    this.userInitiatedCallback_ = function() {};

    this.gestureDetector_ = new GestureDetector(content);

    this.gestureDetector_.getEventTarget().addEventListener(
        'pinchstart',
        e => this.onPinchStart_(e as CustomEvent<PinchEventDetail>));
    this.gestureDetector_.getEventTarget().addEventListener(
        'pinchupdate',
        e => this.onPinchUpdate_(e as CustomEvent<PinchEventDetail>));
    this.gestureDetector_.getEventTarget().addEventListener(
        'pinchend', e => this.onPinchEnd_(e as CustomEvent<PinchEventDetail>));
    this.gestureDetector_.getEventTarget().addEventListener(
        'wheel', e => this.onWheel_(e as CustomEvent<PinchEventDetail>));

    this.swipeDetector_ = new SwipeDetector(content);

    this.swipeDetector_.getEventTarget().addEventListener(
        'swipe', e => this.onSwipe_(e as CustomEvent<SwipeDirection>));

    // Set to a default zoom manager - used in tests.
    this.setZoomManager(new InactiveZoomManager(this.getZoom.bind(this), 1));

    // Print Preview
    if (this.window_ === document.documentElement ||
        // Necessary check since during testing a fake DOM element is used.
        !(this.window_ instanceof HTMLElement)) {
      window.addEventListener('scroll', this.updateViewport_.bind(this));
      this.scrollContent_.setEventTarget(window);
      // The following line is only used in tests, since they expect
      // |scrollCallback| to be called on the mock |window_| object (legacy).
      (this.window_ as HTMLElementWithExtras).scrollCallback =
          this.updateViewport_.bind(this);
      window.addEventListener('resize', this.resizeWrapper_.bind(this));
      // The following line is only used in tests, since they expect
      // |resizeCallback| to be called on the mock |window_| object (legacy).
      (this.window_ as HTMLElementWithExtras).resizeCallback =
          this.resizeWrapper_.bind(this);
    } else {
      // Standard PDF viewer
      this.window_.addEventListener('scroll', this.updateViewport_.bind(this));
      this.scrollContent_.setEventTarget(this.window_);
      const resizeObserver = new ResizeObserver(_ => this.resizeWrapper_());
      const target = this.window_.parentElement;
      assert(target!.id === 'main');
      resizeObserver.observe(target!);
    }

    document.body.addEventListener(
        'change-zoom', e => this.setZoom(e.detail.zoom));
  }

  /**
   * Sets whether the viewport is in Presentation mode.
   */
  setPresentationMode(enabled: boolean) {
    assert((document.fullscreenElement !== null) === enabled);
    this.gestureDetector_.setPresentationMode(enabled);
    this.swipeDetector_.setPresentationMode(enabled);
  }

  /**
   * Sets the contents of the viewport, scrolling within the viewport's window.
   * @param content The new viewport contents, or null to clear the viewport.
   */
  setContent(content: Node|null) {
    this.scrollContent_.setContent(content);
  }

  /**
   * Sets the contents of the viewport, scrolling within the content's window.
   * @param content The new viewport contents.
   */
  setRemoteContent(content: PdfPluginElement) {
    this.scrollContent_.setRemoteContent(content);
  }

  /**
   * Synchronizes scroll position from remote content.
   */
  syncScrollFromRemote(position: Point) {
    this.scrollContent_.syncScrollFromRemote(position);
  }

  /**
   * Receives acknowledgment of scroll position synchronized to remote content.
   */
  ackScrollToRemote(position: Point) {
    this.scrollContent_.ackScrollToRemote(position);
  }

  setViewportChangedCallback(viewportChangedCallback: () => void) {
    this.viewportChangedCallback_ = viewportChangedCallback;
  }

  setBeforeZoomCallback(beforeZoomCallback: () => void) {
    this.beforeZoomCallback_ = beforeZoomCallback;
  }

  setAfterZoomCallback(afterZoomCallback: () => void) {
    this.afterZoomCallback_ = afterZoomCallback;
  }

  setUserInitiatedCallback(
      userInitiatedCallback: (userInitiated: boolean) => void) {
    this.userInitiatedCallback_ = userInitiatedCallback;
  }

  /**
   * @return The number of clockwise 90-degree rotations that have been applied.
   */
  getClockwiseRotations(): number {
    const options = this.getLayoutOptions();
    return options ? options.defaultPageOrientation : 0;
  }

  /** @return Whether viewport is in two-up view mode. */
  twoUpViewEnabled(): boolean {
    const options = this.getLayoutOptions();
    return !!options && options.twoUpViewEnabled;
  }

  /**
   * Clamps the zoom factor (or page scale factor) to be within the limits.
   * @param factor The zoom/scale factor.
   * @return The factor clamped within the limits.
   */
  private clampZoom_(factor: number): number {
    return Math.max(
        this.presetZoomFactors_[0],
        Math.min(
            factor,
            this.presetZoomFactors_[this.presetZoomFactors_.length - 1]));
  }

  /** @param factors Array containing zoom/scale factors. */
  setZoomFactorRange(factors: number[]) {
    assert(factors.length !== 0);
    this.presetZoomFactors_ = factors;
  }

  /**
   * Converts a page position (e.g. the location of a bookmark) to a screen
   * position.
   * @param point The position on `page`.
   * @return The screen position.
   */
  convertPageToScreen(page: number, point: Point): Point {
    const dimensions = this.getPageInsetDimensions(page);

    // width & height are already rotated.
    const height = dimensions.height;
    const width = dimensions.width;

    const matrix = new DOMMatrix();

    const rotation = this.getClockwiseRotations() * 90;
    // Set origin for rotation.
    if (rotation === 90) {
      matrix.translateSelf(width, 0);
    } else if (rotation === 180) {
      matrix.translateSelf(width, height);
    } else if (rotation === 270) {
      matrix.translateSelf(0, height);
    }
    matrix.rotateSelf(0, 0, rotation);

    // Invert Y position with respect to height as page coordinates are
    // measured from the bottom left.
    matrix.translateSelf(0, height);
    matrix.scaleSelf(1, -1);

    const pointsToPixels = 96 / 72;
    const result = matrix.transformPoint(
        new DOMPoint(point.x * pointsToPixels, point.y * pointsToPixels));
    return {
      x: result.x + PAGE_SHADOW.left,
      y: result.y + PAGE_SHADOW.top,
    };
  }

  /**
   * Returns the zoomed and rounded document dimensions for the given zoom.
   * Rounding is necessary when interacting with the renderer which tends to
   * operate in integral values (for example for determining if scrollbars
   * should be shown).
   * @param zoom The zoom to use to compute the scaled dimensions.
   * @return Scaled 'width' and 'height' of the document.
   */
  private getZoomedDocumentDimensions_(zoom: number): Size|null {
    if (!this.documentDimensions_) {
      return null;
    }
    return {
      width: Math.round(this.documentDimensions_.width * zoom),
      height: Math.round(this.documentDimensions_.height * zoom),
    };
  }

  /** @return A dictionary with the 'width'/'height' of the document. */
  getDocumentDimensions(): Size {
    return {
      width: this.documentDimensions_!.width,
      height: this.documentDimensions_!.height,
    };
  }

  /** @return A dictionary carrying layout options from the plugin. */
  getLayoutOptions(): LayoutOptions|undefined {
    return this.documentDimensions_ ? this.documentDimensions_.layoutOptions :
                                      undefined;
  }

  /** @return ViewportRect for the viewport given current zoom. */
  private getViewportRect_(): ViewportRect {
    const zoom = this.getZoom();
    // Zoom can be 0 in the case of a PDF that is in a hidden iframe. Avoid
    // returning undefined values in this case. See https://crbug.com/1202725.
    if (zoom === 0) {
      return {
        x: 0,
        y: 0,
        width: 0,
        height: 0,
      };
    }
    return {
      x: this.position.x / zoom,
      y: this.position.y / zoom,
      width: this.size.width / zoom,
      height: this.size.height / zoom,
    };
  }

  /**
   * @param zoom Zoom to compute scrollbars for
   * @return Whether horizontal or vertical scrollbars are needed.
   * Public so tests can call it directly.
   */
  documentNeedsScrollbars(zoom: number):
      {horizontal: boolean, vertical: boolean} {
    const zoomedDimensions = this.getZoomedDocumentDimensions_(zoom);
    if (!zoomedDimensions) {
      return {horizontal: false, vertical: false};
    }

    return {
      horizontal: zoomedDimensions.width > this.window_.offsetWidth,
      vertical: zoomedDimensions.height > this.window_.offsetHeight,
    };
  }

  /**
   * @return Whether horizontal and vertical scrollbars are needed.
   */
  documentHasScrollbars(): {horizontal: boolean, vertical: boolean} {
    return this.documentNeedsScrollbars(this.getZoom());
  }

  /**
   * Helper function called when the zoomed document size changes. Updates the
   * sizer's width and height.
   */
  private contentSizeChanged_() {
    const zoomedDimensions = this.getZoomedDocumentDimensions_(this.getZoom());
    if (zoomedDimensions) {
      this.scrollContent_.setSize(
          zoomedDimensions.width, zoomedDimensions.height);
    }
  }

  /** Called when the viewport should be updated. */
  private updateViewport_() {
    this.viewportChangedCallback_();
  }

  /** Called when the browser window size changes. */
  private resizeWrapper_() {
    this.userInitiatedCallback_(false);
    this.resize_();
    this.userInitiatedCallback_(true);
  }

  /** Called when the viewport size changes. */
  private resize_() {
    // Force fit-to-height when resizing happens as a result of entering full
    // screen mode.
    if (document.fullscreenElement !== null) {
      this.fittingType_ = FittingType.FIT_TO_HEIGHT;
      this.window_.dispatchEvent(
          new CustomEvent('fitting-type-changed-for-testing'));
    }

    if (this.fittingType_ === FittingType.FIT_TO_PAGE) {
      this.fitToPageInternal_(false);
    } else if (this.fittingType_ === FittingType.FIT_TO_WIDTH) {
      this.fitToWidth();
    } else if (this.fittingType_ === FittingType.FIT_TO_HEIGHT) {
      this.fitToHeightInternal_(document.fullscreenElement !== null);
    } else if (this.internalZoom_ === 0) {
      this.fitToNone();
    } else {
      this.updateViewport_();
    }
  }

  /** @return The scroll position of the viewport. */
  get position(): Point {
    return {
      x: this.scrollContent_.scrollLeft,
      y: this.scrollContent_.scrollTop,
    };
  }

  /**
   * Scroll the viewport to the specified position.
   * @param position The position to scroll to.
   * @param isSmooth Whether to scroll smoothly.
   */
  setPosition(position: Point, isSmooth: boolean = false) {
    this.scrollContent_.scrollTo(position.x, position.y, isSmooth);
  }

  /** @return The size of the viewport. */
  get size(): Size {
    return {
      width: this.window_.offsetWidth,
      height: this.window_.offsetHeight,
    };
  }

  /** Gets the content size. */
  get contentSize(): Size {
    return this.scrollContent_.size;
  }

  /** @return The current zoom. */
  getZoom(): number {
    return this.zoomManager_!.applyBrowserZoom(this.internalZoom_);
  }

  /** @return The preset zoom factors. */
  get presetZoomFactors(): number[] {
    return this.presetZoomFactors_;
  }

  setZoomManager(manager: ZoomManager) {
    this.resetTracker();
    this.zoomManager_ = manager;
    this.tracker_.add(
        this.zoomManager_!.getEventTarget(), 'set-zoom',
        (e: CustomEvent<number>) => this.setZoom(e.detail));
    this.tracker_.add(
        this.zoomManager_!.getEventTarget(), 'update-zoom-from-browser',
        this.updateZoomFromBrowserChange_.bind(this));
  }

  /**
   * @return The phase of the current pinch gesture for the viewport.
   */
  get pinchPhase(): PinchPhase {
    return this.pinchPhase_;
  }

  /**
   * @return The panning caused by the current pinch gesture (as the deltas of
   *     the x and y coordinates).
   */
  get pinchPanVector(): Point|null {
    return this.pinchPanVector_;
  }

  /**
   * @return The coordinates of the center of the current pinch gesture.
   */
  get pinchCenter(): Point|null {
    return this.pinchCenter_;
  }

  /**
   * Used to wrap a function that might perform zooming on the viewport. This is
   * required so that we can notify the plugin that zooming is in progress
   * so that while zooming is taking place it can stop reacting to scroll events
   * from the viewport. This is to avoid flickering.
   */
  private mightZoom_(f: () => void) {
    this.beforeZoomCallback_();
    this.allowedToChangeZoom_ = true;
    f();
    this.allowedToChangeZoom_ = false;
    this.afterZoomCallback_();
    this.zoomManager_!.onPdfZoomChange();
  }

  private setZoomInternal_(newZoom: number) {
    assert(
        this.allowedToChangeZoom_,
        'Called Viewport.setZoomInternal_ without calling ' +
            'Viewport.mightZoom_.');
    // Record the scroll position (relative to the top-left of the window).
    let zoom = this.getZoom();
    const currentScrollPos = {
      x: this.position.x / zoom,
      y: this.position.y / zoom,
    };

    this.internalZoom_ = newZoom;
    this.contentSizeChanged_();
    // Scroll to the scaled scroll position.
    zoom = this.getZoom();
    this.setPosition({
      x: currentScrollPos.x * zoom,
      y: currentScrollPos.y * zoom,
    });
  }

  /**
   * Sets the zoom of the viewport.
   * Same as setZoomInternal_ but for pinch zoom we have some more operations.
   * @param scaleDelta The zoom delta.
   * @param center The pinch center in plugin coordinates.
   */
  private setPinchZoomInternal_(scaleDelta: number, center: Point) {
    assert(
        this.allowedToChangeZoom_,
        'Called Viewport.setPinchZoomInternal_ without calling ' +
            'Viewport.mightZoom_.');
    this.internalZoom_ = this.clampZoom_(this.internalZoom_ * scaleDelta);

    assert(this.oldCenterInContent_);
    const delta =
        vectorDelta(this.oldCenterInContent_, this.pluginToContent_(center));

    // Record the scroll position (relative to the pinch center).
    const zoom = this.getZoom();
    const currentScrollPos = {
      x: this.position.x - delta.x * zoom,
      y: this.position.y - delta.y * zoom,
    };

    this.contentSizeChanged_();
    // Scroll to the scaled scroll position.
    this.setPosition(currentScrollPos);
  }

  /**
   *  Converts a point from plugin to content coordinates.
   *  @param pluginPoint The plugin coordinates.
   *  @return The content coordinates.
   */
  private pluginToContent_(pluginPoint: Point): Point {
    // TODO(mcnee) Add a helper Point class to avoid duplicating operations
    // on plain {x,y} objects.
    const zoom = this.getZoom();
    return {
      x: (pluginPoint.x + this.position.x) / zoom,
      y: (pluginPoint.y + this.position.y) / zoom,
    };
  }

  /** @param newZoom The zoom level to zoom to. */
  setZoom(newZoom: number) {
    this.fittingType_ = FittingType.NONE;
    this.mightZoom_(() => {
      this.setZoomInternal_(this.clampZoom_(newZoom));
      this.updateViewport_();
    });
  }

  /** @param e Event containing the old browser zoom. */
  private updateZoomFromBrowserChange_(e: CustomEvent<number>) {
    const oldBrowserZoom = e.detail;
    this.mightZoom_(() => {
      // Record the scroll position (relative to the top-left of the window).
      const oldZoom = oldBrowserZoom * this.internalZoom_;
      const currentScrollPos = {
        x: this.position.x / oldZoom,
        y: this.position.y / oldZoom,
      };
      this.contentSizeChanged_();
      const newZoom = this.getZoom();
      // Scroll to the scaled scroll position.
      this.setPosition({
        x: currentScrollPos.x * newZoom,
        y: currentScrollPos.y * newZoom,
      });
      this.updateViewport_();
    });
  }

  /**
   * Gets the width of scrollbars in the viewport in pixels.
   */
  get scrollbarWidth(): number {
    return this.scrollContent_.scrollbarWidth;
  }

  /**
   * Gets the width of overlay scrollbars in the viewport in pixels, or 0 if not
   * using overlay scrollbars.
   */
  get overlayScrollbarWidth(): number {
    return this.scrollContent_.overlayScrollbarWidth;
  }

  /** @return The fitting type the viewport is currently in. */
  get fittingType(): FittingType {
    return this.fittingType_;
  }

  /** @return The y coordinate of the bottom of the given page. */
  private getPageBottom_(index: number): number {
    return this.pageDimensions_[index].y + this.pageDimensions_[index].height;
  }

  /**
   * Get the page at a given y position. If there are multiple pages
   * overlapping the given y-coordinate, return the page with the smallest
   * index.
   * @param y The y-coordinate to get the page at.
   * @return The index of a page overlapping the given y-coordinate.
   */
  private getPageAtY_(y: number): number {
    assert(y >= 0);

    // Drop decimal part of |y| otherwise it can appear as larger than the
    // bottom of the last page in the document (even without the presence of a
    // horizontal scrollbar).
    y = Math.floor(y);

    let min = 0;
    let max = this.pageDimensions_.length - 1;
    if (max === min) {
      return min;
    }

    while (max >= min) {
      const page = min + Math.floor((max - min) / 2);
      // There might be a gap between the pages, in which case use the bottom
      // of the previous page as the top for finding the page.
      const top = page > 0 ? this.getPageBottom_(page - 1) : 0;
      const bottom = this.getPageBottom_(page);

      if (top <= y && y <= bottom) {
        return page;
      }

      // If the search reached the last page just return that page. |y| is
      // larger than the last page's |bottom|, which can happen either because a
      // horizontal scrollbar exists, or the document is zoomed out enough for
      // free space to exist at the bottom.
      if (page === this.pageDimensions_.length - 1) {
        return page;
      }

      if (top > y) {
        max = page - 1;
      } else {
        min = page + 1;
      }
    }

    // Should always return within the while loop above.
    assertNotReached('Could not find page for Y position: ' + y);
  }

  /**
   * Return the last page visible in the viewport. Returns the last index of the
   * document if the viewport is below the document.
   * @return The highest index of the pages visible in the viewport.
   */
  private getLastPageInViewport_(viewportRect: ViewportRect): number {
    const pageAtY = this.getPageAtY_(viewportRect.y + viewportRect.height);

    if (!this.twoUpViewEnabled() || pageAtY % 2 === 1 ||
        pageAtY + 1 >= this.pageDimensions_.length) {
      return pageAtY;
    }

    const nextPage = this.pageDimensions_[pageAtY + 1];
    return getIntersectionArea(viewportRect, nextPage) > 0 ? pageAtY + 1 :
                                                             pageAtY;
  }

  /** @return Whether |point| (in screen coordinates) is inside a page. */
  isPointInsidePage(point: Point): boolean {
    const zoom = this.getZoom();
    const size = this.size;
    const position = this.position;
    const page = this.getPageAtY_((position.y + point.y) / zoom);
    const pageWidth = this.pageDimensions_[page].width * zoom;
    const documentWidth = this.getDocumentDimensions().width * zoom;

    const outerWidth = Math.max(size.width, documentWidth);

    if (pageWidth >= outerWidth) {
      return true;
    }

    const x = point.x + position.x;

    const minX = (outerWidth - pageWidth) / 2;
    const maxX = outerWidth - minX;
    return x >= minX && x <= maxX;
  }

  /**
   * @return The index of the page with the greatest proportion of its area in
   *     the current viewport.
   */
  getMostVisiblePage(): number {
    const viewportRect = this.getViewportRect_();

    const firstVisiblePage = this.getPageAtY_(viewportRect.y);
    const lastPossibleVisiblePage = this.getLastPageInViewport_(viewportRect);
    assert(firstVisiblePage <= lastPossibleVisiblePage);
    if (firstVisiblePage === lastPossibleVisiblePage) {
      return firstVisiblePage;
    }

    let mostVisiblePage = firstVisiblePage;
    let largestIntersection = 0;

    for (let i = firstVisiblePage; i < lastPossibleVisiblePage + 1; i++) {
      const pageArea =
          this.pageDimensions_[i].width * this.pageDimensions_[i].height;

      // TODO(thestig): check whether we can remove this check.
      if (pageArea <= 0) {
        continue;
      }

      const pageIntersectionArea =
          getIntersectionArea(this.pageDimensions_[i], viewportRect) / pageArea;

      if (pageIntersectionArea > largestIntersection) {
        mostVisiblePage = i;
        largestIntersection = pageIntersectionArea;
      }
    }

    return mostVisiblePage;
  }

  /**
   * Compute the zoom level for fit-to-page, fit-to-width or fit-to-height.
   * At least one of {fitWidth, fitHeight} must be true.
   * @param pageDimensions The dimensions of a given page in px.
   * @param fitWidth Whether the whole width of the page needs to be in the
   *     viewport.
   * @param fitHeight Whether the whole height of the page needs to be in the
   *     viewport.
   */
  private computeFittingZoom_(
      pageDimensions: Size, fitWidth: boolean, fitHeight: boolean): number {
    assert(
        fitWidth || fitHeight,
        'Invalid parameters. At least one of fitWidth and fitHeight must be ' +
            'true.');

    // First compute the zoom without scrollbars.
    let zoom = this.computeFittingZoomGivenDimensions_(
        fitWidth, fitHeight, this.window_.offsetWidth,
        this.window_.offsetHeight, pageDimensions.width, pageDimensions.height);

    // Check if there needs to be any scrollbars.
    const needsScrollbars = this.documentNeedsScrollbars(zoom);

    // If the document fits, just return the zoom.
    if (!needsScrollbars.horizontal && !needsScrollbars.vertical) {
      return zoom;
    }

    const zoomedDimensions = this.getZoomedDocumentDimensions_(zoom);
    assert(zoomedDimensions !== null);

    // Check if adding a scrollbar will result in needing the other scrollbar.
    const scrollbarWidth = this.scrollContent_.scrollbarWidth;
    if (needsScrollbars.horizontal &&
        zoomedDimensions.height > this.window_.offsetHeight - scrollbarWidth) {
      needsScrollbars.vertical = true;
    }
    if (needsScrollbars.vertical &&
        zoomedDimensions.width > this.window_.offsetWidth - scrollbarWidth) {
      needsScrollbars.horizontal = true;
    }

    // Compute available window space.
    const windowWithScrollbars = {
      width: this.window_.offsetWidth,
      height: this.window_.offsetHeight,
    };
    if (needsScrollbars.horizontal) {
      windowWithScrollbars.height -= scrollbarWidth;
    }
    if (needsScrollbars.vertical) {
      windowWithScrollbars.width -= scrollbarWidth;
    }

    // Recompute the zoom.
    zoom = this.computeFittingZoomGivenDimensions_(
        fitWidth, fitHeight, windowWithScrollbars.width,
        windowWithScrollbars.height, pageDimensions.width,
        pageDimensions.height);

    return this.zoomManager_!.internalZoomComponent(zoom);
  }

  /**
   * Compute a zoom level given the dimensions to fit and the actual numbers
   * in those dimensions.
   * @param fitWidth Whether to constrain the page width to the window.
   * @param fitHeight Whether to constrain the page height to the window.
   * @param windowWidth Width of the window in px.
   * @param windowHeight Height of the window in px.
   * @param pageWidth Width of the page in px.
   * @param pageHeight Height of the page in px.
   */
  private computeFittingZoomGivenDimensions_(
      fitWidth: boolean, fitHeight: boolean, windowWidth: number,
      windowHeight: number, pageWidth: number, pageHeight: number): number {
    // Assumes at least one of {fitWidth, fitHeight} is set.
    let zoomWidth: number|null = null;
    let zoomHeight: number|null = null;

    if (fitWidth) {
      zoomWidth = windowWidth / pageWidth;
    }

    if (fitHeight) {
      zoomHeight = windowHeight / pageHeight;
    }

    let zoom: number;
    if (!fitWidth && fitHeight) {
      zoom = zoomHeight!;
    } else if (fitWidth && !fitHeight) {
      zoom = zoomWidth!;
    } else {
      // Assume fitWidth && fitHeight
      zoom = Math.min(zoomWidth!, zoomHeight!);
    }

    return Math.max(zoom, 0);
  }

  /** Zoom the viewport so that the page width consumes the entire viewport. */
  fitToWidth() {
    this.mightZoom_(() => {
      this.fittingType_ = FittingType.FIT_TO_WIDTH;
      if (!this.documentDimensions_) {
        return;
      }
      // When computing fit-to-width, the maximum width of a page in the
      // document is used, which is equal to the size of the document width.
      this.setZoomInternal_(
          this.computeFittingZoom_(this.documentDimensions_, true, false));
      this.updateViewport_();
    });
  }

  /**
   * Zoom the viewport so that the page height consumes the entire viewport.
   * @param scrollToTopOfPage Set to true if the viewport should be scrolled to
   *     the top of the current page. Set to false if the viewport should remain
   *     at the current scroll position.
   */
  private fitToHeightInternal_(scrollToTopOfPage: boolean) {
    this.mightZoom_(() => {
      this.fittingType_ = FittingType.FIT_TO_HEIGHT;
      if (!this.documentDimensions_) {
        return;
      }
      const page = this.getMostVisiblePage();
      // When computing fit-to-height, the maximum height of the current page
      // is used.
      const dimensions = {
        width: 0,
        height: this.pageDimensions_[page].height,
      };
      this.setZoomInternal_(this.computeFittingZoom_(dimensions, false, true));
      if (scrollToTopOfPage) {
        this.setPosition({
          x: 0,
          y: this.pageDimensions_[page].y * this.getZoom(),
        });
      }
      this.updateViewport_();
    });
  }

  /** Zoom the viewport so that the page height consumes the entire viewport. */
  fitToHeight() {
    this.fitToHeightInternal_(true);
  }

  /**
   * Zoom the viewport so that a page consumes as much as possible of the it.
   * @param scrollToTopOfPage Whether the viewport should be scrolled to the top
   *     of the current page. If false, the viewport will remain at the current
   *     scroll position.
   */
  private fitToPageInternal_(scrollToTopOfPage: boolean) {
    this.mightZoom_(() => {
      this.fittingType_ = FittingType.FIT_TO_PAGE;
      if (!this.documentDimensions_) {
        return;
      }
      const page = this.getMostVisiblePage();
      // Fit to the current page's height and the widest page's width.
      const dimensions = {
        width: this.documentDimensions_.width,
        height: this.pageDimensions_[page].height,
      };
      this.setZoomInternal_(this.computeFittingZoom_(dimensions, true, true));
      if (scrollToTopOfPage) {
        this.setPosition({
          x: 0,
          y: this.pageDimensions_[page].y * this.getZoom(),
        });
      }
      this.updateViewport_();
    });
  }

  /**
   * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
   * the viewport to the top of the current page.
   */
  fitToPage() {
    this.fitToPageInternal_(true);
  }

  /** Zoom the viewport to the default zoom. */
  fitToNone() {
    this.mightZoom_(() => {
      this.fittingType_ = FittingType.NONE;
      if (!this.documentDimensions_) {
        return;
      }
      this.setZoomInternal_(Math.min(
          this.defaultZoom_,
          this.computeFittingZoom_(this.documentDimensions_, true, false)));
      this.updateViewport_();
    });
  }

  /** Zoom out to the next predefined zoom level. */
  zoomOut() {
    this.mightZoom_(() => {
      this.fittingType_ = FittingType.NONE;
      let nextZoom = this.presetZoomFactors_[0];
      for (let i = 0; i < this.presetZoomFactors_.length; i++) {
        if (this.presetZoomFactors_[i] < this.internalZoom_) {
          nextZoom = this.presetZoomFactors_[i];
        }
      }
      this.setZoomInternal_(nextZoom);
      this.updateViewport_();
    });
  }

  /** Zoom in to the next predefined zoom level. */
  zoomIn() {
    this.mightZoom_(() => {
      this.fittingType_ = FittingType.NONE;
      const maxZoomIndex = this.presetZoomFactors_.length - 1;
      let nextZoom = this.presetZoomFactors_[maxZoomIndex];
      for (let i = maxZoomIndex; i >= 0; i--) {
        if (this.presetZoomFactors_[i] > this.internalZoom_) {
          nextZoom = this.presetZoomFactors_[i];
        }
      }
      this.setZoomInternal_(nextZoom);
      this.updateViewport_();
    });
  }

  private pageUpDownSpaceHandler_(e: KeyboardEvent, formFieldFocused: boolean) {
    // Avoid scrolling if the space key is down while a form field is focused
    // on since the user might be typing space into the field.
    if (formFieldFocused && e.key === ' ') {
      this.window_.dispatchEvent(new CustomEvent('scroll-avoided-for-testing'));
      return;
    }

    const isDown = e.key === 'PageDown' || (e.key === ' ' && !e.shiftKey);
    // Go to the previous/next page if we are fit-to-page or fit-to-height.
    if (this.isPagedMode_()) {
      isDown ? this.goToNextPage() : this.goToPreviousPage();
      // Since we do the movement of the page.
      e.preventDefault();
    } else if (isCrossFrameKeyEvent(e)) {
      // Web scrolls by a fraction of the viewport height. Use the same
      // fractional value as `cc::kMinFractionToStepWhenPaging` in
      // cc/input/scroll_utils.h. The values must be kept in sync.
      const MIN_FRACTION_TO_STEP_WHEN_PAGING = 0.875;
      const scrollOffset = (isDown ? 1 : -1) * this.size.height *
          MIN_FRACTION_TO_STEP_WHEN_PAGING;
      this.setPosition(
          {
            x: this.position.x,
            y: this.position.y + scrollOffset,
          },
          this.smoothScrolling_);
    }

    this.window_.dispatchEvent(new CustomEvent('scroll-proceeded-for-testing'));
  }

  private arrowLeftRightHandler_(e: KeyboardEvent, formFieldFocused: boolean) {
    if (formFieldFocused || hasKeyModifiers(e)) {
      return;
    }

    // Go to the previous/next page if there are no horizontal scrollbars.
    const isRight = e.key === 'ArrowRight';
    if (!this.documentHasScrollbars().horizontal) {
      isRight ? this.goToNextPage() : this.goToPreviousPage();
      // Since we do the movement of the page.
      e.preventDefault();
    } else if (isCrossFrameKeyEvent(e)) {
      const scrollOffset = (isRight ? 1 : -1) * SCROLL_INCREMENT;
      this.setPosition(
          {
            x: this.position.x + scrollOffset,
            y: this.position.y,
          },
          this.smoothScrolling_);
    }
  }

  private arrowUpDownHandler_(e: KeyboardEvent, formFieldFocused: boolean) {
    if (formFieldFocused || hasKeyModifiers(e)) {
      return;
    }

    // Go to the previous/next page if Presentation mode is on.
    const isDown = e.key === 'ArrowDown';
    if (document.fullscreenElement !== null) {
      isDown ? this.goToNextPage() : this.goToPreviousPage();
      e.preventDefault();
    } else if (isCrossFrameKeyEvent(e)) {
      const scrollOffset = (isDown ? 1 : -1) * SCROLL_INCREMENT;
      this.setPosition({
        x: this.position.x,
        y: this.position.y + scrollOffset,
      });
    }
  }

  /**
   * Handle certain directional key events.
   * @param formFieldFocused Whether a form field is currently focused.
   * @return Whether the event was handled.
   */
  handleDirectionalKeyEvent(e: KeyboardEvent, formFieldFocused: boolean):
      boolean {
    switch (e.key) {
      case ' ':
        this.pageUpDownSpaceHandler_(e, formFieldFocused);
        return true;
      case 'PageUp':
      case 'PageDown':
        if (hasKeyModifiers(e)) {
          return false;
        }
        this.pageUpDownSpaceHandler_(e, formFieldFocused);
        return true;
      case 'ArrowLeft':
      case 'ArrowRight':
        this.arrowLeftRightHandler_(e, formFieldFocused);
        return true;
      case 'ArrowDown':
      case 'ArrowUp':
        this.arrowUpDownHandler_(e, formFieldFocused);
        return true;
      default:
        return false;
    }
  }

  /**
   * Go to the next page. If the document is in two-up view, go to the left page
   * of the next row. Public for tests.
   */
  goToNextPage() {
    const currentPage = this.getMostVisiblePage();
    const nextPageOffset =
        (this.twoUpViewEnabled() && currentPage % 2 === 0) ? 2 : 1;
    this.goToPage(currentPage + nextPageOffset);
  }

  /**
   * Go to the previous page. If the document is in two-up view, go to the left
   * page of the previous row. Public for tests.
   */
  goToPreviousPage() {
    const currentPage = this.getMostVisiblePage();
    let previousPageOffset = -1;

    if (this.twoUpViewEnabled()) {
      previousPageOffset = (currentPage % 2 === 0) ? -2 : -3;
    }

    this.goToPage(currentPage + previousPageOffset);
  }

  /**
   * Go to the given page index.
   * @param page the index of the page to go to. zero-based.
   */
  goToPage(page: number) {
    this.goToPageAndXY(page, 0, 0);
  }

  /**
   * Go to the given y position in the given page index.
   * @param page the index of the page to go to. zero-based.
   */
  goToPageAndXY(page: number, x: number|undefined, y: number|undefined) {
    this.mightZoom_(() => {
      if (this.pageDimensions_.length === 0) {
        return;
      }
      if (page < 0) {
        page = 0;
      }
      if (page >= this.pageDimensions_.length) {
        page = this.pageDimensions_.length - 1;
      }
      const dimensions = this.pageDimensions_[page];

      // If `x` or `y` is not a valid number or specified, then that
      // coordinate of the current viewport position should be retained.
      const currentCoords = this.retrieveCurrentScreenCoordinates_();
      if (x === undefined || Number.isNaN(x)) {
        x = currentCoords.x;
      }
      if (y === undefined || Number.isNaN(y)) {
        y = currentCoords.y;
      }

      this.setPosition({
        x: (dimensions.x + x) * this.getZoom(),
        y: (dimensions.y + y) * this.getZoom(),
      });
      this.updateViewport_();
    });
  }

  setDocumentDimensions(documentDimensions: DocumentDimensions) {
    this.mightZoom_(() => {
      const initialDimensions = !this.documentDimensions_;
      this.documentDimensions_ = documentDimensions;

      // Override layout direction based on isRTL().
      if (this.documentDimensions_.layoutOptions) {
        if (isRTL()) {
          // `base::i18n::TextDirection::RIGHT_TO_LEFT`
          this.documentDimensions_.layoutOptions.direction = 1;
        } else {
          // `base::i18n::TextDirection::LEFT_TO_RIGHT`
          this.documentDimensions_.layoutOptions.direction = 2;
        }
      }

      this.pageDimensions_ = this.documentDimensions_.pageDimensions;
      if (initialDimensions) {
        this.setZoomInternal_(Math.min(
            this.defaultZoom_,
            this.computeFittingZoom_(this.documentDimensions_, true, false)));
        this.setPosition({x: 0, y: 0});
      }
      this.contentSizeChanged_();
      this.resize_();
    });
  }

  /** @return The bounds for page `page` minus the shadows. */
  getPageInsetDimensions(page: number): ViewportRect {
    const pageDimensions = this.pageDimensions_[page];
    const shadow = PAGE_SHADOW;
    return {
      x: pageDimensions.x + shadow.left,
      y: pageDimensions.y + shadow.top,
      width: pageDimensions.width - shadow.left - shadow.right,
      height: pageDimensions.height - shadow.top - shadow.bottom,
    };
  }

  /**
   * Get the coordinates of the page contents (excluding the page shadow)
   * relative to the screen.
   * @param page The index of the page to get the rect for.
   * @return A rect representing the page in screen coordinates.
   */
  getPageScreenRect(page: number): ViewportRect {
    if (!this.documentDimensions_) {
      return {x: 0, y: 0, width: 0, height: 0};
    }
    if (page >= this.pageDimensions_.length) {
      page = this.pageDimensions_.length - 1;
    }

    const pageDimensions = this.pageDimensions_[page];

    // Compute the page dimensions minus the shadows.
    const insetDimensions = this.getPageInsetDimensions(page);

    // Compute the x-coordinate of the page within the document.
    // TODO(raymes): This should really be set when the PDF plugin passes the
    // page coordinates, but it isn't yet.
    const x = (this.documentDimensions_.width - pageDimensions.width) / 2 +
        PAGE_SHADOW.left;
    // Compute the space on the left of the document if the document fits
    // completely in the screen.
    const zoom = this.getZoom();
    let spaceOnLeft =
        (this.size.width - this.documentDimensions_.width * zoom) / 2;
    spaceOnLeft = Math.max(spaceOnLeft, 0);

    return {
      x: x * zoom + spaceOnLeft - this.scrollContent_.scrollLeft,
      y: insetDimensions.y * zoom - this.scrollContent_.scrollTop,
      width: insetDimensions.width * zoom,
      height: insetDimensions.height * zoom,
    };
  }

  /**
   * Check if the current fitting type is a paged mode.
   * In a paged mode, page up and page down scroll to the top of the
   * previous/next page and part of the page is under the toolbar.
   * @return Whether the current fitting type is a paged mode.
   */
  private isPagedMode_(): boolean {
    return (
        this.fittingType_ === FittingType.FIT_TO_PAGE ||
        this.fittingType_ === FittingType.FIT_TO_HEIGHT);
  }

  /**
   * Retrieves the in-screen coordinates of the current viewport position.
   */
  private retrieveCurrentScreenCoordinates_(): Point {
    const currentPage = this.getMostVisiblePage();
    const dimension = this.pageDimensions_[currentPage];
    const x = this.position.x / this.getZoom() - dimension.x;
    const y = this.position.y / this.getZoom() - dimension.y;
    return {x: x, y: y};
  }

  /**
   * Handles a navigation request to a destination from the current controller.
   * @param x The in-screen x coordinate for the destination.
   *     If `x` is undefined, retain current x coordinate value.
   * @param y The in-screen y coordinate for the destination.
   *     If `y` is undefined, retain current y coordinate value.
   */
  handleNavigateToDestination(
      page: number, x: number|undefined, y: number|undefined, zoom: number) {
    if (zoom) {
      this.setZoom(zoom);
    }
    this.goToPageAndXY(page, x, y);
  }

  setSmoothScrolling(isSmooth: boolean) {
    this.smoothScrolling_ = isSmooth;
  }

  /** @param point The position to which to scroll the viewport. */
  scrollTo(point: Partial<Point>) {
    let changed = false;
    const newPosition = this.position;
    if (point.x !== undefined && point.x !== newPosition.x) {
      newPosition.x = point.x;
      changed = true;
    }
    if (point.y !== undefined && point.y !== newPosition.y) {
      newPosition.y = point.y;
      changed = true;
    }

    if (changed) {
      this.setPosition(newPosition);
    }
  }

  /** @param delta The delta by which to scroll the viewport. */
  scrollBy(delta: Point) {
    const newPosition = this.position;
    newPosition.x += delta.x;
    newPosition.y += delta.y;
    this.scrollTo(newPosition);
  }

  /** Removes all events being tracked from the tracker. */
  resetTracker() {
    if (this.tracker_) {
      this.tracker_.removeAll();
    }
  }

  /**
   * Dispatches a gesture external to this viewport.
   */
  dispatchGesture(gesture: Gesture) {
    this.gestureDetector_.getEventTarget().dispatchEvent(
        new CustomEvent(gesture.type, {detail: gesture.detail}));
  }

  /**
   * Dispatches a swipe event of |direction| external to this viewport.
   */
  dispatchSwipe(direction: SwipeDirection) {
    this.swipeDetector_.getEventTarget().dispatchEvent(
        new CustomEvent('swipe', {detail: direction}));
  }

  /**
   * A callback that's called when an update to a pinch zoom is detected.
   */
  private onPinchUpdate_(e: CustomEvent<PinchEventDetail>) {
    // Throttle number of pinch events to one per frame.
    if (this.sentPinchEvent_) {
      return;
    }

    this.sentPinchEvent_ = true;
    window.requestAnimationFrame(() => {
      this.sentPinchEvent_ = false;
      this.mightZoom_(() => {
        const {direction, center, startScaleRatio} = e.detail;
        this.pinchPhase_ = direction === 'out' ? PinchPhase.UPDATE_ZOOM_OUT :
                                                 PinchPhase.UPDATE_ZOOM_IN;

        const scaleDelta = startScaleRatio! / this.prevScale_;
        if (this.firstPinchCenterInFrame_ != null) {
          this.pinchPanVector_ =
              vectorDelta(center, this.firstPinchCenterInFrame_);
        }

        const needsScrollbars =
            this.documentNeedsScrollbars(this.zoomManager_!.applyBrowserZoom(
                this.clampZoom_(this.internalZoom_ * scaleDelta)));

        this.pinchCenter_ = center;

        // If there's no horizontal scrolling, keep the content centered so
        // the user can't zoom in on the non-content area.
        // TODO(mcnee) Investigate other ways of scaling when we don't have
        // horizontal scrolling. We want to keep the document centered,
        // but this causes a potentially awkward transition when we start
        // using the gesture center.
        if (!needsScrollbars.horizontal) {
          this.pinchCenter_ = {
            x: this.window_.offsetWidth / 2,
            y: this.window_.offsetHeight / 2,
          };
        } else if (this.keepContentCentered_) {
          this.oldCenterInContent_ = this.pluginToContent_(this.pinchCenter_);
          this.keepContentCentered_ = false;
        }

        this.fittingType_ = FittingType.NONE;

        this.setPinchZoomInternal_(scaleDelta, center);
        this.updateViewport_();
        this.prevScale_ = startScaleRatio!;
      });
    });
  }

  /**
   * A callback that's called when the end of a pinch zoom is detected.
   */
  private onPinchEnd_(e: CustomEvent<PinchEventDetail>) {
    // Using rAF for pinch end prevents pinch updates scheduled by rAF getting
    // sent after the pinch end.
    window.requestAnimationFrame(() => {
      this.mightZoom_(() => {
        const {center, startScaleRatio} = e.detail;
        this.pinchPhase_ = PinchPhase.END;
        const scaleDelta = startScaleRatio! / this.prevScale_;
        this.pinchCenter_ = center;

        this.setPinchZoomInternal_(scaleDelta, this.pinchCenter_);
        this.updateViewport_();
      });

      this.pinchPhase_ = PinchPhase.NONE;
      this.pinchPanVector_ = null;
      this.pinchCenter_ = null;
      this.firstPinchCenterInFrame_ = null;
    });
  }

  /**
   * A callback that's called when the start of a pinch zoom is detected.
   */
  private onPinchStart_(e: CustomEvent<PinchEventDetail>) {
    // Disable pinch gestures in Presentation mode.
    if (document.fullscreenElement !== null) {
      return;
    }

    // We also use rAF for pinch start, so that if there is a pinch end event
    // scheduled by rAF, this pinch start will be sent after.
    window.requestAnimationFrame(() => {
      this.pinchPhase_ = PinchPhase.START;
      this.prevScale_ = 1;
      this.oldCenterInContent_ = this.pluginToContent_(e.detail.center);

      const needsScrollbars = this.documentNeedsScrollbars(this.getZoom());
      this.keepContentCentered_ = !needsScrollbars.horizontal;
      // We keep track of beginning of the pinch.
      // By doing so we will be able to compute the pan distance.
      this.firstPinchCenterInFrame_ = e.detail.center;
    });
  }

  /**
   * A callback that's called when a Presentation mode wheel event is detected.
   */
  private onWheel_(e: CustomEvent<PinchEventDetail>) {
    if (e.detail.direction === 'down') {
      this.goToNextPage();
    } else {
      this.goToPreviousPage();
    }
  }

  getGestureDetectorForTesting(): GestureDetector {
    return this.gestureDetector_;
  }

  /**
   * A callback that's called when a left/right swipe is detected in
   * Presentation mode.
   */
  private onSwipe_(e: CustomEvent<SwipeDirection>) {
    // Left and right swipes are enabled only in Presentation mode.
    if (document.fullscreenElement === null && !this.fullscreenForTesting_) {
      return;
    }

    if ((e.detail === SwipeDirection.RIGHT_TO_LEFT && !isRTL()) ||
        (e.detail === SwipeDirection.LEFT_TO_RIGHT && isRTL())) {
      this.goToNextPage();
    } else {
      this.goToPreviousPage();
    }
  }

  enableFullscreenForTesting() {
    this.fullscreenForTesting_ = true;
  }
}

/**
 * Enumeration of pinch states.
 * This should match PinchPhase enum in pdf/pdf_view_web_plugin.cc.
 */
export enum PinchPhase {
  NONE = 0,
  START = 1,
  UPDATE_ZOOM_OUT = 2,
  UPDATE_ZOOM_IN = 3,
  END = 4,
}

/**
 * The increment to scroll a page by in pixels when up/down/left/right arrow
 * keys are pressed. Usually we just let the browser handle scrolling on the
 * window when these keys are pressed but in certain cases we need to simulate
 * these events.
 */
const SCROLL_INCREMENT: number = 40;

/**
 * Returns whether a keyboard event came from another frame.
 */
function isCrossFrameKeyEvent(keyEvent: ExtendedKeyEvent): boolean {
  return !!keyEvent.fromPlugin || !!keyEvent.fromScriptingAPI;
}

/**
 * The width of the page shadow around pages in pixels.
 */
export const PAGE_SHADOW:
    {top: number, bottom: number, left: number, right: number} = {
      top: 3,
      bottom: 7,
      left: 5,
      right: 5,
    };

/**
 * A wrapper around the viewport's scrollable content. This abstraction isolates
 * details concerning internal vs. external scrolling behavior.
 */
class ScrollContent {
  private readonly container_: HTMLElement;
  private readonly sizer_: HTMLElement;
  private target_: EventTarget|null = null;
  private readonly content_: HTMLElement;
  private readonly scrollbarWidth_: number;
  private plugin_: PdfPluginElement|null = null;
  private width_: number = 0;
  private height_: number = 0;
  private scrollLeft_: number = 0;
  private scrollTop_: number = 0;
  private unackedScrollsToRemote_: number = 0;

  /**
   * @param container The element which contains the scrollable content.
   * @param sizer The element which represents the size of the scrollable
   *     content.
   * @param content The element which is the parent of the scrollable content.
   * @param scrollbarWidth The width of any scrollbars.
   */
  constructor(
      container: HTMLElement, sizer: HTMLElement, content: HTMLElement,
      scrollbarWidth: number) {
    this.container_ = container;
    this.sizer_ = sizer;
    this.content_ = content;
    this.scrollbarWidth_ = scrollbarWidth;
  }

  /**
   * Sets the target for dispatching "scroll" events.
   */
  setEventTarget(target: EventTarget) {
    this.target_ = target;
  }

  /**
   * Dispatches a "scroll" event.
   */
  private dispatchScroll_() {
    this.target_ && this.target_.dispatchEvent(new Event('scroll'));
  }

  /**
   * Sets the contents, switching to scrolling locally.
   * @param content The new contents, or null to clear.
   */
  setContent(content: Node|null) {
    if (content === null) {
      this.sizer_.style.display = 'none';
      return;
    }
    this.attachContent_(content);

    // Switch to local content.
    this.sizer_.style.display = 'block';
    if (!this.plugin_) {
      return;
    }
    this.plugin_ = null;

    // Synchronize remote state to local.
    this.updateSize_();
    this.scrollTo(this.scrollLeft_, this.scrollTop_);
  }

  /**
   * Sets the contents, switching to scrolling remotely.
   * @param content The new contents.
   */
  setRemoteContent(content: PdfPluginElement) {
    this.attachContent_(content);

    // Switch to remote content.
    const previousScrollLeft = this.scrollLeft;
    const previousScrollTop = this.scrollTop;
    this.sizer_.style.display = 'none';
    assert(!this.plugin_);
    this.plugin_ = content;

    // Synchronize local state to remote.
    this.updateSize_();
    this.scrollTo(previousScrollLeft, previousScrollTop);
  }

  /**
   * Attaches the contents to the DOM.
   * @param content The new contents.
   */
  private attachContent_(content: Node) {
    // We don't actually replace the content in the DOM, as the controller
    // implementations take care of "removal" in controller-specific ways:
    //
    // 1. Plugin content gets added once, then hidden and revealed using CSS.
    // 2. Ink content gets removed directly from the DOM on unload.
    if (!content.parentNode) {
      this.content_.appendChild(content);
    }
    assert(content.parentNode === this.content_);
  }

  /**
   * Synchronizes scroll position from remote content.
   */
  syncScrollFromRemote(position: Point) {
    if (this.unackedScrollsToRemote_ > 0) {
      // Don't overwrite scroll position while scrolls-to-remote are pending.
      // TODO(crbug.com/1246398): Don't need this if we make this synchronous
      // again, by moving more logic to the plugin frame.
      return;
    }

    if (this.scrollLeft_ === position.x && this.scrollTop_ === position.y) {
      // Don't trigger scroll event if scroll position hasn't changed.
      return;
    }

    this.scrollLeft_ = position.x;
    this.scrollTop_ = position.y;
    this.dispatchScroll_();
  }

  /**
   * Receives acknowledgment of scroll position synchronized to remote content.
   */
  ackScrollToRemote(position: Point) {
    assert(this.unackedScrollsToRemote_ > 0);

    if (--this.unackedScrollsToRemote_ === 0) {
      // Accept remote adjustment when there are no pending scrolls-to-remote.
      this.scrollLeft_ = position.x;
      this.scrollTop_ = position.y;
    }

    this.dispatchScroll_();
  }

  get scrollbarWidth(): number {
    return this.scrollbarWidth_;
  }

  get overlayScrollbarWidth(): number {
    let overlayScrollbarWidth = 0;

    // TODO(crbug.com/1286009): Support overlay scrollbars on all platforms.
    // <if expr="is_macosx">
    overlayScrollbarWidth = 16;
    // </if>
    // <if expr="not is_macosx">
    if (this.plugin_) {
      overlayScrollbarWidth = this.scrollbarWidth_;
    }
    // </if>

    return overlayScrollbarWidth;
  }

  /** Gets the content size. */
  get size(): Size {
    return {
      width: this.width_,
      height: this.height_,
    };
  }

  /** Sets the content size. */
  setSize(width: number, height: number) {
    this.width_ = width;
    this.height_ = height;
    this.updateSize_();
  }

  private updateSize_() {
    if (this.plugin_) {
      this.plugin_.postMessage({
        type: 'updateSize',
        width: this.width_,
        height: this.height_,
      });
    } else {
      this.sizer_.style.width = `${this.width_}px`;
      this.sizer_.style.height = `${this.height_}px`;
    }
  }

  /**
   * Gets the scroll offset from the left edge.
   */
  get scrollLeft(): number {
    return this.plugin_ ? this.scrollLeft_ : this.container_.scrollLeft;
  }

  /**
   * Gets the scroll offset from the top edge.
   */
  get scrollTop(): number {
    return this.plugin_ ? this.scrollTop_ : this.container_.scrollTop;
  }

  /**
   * Scrolls to the given coordinates.
   * @param isSmooth Whether to scroll smoothly.
   */
  scrollTo(x: number, y: number, isSmooth: boolean = false) {
    if (this.plugin_) {
      // TODO(crbug.com/1277228): Can get NaN if zoom calculations divide by 0.
      x = Number.isNaN(x) ? 0 : x;
      y = Number.isNaN(y) ? 0 : y;

      // Clamp coordinates to scroll limits. Note that the order of min() and
      // max() operations is significant, as each "maximum" can be negative.
      const maxX = this.maxScroll_(
          this.width_, this.container_.clientWidth,
          this.height_ > this.container_.clientHeight);
      const maxY = this.maxScroll_(
          this.height_, this.container_.clientHeight,
          this.width_ > this.container_.clientWidth);

      if (this.container_.dir === 'rtl') {
        // Right-to-left. If `maxX` > 0, clamp to [-maxX, 0]. Else set to 0.
        x = Math.min(Math.max(-maxX, x), 0);
      } else {
        // Left-to-right. If `maxX` > 0, clamp to [0, maxX]. Else set to 0.
        x = Math.max(0, Math.min(x, maxX));
      }
      // If `maxY` > 0, clamp to [0, maxY]. Else set to 0.
      y = Math.max(0, Math.min(y, maxY));

      // To match the DOM's scrollTo() behavior, update the scroll position
      // immediately, but fire the scroll event later (when the remote side
      // triggers `ackScrollToRemote()`).
      this.scrollLeft_ = x;
      this.scrollTop_ = y;

      ++this.unackedScrollsToRemote_;
      this.plugin_.postMessage({
        type: 'syncScrollToRemote',
        x: this.scrollLeft_,
        y: this.scrollTop_,
        isSmooth: isSmooth,
      });
    } else {
      this.container_.scrollTo(x, y);
    }
  }

  /**
   * Computes maximum scroll position.
   * @param maxContent The maximum content dimension.
   * @param maxContainer The maximum container dimension.
   * @param hasScrollbar Whether to compensate for a scrollbar.
   */
  private maxScroll_(
      maxContent: number, maxContainer: number, hasScrollbar: boolean): number {
    if (hasScrollbar) {
      maxContainer -= this.scrollbarWidth_;
    }

    // This may return a negative value, which is fine because scroll positions
    // are clamped to a minimum of 0.
    return maxContent - maxContainer;
  }
}

redpajama_set_name: RedPajamaGithub


Since early 2007, the Library of Congress has been developing Library of Congress Genre/Form Terms for Library and Archival Materials (LCGFT), whose terms describe what something is rather than what it is about, as subject headings do. In January 2015 the Policy and Standards Division (PSD) will approve approximately 175 "general" genre/form terms for inclusion in LCGFT. The terms describe works such as abstracts, dictionaries, periodicals, and yearbooks, which are not specific to a particular discipline, and also include some other terms that do not fall neatly into a particular discipline (e.g., Logic puzzles; Passenger lists).
The terms appear on Tentative List 1513, to be approved on January 12, 2015. PSD is requesting comments from the library community; please email Janis Young at [email protected] through January 9, 2015.
The "general terms" project was a partnership undertaken by PSD and the ALA/ALCTS Subject Analysis Committee's Subcommittee on Genre/Form Implementation, which formed the General Terms Working Group. PSD thanks the members of the Working Group for their time and effort. Special thanks are also due to MARCIVE, Inc., who created MARC records for the proposals from a Word document provided by the Working Group.
The Working Group based its list primarily on headings and form subdivisions in Library of Congress Subject Headings (LCSH), which was used as the starting point for the project. LCSH terminology was adapted as necessary for LCGFT; for instance, the LCSH form subdivision –Biography will be approved in the plural form Biographies for LCGFT. The Working Group was not strictly limited to existing LCSH terminology, though, and has proposed a number of terms that do not appear in LCSH.
This project is different from the previous four (moving images, sound recordings, law materials, and cartographic materials) in that there was no clear "broadest term" for the wide assortment of terms. Instead of a single collocation point, therefore, there are ten: Commemorative works; Creative nonfiction; Derivative works; Discursive works; Ephemera; Illustrated works; Informational works; Instructional and educational works; Recreational works; and Tactile works. It is not expected that these broadest terms will be assigned frequently, if at all. Instead, more specific terms should be assigned (e.g., a transcript of an interview would be assigned Interviews, not its BT, Discursive works). For the outline of the hierarchies, see Hierarchies for General Genre/Form Terms.
Also unlike previous projects, the approval of the "general" terms will impact the hierarchies of some of the terms already in LCGFT. Some of those changes appear on Tentative List 1513 (e.g., addition of the BT Interviews to Filmed interviews), while additional revisions will be approved on future Tentative Lists.
245 00 $a George Washington : $b an illustrated biography / $c by David A. Adler.


text:
The Puzzle of Jim Slater
By The Falconer Apr 22, 2007, 2:43pm EDT
Share All sharing options for: The Puzzle of Jim Slater
So I'm starting to think about the off-season and give some thought as to what the 2007 Thrashers team might look like. I'm plugging some salary numbers into my handy Excel spreadsheet and making estimates about how much money certain players will receive and I get to Jim Slater.
Slater was a very late 1st round pick and signed an entry level contract that pays him close to the maximum allowed for an entry level contract. This summer Slater will be a restricted free agent which essentially means that as long as the Thrashers make him a qualifying offer he is extremely likely to sign a deal with Atlanta.
Slater currently makes $900,000 a year according to Irish Blues exhaustive salary website. The Thrashers qualifying offer has to include a 5% raise. The team must send Slater a written offer for a contract with a salary of 945,000 in order to qualify him. Slater can decline the offer or sign it (If he declines it the Thrashers retain their right to match any offers made to him by another team.) But the problem is that he will be way overpaid at $945,000 a year.
Using Irish Blues NHL salary data I collected together a list of all NHL forwards making between 1 million and 800,000--players who make about the same amount of money as Slater (and were regular NHLers--minor league prospects were excluded). I came up with 39 players in Slater's salary neighborhood and then looked at their points-per-game. Jim Slater is right down the bottom of the list of comparables. I have him ranked 33 out of 39 just behind Patrik Stefan's .268 PPG and just above Brian Sutherby's .246 PPG. To be perfectly honest Slater is getting paid nearly $1 million a year because of where he was selected in the draft rather than his NHL performance.
So if you're the Thrashers is it worth it? Do you make him a qualifying offer that pays him double what he is worth or do you let him walk? Slater was in effect a checking line player most of the year making twice as much money as most checkers (who often make around the NHL minimum of $450,000). Is Slater really equal to Vigier and Larsen combined?
Slater has great speed but very poor balance. I can't think of a NHL player who falls over every single game like Jim does. He is fast but lacks lateral quickness because of his balance issues and thus he is not used as penalty killer. He doesn't score all that much. In the playoffs he centered our 4th line. Can this team afford a $1 million 4th line guy?
Now you might say that Slater was poorly used and he has the potential to do more, and I would probably agree with that statement. But Bob Hartley is coming back as coach I have no reason to think he will used in a significantly different fashion in the coming season. After all the Thrashers had a gaping hole at center all year and Hartley never gave him more than occasional chances to show what he could do. If DW signs some more centers I can see Slater being buried on the 4th line all next season.
The harsh reality is that in a salary cap league every dollar you overpay for a player is a dollar wasted and the Thrasher wasted their money on Slater last year. He gave them nothing that a 500,000 checker couldn't have provided. Two years ago the Thrashers faced a similar problem when Stefan's contract expired. Stefan ended up taking a slight pay cut in exchange for a two-year deal. To put it nicely, that didn't work out very well. The team would have been better off to let him walk and spend that money on a cheaper more effective guy.
Personally I'm much rather see those dollars go towards resigning Pascal Dupuis who was a solid PK guy, crash line member and even got some PP time late in the playoffs. I'd rather see those dollars go towards bringing Jon Sim back for another season. But the team found it hard to give up on a former 1st rounder last time and I doubt we will see them let Slater depart for nothing either. Slater probably has more upside than Stefan did, but it still requires a bit of "wish-casting" (as opposed to "forecasting") to justify his salary requirements.

redpajama_set_name: RedPajamaCommonCrawl


The Stress Management Tips worksheet presents a number of valuable and easy-to-follow tips for dealing with stress. These tips encourage a healthy mindset about stress, the maintenance of a healthy lifestyle, and they encourage the use of several valuable coping skills. This handout works well as a prompt for group discussion, or as a take-home reminder of what your clients have learned in session.


The European ACP Women's Rapid and Blitz Chess Championship 2018 took place from 29 March to 1 April in Tbilisi, Georgia. In total there were 78 players from 16 federations.
The total prize fund of 20.500 EUR was shared between the rapid (14.500 EUR) and the blitz (6.000 EUR).
The ACP contributed with 5000 EUR to the prize fund, courtesy of the company Archibaldchess, and managed to secure conditions for 8 ACP Premiums.
The rapid part of the event was won by our Premium Member IM Elizabeth Paehtz from Germany who had a better tie-break than several other Premium Members, all of them on 8/11: IM Anastasia Bodnaruk (silver), GM Anna Muzychuk (bronze), IM Anna Ushenina and GM Alexandra Kosteniuk. You can see the full standings here.
The blitz part was won by our Premium GM Anna Muzychuk who scored 10/13. There was a three-way share for second place with 9.5 points with silver going to our Premium IM Elizabeth Paehtz and bronze to our Premium GM Monika Socko. Fourth place was taken by IM Anna Matnadze. The full standings can be found here.


text:
'Isn't Pawan Kalyan like a chameleon?': Prakash Raj hits out for alliance with BJP
28 November 2020, 7:54 am ·2-min read
Joining in the heated political atmosphere in the run-up to the Greater Hyderabad Municipal Corporation (GHMC) elections, actor Prakash Raj stoked a controversy by calling actor and Jana Sena Party chief Pawan Kalyan a "chameleon". The statement from the actor came against the backdrop of the Jana Sena's surprising electoral alliance with the BJP in GHMC polls, during an interview with Telugu news channel TV9.
In Mukha-Mukhi (Face to Face) show, hosted by journalist Murali Krishna, Prakash Raj made bold statements while declaring his support to the K Chandrasekhar Rao (KCR) led TRS party in the GHMC polls.
When asked about Pawan Kalyan's support to BJP in the elections and withdrawal of candidates from the contest, Prakash Raj said that he was "disappointed" with Pawan Kalyan's move. He said, "I really don't understand what happened to him, I'm very disappointed. You (Pawan Kalyan) are a leader by yourself, you have a party (Jana Sena), I wonder why you are looking up/pointing at another leader."
He further said that Pawan Kalyan has not considered the BJP's share and his party's vote share in the last elections and is riding on Narendra Modi's BJP. The actor also said that Pawan supported BJP by praising them as a "wonder" in 2014 but five years later, distanced himself saying they have betrayed and is now again seeing the BJP as a leader.
He said, "If he is changing three-four times, isn't he a chameleon (Oosaravelli)?" While referring to Pawan's claims of alliance with the BJP for the sake of larger national interest, Prakash Raj said, "I wonder you believe they are doing that in this election, what are they talking about?"
Prakash Raj also suggested that Pawan Kalyan should join the BJP instead of riding on its shoulders, while stating that the BJP needs 1000 births before rivalling KCR.
Though Pawan's Jana Sena had initially roped in the candidates for the GHMC elections, it has asked its candidates to withdraw their nominations for the sake of "larger interest".
ప్రకాష్ రాజ్ కి నా ans pic.twitter.com/Nu3WKdqMzr
— Naga Babu Konidela (@NagaBabuOffl) November 27, 2020
However, Pawan's brother Nagababu who is also an actor, responded strongly to Prakash Raj. He said that every useless person is criticising Jana Sena leader Pawan Kalyan, while stating that there are "larger interests" and party's interests lie in Jana Sena supporting the BJP in GHMC elections.
Nagababu also said that he understood Prakash Raj's political oscillation when he failed to answer BJP leader Subramany Swamy in a debate. He has also asked Prakash Raj to be a good human before criticising an "unselfish leader" like Pawan Kalyan, while accusing him of harassing cinema producers for money.


text:
Our Bury St Edmunds welcomes latest town centre research...
Visitors to Bury St Edmunds are demanding better parking facilities as a new study shows that on certain days of the week the town's car parks are busier than ever. But, as in previous years' reports, there are clear reasons why the town is busy with cafes and restaurants highlighted as among the most positive aspects of the town centre, together with its retail offer and physical appearance. All are rated significantly higher here compared to large towns nationally and 96% of those who took part in the survey said they would recommend a visit to Bury St Edmunds.
The figures are revealed in a 'benchmarking' study looking at how the town is perceived by the people who use it. The findings will be presented to businesses in the Business Improvement District (BID) tonight (July 19) at the Our Bury St Edmunds Annual General Meeting.
Our Bury St Edmunds Chief Executive Mark Cordell has welcomed the report, while acknowledging that there is no room for complacency. He said: "This survey has reinforced the view that Bury St Edmunds is still a great place to live, work and run a business. It's perceived as considerably cleaner, safer and more attractive as a town than other large towns nationally and people are keen to spend money here – 40% of town centre users typically spend more than £20 on their visits to the town.
"We asked people for their honest opinions on why they do – or don't – visit Bury St Edmunds and the improvements they'd like to see. We have commissioned this survey for a number of years and parking has always been the area in which we get most calls for improvement. I hope this is something that we and other agencies can look at very seriously in the next twelve months, along with the other areas that our users rated less positively."
In many aspects of the report, Bury St Edmunds scored well above the national average for large and small town centres. The significant findings of the research included:
96% of people said they would recommend a visit to Bury St Edmunds to others – which is 28% higher than other towns. Bury's car parks are busier on both market days and non-market days than the national average. Only 19% of spaces are empty on a market day and 35% on a non-market day. A new question, introduced this year, about provision of public toilets in the town saw 37% rate the facilities as a negative aspect with a number of comments calling for better provision. 34% of respondents called for better transport links to Bury St Edmunds, a slight increase on previous years.
The study was commissioned for the sixth year running and was carried out by researchers from People and Places via an online survey and face to face interviews. The full report will be considered by the board of Our Bury St Edmunds in the coming weeks.
Address 2nd Floor, 2 Woolhall Street, Bury St Edmunds, IP33 1LA
IP33 1LA
Gibbs Denley Financial Services
Everyone should receive the best financial and wealth management advice, whether you are an individual or a business.
HSBC Personal Banking offers a range of bank accounts with online banking 24/7
Offers a range of products includes car, home, van, taxi and specialist insurance.
Cooper Lomaz
Committed to delivering an unrivalled range of recruitment services covering permanent, contract, and interim appointments.


WE'VE JUST TAKEN THIS LOVELY CORSA IN PART EXCHANGE WHICH WE SOLD 5 YEARS AGO. IT COMES WITH LOTS OF SERVICE HISTORY INCLUDING MOTS. FINISHED IN SILVER METALLIC. SMALL ENGINE SO MIGHT SUIT SOMEONE FOR THEIR FIRST CAR MAYBE? GOOD SPECIFICATION INCLUDES AIR CONDITIONING, ELECTRIC WINDOWS AND MIRRORS AND STEERING WHEEL AUDIO CONTROLS.


Sept. 8, 1998, the Opening Ceremony of the 2nd China International Fair for Investment & Trade was held at Xiamen Fushan International Exhibition Hall. Minister of MOFTEC Mr. Shi Guangsheng, Secretary of CPC Fujian Committee Mr. Chen Mingyi, Secretary General of UNCTAD Mr. Rubens Ricupero, cut the ribbon at the opening ceremony.This fair attracted 6377 overseas business people of 156 delegations from 60 countries and regions. 42 Member units took part in the 2nd CIFIT. Hubei, Gansu, Heilongjiang, Henan, Shannxi, Hainan, Tibet, Guangxi, JinanCity, and CCPIT became new member units.
Two high level seminars, namely, Forum on Director Investment Strategies of Multinational Corporation and Symposium on China's Foreign Investment Policies, were held during the Fair.
Wu Yi, State Councilor of PRC, Secretary General of UNCTAD Mr. Rubens Ricupero, President of Japan Matsushita Denko Co. Ltd, Vice President of International Finance Cooperation Mr. Jemal-ud-din Kassum, Minister of MOFTEC Mr. Shi Guangsheng, Deputy Director General of State Development Planning Commission Mr. Li Rongrong, Vice Minister of General Administration of Customs Mr. Zhao Guanghua, Vice Governor of the People's Bank of China Mr. Liu Mingkang, Vice Minister of the Construction Mr. Song Chunhua, Minister of Labour and Social Security Mr. Wang Dongjin were delivered their speeches on the two seminars respectively.
The 2nd CIFIT had 1606 contracts signed with a total amount of 5.52 billion USD. The trade volume totaled to 1.053 billion USD.


"We've all gotta crawl". Thanks to every single one of you who came out to my shows on my album launch; Byron Bay-Gold Coast-Brisbane-Melbourne-Sydney.
It really means a lot to have your support. It's a long apprenticeship …and you lifted me. Thank you for celebrating the songs and the journey with me.


text:
Quick Bio: Jeni LeGon
23 July 2021 23 July 2021 ~ Lifelongdancestudent
Over the last year or so of online talks and events, I've learned about so many different legendary tap dance artists, and written about a few of them too. This time, I thought I would write about one of the pioneering women of the artform – Jeni LeGon:
Born: 14th August 1916 in Chicago, Illinois as Jennie Ligon.
Died: 7 December 2012 in Vancouver, British Columbia.
Known For: Being one of the first black women to develop a successful solo dance career, for dancing in trousers (unusual for women at the time) and for being the only black woman to dance on screen with Bill 'Bojangles' Robinson, who was regularly paired with Shirley Temple.
Training: She developed her art on the streets of Chicago's South Side and went on to win a tap dance contest in Savannah, Georgia. At 13 years old, she got her first musical theatre job, dancing as a soubrette. She went professional by age 14 when she started performing with Count Basie. Later, in 1943 she took lessons in African dance from the amazing Katherine Dunham, dancer, anthropologist and creator of the Dunham Technique.
Style: Flash – acrobatics, flips, double spins, knee drops, flying splits, toe stands
Early Career: In 1931 she started performing across the Southern US states with the Whitman Sisters' all female chorus (the Whitman Sisters were four African American sisters whose successful touring company challenged racial stereotypes, ran for over 40 years and was the best paid act on the TOBA circuit). Later, Jeni formed the LeGon & Lane tap duo with her half-sister Willa Mae Lane in 1933. She danced in speciality acts in the Detroit nightclubs and then went on to dance in Los Angeles.
Hollywood & Beyond: Jeni was discovered in 1935 by manager Earl Dancer and she signed an extended contract with MGM Studios. Her first film casting was Broadway Melody of 1936, starring Eleanor Powell, but the contract was cancelled soon after because the studio "didn't want to have two [female solo] dancers", which basically meant they didn't want her to outshine Powell. In the end, LeGon worked as a dance consultant and dance director for MGM, who didn't know how to market a solo black female dancer. She said "I didn't fit in at that time as one of the kids next door". As was the case in those days, many of the Hollywood roles she did get cast her as some type of servant or maid. It was only when she starred in all-black films with people such as Cab Calloway, such as Hi De Ho (1947), that she said she got the chance "to be the heroine, to get kissed". Her first actual screen role was in Hooray for Love (1935) where she danced with Bill Robinson. The film also starred Fats Waller, who she went on to work with for much of her career.
In 1936 she got some work in the UK, appearing on the West End stage in C.B Cochran's revue, Follow the Sun and in the film Dishonour Bright (1936).
Movie Credits: Hooray for Love (1935), Dishonour Bright (1936), Ali Baba Goes to Town (1937), This Was Paris (1937), Start Cheering (1938), Fools for Scandal (1938), Double Deal (1939), I Can't Give You Anything But Love (1940), Birth Of The Blues (1941), Bahama Passage (1941), Sundown (1941), Arabian Nights (1941), While Thousands Cheered (1943), Stormy Weather (1943), Hi De Ho (1947), Easter Parade (1948), I Shot Jesse James (1949), Somebody Loves Me (1952), Bright Road (1953), Documentary – Jeni LeGon: Living in a Great Big Way (1999), Bones (2001).
Later Life: Jeni quit Hollywood in the 1950s, studied percussion, and continued to work in dance and music, including teaching and running drama workshops. She relocated to Vancouver in Canada, a place she found much more welcoming than the US.
I decided I wanted to be in show business. I used to sneak out of school and go see Duke Ellington and Cab Calloway and all the wonderful bands they had at that time
I've had a dance school all my life
I danced like a boy
Hollywood was a black and white world
[MGM] were paying me $1,250 a week and telling me I wasn't good enough to eat in their dining room
Black Filmmakers Hall of Fame
National Congress of Black Women
Doctorate of Performing Arts in American Dance, Oklahoma City University (2002)
International Tap Dance Hall of Fame, American Tap Dance Foundation (2002)
Modern References:
Jeni LeGon is the subject of obsession for one of the protagonists in Zadie Smith's novel Swing Time, that I am yet to read!
BFI https://www2.bfi.org.uk/news-opinion/sight-sound-magazine/features/jeni-legon-hollywood-dance-pioneer-swing-time
The History Makers https://www.thehistorymakers.org/biography/jeni-legon-38
Vancouver Sun https://vancouversun.com/Entertainment/african-american-tap-dancing-diva-jeni-legon-dies-at-age-96-in-vancouver-with-video?r
The Province https://theprovince.com/entertainment/celebrity/im-doing-ok-and-im-living-in-a-great-big-way-jeni-legon-often-stole-the-spotlight-dancing-with-the-biggest-stars-of-the-20th-century
American Tap Dance Foundation https://www.atdf.org/hall-of-fame-bios
Malone, Jacqui, Steppin' on the Blues: The Visible Rhythms of African American Dance, University of Illinois Press, Urbana and Chicago (1996)
Valis-Hill, Constance, Tap Dancing America: A Cultural History, Oxford University Press, New York (2010)
Posted in Uncategorised americantapblackhistoryDance Historyjeni legonswingtimetapdancetapdancehistory
< Previous A Holiday, A Decision & A Festival
Next > Swinging at the Cotton Club


text:
CSE:PSE 0.045 CAD | FRA:PNX 0.0270 EUR | Zinc: $1.17/lb USD
Spur Zinc Project, USA
Pinargozu Zinc Mine
Akkaya Zinc Project
Investors Hub
Press & Interviews
Message From Steve Williams
One Pager
Hello, I'm Steve Williams, CEO of Pasinex Resources. And I'm here broadcasting to you because I really think it be part of a CEO's role is to communicate transparency with investors and shareholders so, we're starting that process right here.
Let me tell you what Pasinex is doing, our vision and what we're doing. Our vision is rooted in three things: base metals and a specific focus in zinc. The reason we're focused in zinc is we think zinc is a metal that's really going to move in terms of price in the next two to three years. There's a lot of closures of zinc mines in the world and so we're expecting that the zinc price this year and next year is gonna go up significantly, so we're building zinc to take advantage that.
Second thing, is we're focused on exploration and mining in Turkey. The reason for Turkey, great geology, highly faltered, lots of mountains, and so, great opportunity for exploration. Also, Turkey, you can do business. You can build a mine in Turkey, you can get it permitted, you can get community support, it's a country rooted in mining, supports mining. So, that's why we're in business in Turkey.
The third thing that we we're rooted in our pillar of our vision is to be a mining company. We're not just trying to be an exploration company, we're going the full mile. Our aim is to build a company that does mining, produces cash flow, and I really think that that's what investors want. So, we're rooted and being a mining company in Turkey. By having decided that we're going to be a mining company in Turkey, it was important to us to have a local partner. You need to have a local partner who has, you know, government connections, community connections, connections with suppliers. And as soon as we got to Turkey, we went out and looking for a partner and we ended up with a Turkish part called, Akmetal.
They are well established to mining come in Turkey, they've got three mines, just a great partner and they really got us rolling on our zinc project in Turkey. Our zinc projects in Turkey, we've got two, one's called Pinargozu, one's called, Akkaya. Two properties, they're just back-to-back properties.
Our priority focus at the moment is the Pinargozu property. In the Pinargozu property, we're drilling from the surface, we're drilling from underground. We've got two underground adits and we're drilling from underground. And as of mid last year, we've started mining on Pinargozu, we've now mined over 10,000 tons of high-grade zinc, 32% zinc from the Pinargozu mine.
Our objective this year is to build our first resource on Pinargozu and issue a 43-101, probably called a three and continue with drilling both the Pinargozu and our Akkaya property next door.


text:
Oaxis InkCase Plus Achieves Kickstarter Funding Goal in 3 Hours
Last updated on July 16th, 2014 3 Comments
Oaxis is coming out with a new E Ink phone case called the InkCase Plus that works as a secondary screen with all Bluetooth-enabled Android smartphones running Android 4.0 and up.
Yesterday Oaxis launched a kickstarter campaign to raise funding for the InkCase Plus with a goal of $100,000. It only took 3 hours to hit that number; now a day later the total has risen to over $145,000 with over 900 backers.
The InkCase Plus is the second generation model from Oaxis; the original InkCase was limited to just a few smartphones and had to be attached to the back of them. The new model is a lot more versatile. It works with a wide variety of Android phones by using a cover called the FitCase, a leather case that is designed to hold the phone as well as the InkCase Plus.
The FitCase also allows you to flip the E Ink screen to face either outward or inward. You can remove it from the case entirely to use separately as well.
The E Ink screen communicates with the phone via Bluetooth and can display anything from ebooks to emails, notifications, messages, photos, and you can even use it to receive incoming calls.
The InkCase Plus has a 3.5-inch E Ink screen with a resolution of 360 x 600 (200dpi). The 500mAh battery gives the InkCase enough juice to last up to 3400 pages of reading, or 19 hours of continuous use, and 7 days on standby.
If everything goes to plan, Oaxis anticipates starting to send shipments to backers in October 2014.
InkCase Plus Specifications
3.5″ E Ink display
360 x 600 display resolution
16 shades of grayscale
3.7V 500mAh battery
32MB memory
Supports Android 4.0 and above, better for above Android 4.3
Dimensions: 106.7 x 58.4 x 5.2mm
e ink, inkcase
Scribd Launches Windows App and Updates iOS and Android Apps
Kindle Unlimited: Amazon to Offer eBook Subscription Service for $10/month
3 Responses to "Oaxis InkCase Plus Achieves Kickstarter Funding Goal in 3 Hours"
Sjaak July 16, 2014 at 8:08 pm
Cool! That is best of both worlds, thanks for posting!
Joan July 18, 2014 at 12:07 am
Cool! I like the design.
softsmart July 18, 2014 at 12:20 am
Thanks for posting. It is really interesting.


Simon Snow just wants to relax and savour his last year at the Watford School of Magicks, but no one will let him. His girlfriend broke up with him, his best friend is a pest and his mentor keeps trying to hide him away in the mountains where maybe he'll be safe. Simon can't even enjoy the fact that his room-mate and longtime nemesis is missing, because he can't stop worrying about the evil git. Plus there are ghosts. And vampires. And actual evil things trying to shut Simon down. When you're the most powerful magician the world has ever known, you never get to relax and savour anything.
Based on the characters Simon and Baz who featured in Rainbow Rowell's bestselling novel Fangirl, Carry On is a ghost story, a love story, a mystery and a melodrama. It has just as much kissing and talking as you'd expect from a Rainbow Rowell story - but far, far more monsters.
Rainbow Rowell lives in Omaha, Nebraska. FANGIRL is her second YA novel and a New York Times bestseller, as is her first novel, ELEANOR AND PARK.


Goodridge G-Stop High Performance Brakeline race-bred street brake lines are constructed from the finest quality hose, consisting of a PTFE inner hose covered by braided stainless steel. In addition to providing a more responsive and firm brake pedal by eliminating the spongy feel that often accompanies stock rubber brake lines, G-Stop High Performance Brakelines are abrasion and corrosion resistant to stay good looking for a long time to come.
Goodridge G-Stop High Performance Brakeline Kits are direct bolt-on replacements for all of the rubber brake lines in the vehicles brake system. (Three to six brake lines are included in each kit depending on the vehicles original design). To ensure proper fit and easy installation, hose ends and brake line brackets that match the vehicles original equipment style are used for each application.


Trees, shrubs, and colorful flowers create a beautiful landscape around The Windsor Apartment community's charming exterior. Quiet courtyard and patio areas offer spots to relax or socialize in the shade, and outdoor spa access means you can rejuvenate during a free afternoon or evening.
Indoors, the apartments' main features are light and space, with plenty of room to arrange your furniture and décor the way you'd like. Check out our photo gallery and a video tour for an inside glance at Windsor bedrooms, bathrooms, kitchens, outdoor areas, and more.


text:
5 Ways Ayurveda Helps Us Take Health Into Our Own Hands
Happiness Lifestyle
By Teacher and Ayurveda Practitioner Ripa Ajmera
Ripa Ajmera Teacher and Ayurveda Practitioner
Founder of Whole Yoga & Ayurveda, Writer, Yoga Teacher, Ayurveda Health Practitioner.
Dating its origins to more than 5,000 years ago in the Indian subcontinent, Ayurveda (the science of life) is the world's oldest system of healing. Ayurveda is known as the sister science of Yoga, which is found most often in spas and celebrity circles today than in the hands of the general public, where it was once practiced with great fervor in ancient India.
Having been abolished by British colonists for many years, Ayurveda is now reemerging worldwide as a sustainable health solution. So, why Ayurveda? What makes this ancient science so unique, effective, and inspiring in our modern times?
1. Ayurveda addresses the root causes of disease and imbalance, rather than just mere symptom management.
In the modern medical system, which is based more on lab results, testing, and research than principles, there is an emphasis upon managing one's symptoms while searching for root causes of health problems 'out there' somewhere (think germs, genes, etc.). The search process takes place primarily via the use of computers and other modern medical equipment.
In Ayurveda, however, we are trained to dig deep to discover the root cause(s) within a person's own life, whether that be their diet, daily routine, work, relationships, thoughts, or some combination of all of these. The search process takes place right with the patient via asking the right questions and getting to know the person deeply.
Once determined, Ayurveda employs its principles, which have withstood the test of time, to work to reverse the root causes. Ayurveda's principles can be applied to any difficulty, even the most chronic conditions.
2. In Ayurveda, each person is treated as a unique individual and is provided customized wellness solutions.
Nowadays, when we go to a doctor, we are typically examined primarily at the level of the body and given a one-size-fits-all prescription to deal with our disease. And if we exhibit even the slightest bit of mental anguish, we are automatically labeled as being bipolar, depressed, or hyperactive, and accordingly given either antidepressants or ADHD medication to cope.
The western medical system is designed such that doctors are encouraged to treat diseases, rather than people.
Ayurveda practitioners, however, spend more quality time with patients—building a relationship, and thereby focusing on the person first, and the disease or imbalances later. A lot of care is put into developing the most optimal diet, daily or seasonal routine, exercise regimen, yoga and meditation practice, herbal medicines (only if needed), and more for each person.
3. Ayurveda has a truly holistic definition of health that includes body, mind, soul, and senses.
A child who spends his entire day watching destructive and violent movies may start feeling depressed and hopeless due to what he is taking in through his eyes and ears, not necessarily due to inherent or genetic mental imbalances. But this same child may still easily be written off by doctors as depressed and given Prozac, which gives him side effects that keep him coming back to the doctor in an endless, vicious cycle.
In Ayurveda, the human being is seen as an integrated whole, with not only a body and mind, but also a soul and five senses—which are like the five doorways through which we absorb and perceive the world around us. Because an Ayurveda practitioner would dig deep into the child's daily routines and habits as part of the initial intake process, this practitioner would determine excess intake of violent films as a root cause of the child's depression.
The Ayurveda practitioner would not resort to prescribing medications right away. The practitioner would rather recommend the child spend more time in nature and only watch movies and read books that instill hope, joy, and inspiration, as a start.
4. There is an entire sub-branch of Ayurveda that focuses exclusively on how to actually be healthy.
The reason why a classically trained Ayurveda practitioner will not automatically resort to writing prescriptions is because there is an amazing two-pronged scope to the practice of this science. The first is known in Sanskrit as "Svasthasya Svasthya Rakshanam," which means "protecting the health of the healthy." The second part of Ayurveda's scope is therapeutics to skillfully treat disease.
Ayurveda has a sub-branch called Svasthavritta, which literally means "the protocols by which one can remain healthy." Before prescribing medications, Ayurveda practitioners have a toolbox of daily and seasonal routines, balanced food, sleep, and sex protocols (the three pillars of health, according to Ayurveda), yoga and spiritual practices, healthy exercise patterns, and communication strategies to equip you with.
5. Ayurveda empowers us to take health into our own hands.
Investing the time to learn new tools for your Svasthavritta health toolbox allows you to not only reclaim the health that Ayurveda teaches is our birthright – but to sustain it, too. There is no such thing as developing a dependency on the doctor in this system of health. In this way, Ayurveda does not attempt to "fix" patients, but rather to truly set us free.
What's more is that Ayurveda teaches us that there is, in fact, a deep part of us, our innermost essence, which is always completely healthy. To learn and practice Ayurveda means to connect with countless incredible, drug-free ways to experience your wholeness. Grounded in unshakeable principles that have withstood the test of time, Ayurveda is just as beneficial today as it was thousands of years ago.


Best Gallery on DA, by far. Your work is incredible, Elreviae. There are not enough props in existence for me to give you, my friend. Regardless, keep up the amazing work!
So happy i came across this, It's like your in my head.
bringing the things you see in your mind to life for all to enjoy is noble and generous.


Optimum Motorsport is recognised as the world's top Ginetta team, having notched up British GT4 title successes and high profile endurance race victories during a long-standing and lucrative association with the flourishing British sportscar marque.
Shaun Goff, who has over a decade of experience in motor racing, has been at the helm of the Optimum Motorsport since 2012 and saw his outfit to the 2013 and 2016 British GT4 titles and the 2018 GT3 Championship. The Wakefield-based team has also taken the top step of the SP3 class podium in the 24H Dubai and 12H Mugello.
The team stepped up to GT3-specification machinery for the first time in 2016, fielding the first new-generation customer Audi R8 LMS in rounds of the British GT Championship and CREVENTIC 24H Series alongside its existing Ginetta GT4 and GT5 programmes.
Notably, both Mike Robinson and Graham Johnson prevailed in their second full-time British GT season with Optimum Motorsport, as the teammates took their Ginetta G55 to the top in 2016's hugely competitive GT4 title race. This was followed up in 2017 with the Pro-Am title.
For 2018, Optimum Motorsport returned to the GT3 ranks, with Flick Haigh and Jonny Adam. In a stunning debut season with the Aston Martin Vantage GT3, the team clinched the drivers' title at the final round having proven race-winning speed at multiple rounds of the championship, and great consistency throughout the season.
In addition, the team competed in the GT Open with Oliver Wilkinson and Bradley Ellis, taking multiple podiums ahead of a championship challenge in 2019.
Optimum return to British GT to defend their GT3 title in 2019, while also fielding two Aston Martin entries for the first time in what is a three-car programme.


My little "monsters" love to play with play - doh. I was at first buying the one from the play - store, you know the one that's suppose to be friendly and without chemicals for our children. My mother then told me that when I was a little girl she made her own play- doh for my siblings and me. I then started to search on internet about it and found many recipes. Some of them were ok but other not. A college of mine suggested me to try her recipe that she makes for her children and for the kids at school. It was amazing and it had a little bit of magic in it.
So today I thought that it was a perfect opportunity to test it out cause my monkeys are sick with flue and we can't go outside and play with the snow. And the little ones helped out too. So we are going to show you how it is done.
Put all the dry ingredients one by one in a large bowl.
And then the liquid ingredients one by one.
And then the boiling water.
Mix all the ingredients with your hand until they become a dough.
Attention!!! the dough is very hot because of the water and always wear gloves to avoid discolor your hands from the food color.
and shape it well to spread the glitter everywhere!
Voila!!!! The magical sparkling homemade play doo is ready!!!
Finally a recipe that doesn't need cooking all the ingredients! I love the glitter in playdough!!
Thank you Niki. It's really easy to make it. And the glitter gives it a little bit of magic. Thank you for stopping by.
Καλε σε λεω σε 5 λεπτα ειναι ετοιμη και αν την βαλεις σε ταπερακι ή σακουλα που κλεινει καλα, κραταει μεχρι και 15 μερες. Εγω την αλλαζω καθε βδομαδα.


text:
La gara dei 50 metri stile libero femminili dei Giochi della XXX Olimpiade si è svolta il 3 e il 4 agosto 2012. Hanno partecipato 73 atlete

La gara è stata vinta dall'olandese Ranomi Kromowidjojo con il tempo di 24"05 (nuovo record olimpico), mentre l'argento e il bronzo sono andati rispettivamente ad Aljaksandra Herasimenja e a Marleen Veldhuis.

Programma

Record
Prima della competizione, i record olimpici e mondiali erano i seguenti:

Durante l'evento è stato migliorato il seguente record:

Risultati

Batterie

Spareggio

Semifinali

Finale

Collegamenti esterni
 London2012.com , Women's 50 meters Freestyle

sl 050 F

redpajama_set_name: RedPajamaWikipedia