github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/ccl/src/views/clusterviz/containers/map/mapLayout.tsx (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 d3 from "d3"; 11 import React from "react"; 12 13 import * as protos from "src/js/protos"; 14 import { LocalityTree } from "src/redux/localities"; 15 import { LocationTree } from "src/redux/locations"; 16 import { getChildLocalities } from "src/util/localities"; 17 import { findOrCalculateLocation } from "src/util/locations"; 18 import * as vector from "src/util/vector"; 19 20 import { LocalityView } from "./localityView"; 21 import { WorldMap } from "./worldmap"; 22 import { Box, ZoomTransformer } from "./zoom"; 23 import { LivenessStatus } from "src/redux/nodes"; 24 25 import "./mapLayout.styl"; 26 27 interface MapLayoutProps { 28 localityTree: LocalityTree; 29 locationTree: LocationTree; 30 livenessStatuses: { [id: string]: LivenessStatus }; 31 viewportSize: [number, number]; 32 } 33 34 interface MapLayoutState { 35 zoomTransform: ZoomTransformer; 36 prevLocations: protos.cockroach.server.serverpb.LocationsResponse.ILocation[]; 37 } 38 39 export class MapLayout extends React.Component<MapLayoutProps, MapLayoutState> { 40 gEl: React.RefObject<SVGGElement> = React.createRef(); 41 zoom: d3.behavior.Zoom<any>; 42 43 constructor(props: MapLayoutProps) { 44 super(props); 45 46 const projection = d3.geo.equirectangular(); 47 const topLeft = projection([-180, 140]); 48 const botRight = projection([180, -120]); 49 const bounds = new Box( 50 topLeft[0], 51 topLeft[1], 52 botRight[0] - topLeft[0], 53 botRight[1] - topLeft[1], 54 ); 55 56 const zoomTransform = new ZoomTransformer(bounds, props.viewportSize); 57 this.state = { 58 zoomTransform, 59 prevLocations: [], 60 }; 61 62 // Create a new zoom behavior and apply it to the svg element. 63 this.zoom = d3.behavior.zoom() 64 .on("zoom", this.onZoom); 65 66 // Set initial zoom state. 67 this.updateZoom(zoomTransform); 68 } 69 70 // updateZoom programmatically requests zoom transition to the target 71 // specified by the provided ZoomTransformer. If 'animate' is true, this 72 // transition is animated; otherwise, the transition is instant. 73 // 74 // During the transition, d3 will repeatedly call the 'onZoom' method with the 75 // appropriate translations for the animation; that is the point where this 76 // component will actually be re-rendered. 77 updateZoom(zt: ZoomTransformer, animate = false) { 78 const minScale = zt.minScale(); 79 80 this.zoom 81 .scaleExtent([minScale, minScale * 10]) 82 .size(zt.viewportSize()); 83 84 if (animate) { 85 // Call zoom.event on the current zoom state, then transition to the 86 // target zoom state. This is needed because free pan-and-zoom does not 87 // update the internal animation state used by zoom.event, and will cause 88 // animations after the first to have the wrong starting position. 89 d3.select(this.gEl.current) 90 .call(this.zoom.event) 91 .transition() 92 .duration(750) 93 .call(this.zoom 94 .scale(zt.scale()) 95 .translate(zt.translate()) 96 .event, 97 ); 98 } else { 99 // Call zoom.event on the element itself, rather than a transition. 100 d3.select(this.gEl.current) 101 .call(this.zoom 102 .scale(zt.scale()) 103 .translate(zt.translate()) 104 .event, 105 ); 106 } 107 } 108 109 // onZoom is called by d3 whenever the zoom needs to be updated. We apply 110 // the translations from d3 to our react-land zoomTransform state, causing 111 // the component to re-render with the new zoom. 112 onZoom = () => { 113 const zoomTransform = this.state.zoomTransform.withScaleAndTranslate( 114 this.zoom.scale(), this.zoom.translate(), 115 ); 116 117 // In case the transform was adjusted, apply the scale and translation back 118 // to the d3 zoom behavior. 119 this.zoom 120 .scale(zoomTransform.scale()) 121 .translate(zoomTransform.translate()); 122 123 this.setState({ zoomTransform }); 124 } 125 126 // rezoomToLocalities is called to properly re-zoom the map to display all 127 // localities. Should be supplied with the current ZoomTransformer setting. 128 rezoomToLocalities(zoomTransform: ZoomTransformer) { 129 const { prevLocations } = this.state; 130 const { localityTree, locationTree } = this.props; 131 const locations = _.map( 132 getChildLocalities(localityTree), l => findOrCalculateLocation(locationTree, l), 133 ); 134 135 // Deep comparison to previous location set. If any locations have changed, 136 // this indicates that the user has navigated to a different level of the 137 // locality tree OR that new data has been added to the currently visible 138 // locality. 139 if (_.isEqual(locations, prevLocations)) { 140 return; 141 } 142 143 // Compute a new zoom based on the new set of localities. 144 const projection = d3.geo.mercator(); 145 const boxes = locations.map(location => { 146 const center = projection([location.longitude, location.latitude]); 147 148 // Create a 100 unit box centered on each mapped location. This is an 149 // arbitrary size in order to reserve enough space to display each 150 // location. 151 return new Box(center[0] - 50, center[1] - 50, 100, 100); 152 }); 153 zoomTransform = zoomTransform.zoomedToBox(Box.boundingBox(...boxes)); 154 this.setState({ 155 prevLocations: locations, 156 }); 157 158 this.updateZoom(zoomTransform, !_.isEmpty(prevLocations)); 159 } 160 161 componentDidMount() { 162 d3.select(this.gEl.current).call(this.zoom); 163 this.rezoomToLocalities(this.state.zoomTransform); 164 } 165 166 componentDidUpdate() { 167 const zoomTransform = this.state.zoomTransform.withViewportSize(this.props.viewportSize); 168 if (!_.isEqual(this.state.zoomTransform, zoomTransform)) { 169 this.setState({ 170 zoomTransform, 171 }); 172 } 173 this.rezoomToLocalities(zoomTransform); 174 } 175 176 renderChildLocalities(projection: d3.geo.Projection) { 177 const { localityTree, locationTree } = this.props; 178 return _.map(getChildLocalities(localityTree), locality => { 179 const location = findOrCalculateLocation(locationTree, locality); 180 const center = projection([location.longitude, location.latitude]); 181 182 return ( 183 <g transform={`translate(${center})`}> 184 <LocalityView localityTree={locality} livenessStatuses={this.props.livenessStatuses} /> 185 </g> 186 ); 187 }); 188 } 189 190 render() { 191 // Apply the current zoom transform to a mercator projection to pass to 192 // components of the ClusterVisualization. Our zoom bounds are computed 193 // from the default projection, so we apply the scale and translation on 194 // top of the default scale and translation. 195 const scale = this.state.zoomTransform.scale(); 196 const translate = this.state.zoomTransform.translate(); 197 const projection = d3.geo.mercator(); 198 projection.scale(projection.scale() * scale); 199 projection.translate(vector.add(vector.mult(projection.translate(), scale), translate)); 200 201 const { viewportSize } = this.props; 202 203 return ( 204 <g ref={this.gEl}> 205 <rect width={viewportSize[0]} height={viewportSize[1]} fill="#E2E5EE" /> 206 <WorldMap projection={projection} /> 207 { this.renderChildLocalities(projection) } 208 </g> 209 ); 210 } 211 }