github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/ccl/src/views/clusterviz/containers/map/zoom.ts (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Licensed as a CockroachDB Enterprise file under the Cockroach Community
     4  // License (the "License"); you may not use this file except in compliance with
     5  // the License. You may obtain a copy of the License at
     6  //
     7  //     https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt
     8  
     9  import _ from "lodash";
    10  import * as vector from "src/util/vector";
    11  import * as d3 from "d3";
    12  
    13  // Box is an immutable construct for a box.
    14  export class Box {
    15    // Compute a minimum bounding box for a supplied collection of boxes.
    16    static boundingBox(...boxes: Box[]): Box {
    17      if (_.isEmpty(boxes)) {
    18        return null;
    19      }
    20  
    21      const left = d3.min(boxes, b => b.left());
    22      const top = d3.min(boxes, b => b.top());
    23      const right = d3.max(boxes, b => b.right());
    24      const bottom = d3.max(boxes, b => b.bottom());
    25      return new Box(left, top, right - left, bottom - top);
    26    }
    27  
    28    constructor(private x: number, private y: number, private w: number, private h: number) { }
    29  
    30    width() {
    31      return this.w;
    32    }
    33  
    34    height() {
    35      return this.h;
    36    }
    37  
    38    right() {
    39      return this.x + this.w;
    40    }
    41  
    42    left() {
    43      return this.x;
    44    }
    45  
    46    top() {
    47      return this.y;
    48    }
    49  
    50    bottom() {
    51      return this.y + this.h;
    52    }
    53  
    54    origin(): Point {
    55      return [this.x, this.y];
    56    }
    57  
    58    size(): Size {
    59      return [this.w, this.h];
    60    }
    61  
    62    center(): Point {
    63      return [this.x + this.w / 2, this.y + this.h / 2];
    64    }
    65  
    66    scale(scale: number): Box {
    67      return new Box(this.x, this.y, this.w * scale, this.h * scale);
    68    }
    69  
    70    translate(vec: Point): Box {
    71      return new Box(this.x + vec[0], this.y + vec[1], this.w, this.h);
    72    }
    73  }
    74  
    75  // Point is a [number, number] which represents a 2 dimensional vector.
    76  type Point = [number, number];
    77  
    78  // Size is a [number, number] which represents a width/height pair.
    79  type Size = [number, number];
    80  
    81  export class ZoomTransformer {
    82    // Bounding box of the scene.
    83    private _bounds: Box;
    84    // Size of the viewport.
    85    private _viewportSize: Size;
    86  
    87    // Current scale of the zoom.
    88    private _scale: number;
    89    // Current translation of the zoom.
    90    private _translate: Point;
    91  
    92    // Construct a new ZoomTransformer for the given bounding box and viewportSize.
    93    // The area is initially centered over the center of the bounding box.
    94    constructor(bounds: Box, viewportSize: Size) {
    95      this._bounds = bounds;
    96      this._viewportSize = viewportSize;
    97      this._scale = this.minScale();
    98      this.centerOnBox(bounds);
    99    }
   100  
   101    minScale() {
   102      // Increase scaling if we are below the minimum.
   103      const boundsSize = this._bounds.size();
   104      return Math.max(
   105        this._viewportSize[0] / boundsSize[0],
   106        this._viewportSize[1] / boundsSize[1],
   107      );
   108    }
   109  
   110    scale() {
   111      return this._scale;
   112    }
   113  
   114    translate() {
   115      return this._translate;
   116    }
   117  
   118    viewportSize() {
   119      return this._viewportSize;
   120    }
   121  
   122    withViewportSize(viewportSize: Size): ZoomTransformer {
   123      const newZoom = _.clone(this);
   124      newZoom._viewportSize = viewportSize;
   125      newZoom.adjustZoom();
   126      return newZoom;
   127    }
   128  
   129    withScaleAndTranslate(scale: number, translate: Point) {
   130      const newZoom = _.clone(this);
   131      newZoom._scale = scale;
   132      newZoom._translate = translate;
   133      newZoom.adjustZoom();
   134      return newZoom;
   135    }
   136  
   137    // zoomedToBox returns a ZoomTransformer which has been adjusted to the
   138    // maximum zoom such that the provided bounding box is centered and entirely
   139    // in frame. Note that the resulting zoom will be adjusted if it does not fit
   140    // inside the top-level bounds of the ZoomTransformer.
   141    zoomedToBox(bounding: Box): ZoomTransformer {
   142      if (_.isNil(bounding)) {
   143        return this;
   144      }
   145  
   146      const newZoom = _.clone(this);
   147      const boundingSize = bounding.size();
   148      newZoom._scale = Math.min(
   149        this._viewportSize[0] / boundingSize[0],
   150        this._viewportSize[1] / boundingSize[1],
   151      );
   152      newZoom.centerOnBox(bounding);
   153      newZoom.adjustZoom();
   154      return newZoom;
   155    }
   156  
   157    // centerOnBox adjusts the zoom translation such that the provided box is
   158    // exactly at the center of the viewport.
   159    private centerOnBox(bounding: Box) {
   160      this._translate = vector.sub(
   161        // This represents the vector from the top-left origin (0, 0) to the
   162        // center of the viewport.
   163        vector.mult(this._viewportSize, 0.5),
   164        // Subtract the vector representing the *scaled* location of the center
   165        // of the target box. This gives the necessary adjustment from origin
   166        // to move the center of the box to the center of the viewport.
   167        vector.mult(bounding.center(), this._scale),
   168      );
   169    }
   170  
   171    private adjustZoom() {
   172      // Increase scaling if we are below the minimum.
   173      const newScale = Math.max(this._scale, this.minScale());
   174      const scaledBounds = this._bounds.scale(newScale);
   175      const newTranslate = _.clone(this._translate);
   176  
   177      // Adjust translation so that viewport is within the bounds.
   178      const translatedBounds = scaledBounds.translate(this._translate);
   179      if (this._viewportSize[0] > translatedBounds.right()) {
   180        // Bounds aligned with right of viewport.
   181        newTranslate[0] = this._viewportSize[0] - scaledBounds.right();
   182      } else if (translatedBounds.left() > 0) {
   183        // Bounds aligned with left of viewport.
   184        newTranslate[0] = -scaledBounds.left();
   185      }
   186  
   187      if (this._viewportSize[1] > translatedBounds.bottom()) {
   188        // Bounds aligned with bottom of viewport.
   189        newTranslate[1] = this._viewportSize[1] - scaledBounds.bottom();
   190      } else if (translatedBounds.top() > 0) {
   191        // Bounds aligned with left of viewport.
   192        newTranslate[1] = -scaledBounds.top();
   193      }
   194  
   195      this._scale = newScale;
   196      this._translate = newTranslate;
   197    }
   198  }