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 }