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  }