Introduction: I received a product demand two days ago, which needs to generate a map randomly according to the contribution value of different anchors in proportion. Many students may think of using echart’s map at first time, but those who have used echart know that maps in Echart need various latitude and longitude. I googled and found few, so I built a wheel JS-map-generator.

Take a look at the renderings first

Requirements: large map randomly generated, click each need to use a small map to fill the content of the large map, large map adjacent display

implementation

For this kind of demand, I believe the first idea that pops out of everyone’s mind is to use canvas for drawing. Then we need to solve the following problems:

  1. How to randomly generate maps?
  2. How to determine which area is clicked?
  3. How to fill a specified region block?

How to randomly generate maps

Before generating the map, we need to convert the map into a two-dimensional array in the program, so that we can modify, delete and query the data of the map.

const canvasWidth = ctx.canvas.width;
const canvasHeight = ctx.canvas.height;
var xLineTotals = Math.floor(canvasHeight / ItemGridSize);
var yLineTotals = Math.floor(canvasWidth / ItemGridSize);
this.numYs = xLineTotals;
this.numXs = yLineTotals;

// Build a 2d map of the data
for (let i = 0; i < this.numXs; i++) {
  const tmp: {
    value: number,
    name: string, color? : string, }[] = [];for (let j = 0; j < this.numYs; j++) {
    tmp.push({ color: "transparent".value: -1.name: "" });
  }
  this.dataMap.push(tmp);
}
Copy the code

With the two-dimensional array constructed, we started to consider how to generate a random map, and the steps were roughly as follows:

  1. Select the starting point (default is map center);
  2. Checks whether the starting point has been drawn
    • Drawn: continue the query in a random direction
    • Not plotted: Tag data map
  3. Find a random direction of the starting point to see if the random direction can be used
    • Can be used: tag data map
    • Do not use: find the random direction of the random direction to judge

Its code is roughly as follows:

// Get the random direction of a point
 _getContiguous(frontier: PointType) {
    return[[0.1],
      [0, -1],
      [1.0], [...1.0],
    ].map((dir) = > ({
      x: frontier.x + dir[0].y: frontier.y + dir[1]})); }// Data is populated

fill(dataMap: DataMapType, start: PointType) {
  // Ensure that the number of cycles is up to half of the entire grid
    if (this._loopCount >= (this.xCount * this.yCount * 2) / 3) {
      console.log("Maximum number of queries exceeded");
      return;
    }
    this._loopCount++;
    // It is not occupied
    if (
      start.x > 0 &&
      start.y > 0 &&
      dataMap[start.x][start.y] &&
      dataMap[start.x][start.y].value === -1
    ) {
      this.frontierCount++;
      this.frontiers[`${start.x}:${start.y}`] = true;
      this.changeMapItem(dataMap, start.x, start.y);
    }
    // select left, right, and front randomly
    let newCoors = this._getContiguous({
      x: start.x,
      y: start.y,
    });
    // Find the last one to use
    let canUseCoors = this.filterCanUse(dataMap, newCoors);

    if (canUseCoors.length === 0) {
      // Handle extreme cases
      // find the diagonal
      const skewCoors = this._getSkewContiguous(start);
      const skewCanUse = this.filterCanUse(dataMap, skewCoors);
      if (skewCanUse.length === 0) {
        // The oblique side is fully occupied, so try to break out again from the front side and the left side
        this.fill(dataMap, newCoors[random(3)]);
        return;
      } else{ newCoors = skewCoors; canUseCoors = skewCanUse; }}// mark all available
    for (let j = 0; j < canUseCoors.length; j++) {
      const ele = canUseCoors[j];
      this.changeMapItem(dataMap, ele.x, ele.y);
      this.frontiers[`${ele.x}:${ele.y}`] = true;

      this.frontierCount++;
    }

   / /... Continue with random fill

}

Copy the code

How to ensure continuity?

If you judge from the center every time you’re going to make a lot of unnecessary judgments; We can find the generated map boundary, and then recursively judge from the random boundary;

How to determine which area is clicked?

After the generation of the random map above, we have marked the part to be rendered on the data map, so we can monitor the binding event of canvas to determine whether there is a certain region in the triggered position.

function getEventPosition(ev: any) {
  var x, y;
  if (ev.layerX || ev.layerX === 0) {
    x = ev.layerX;
    y = ev.layerY;
  } else if (ev.offsetX || ev.offsetX === 0) {
    x = ev.offsetX;
    y = ev.offsetY;
  }
  return { x: x, y: y };
}
  getEventInWhereMap(position: PointType) {
    for (let i = 0; i < this.labelQueue.length; i++) {
      const ele = this.labelQueue[i];
      if (ele.positions[`${position.x}:${position.y}`]) {
        return { id: ele.id, index: i }; }}return {
      index: -1.id: ""}; }this.canvasRef.current? .addEventListener("click".(event) = > {
  // Get the coordinates of the event
  const p = getEventPosition(event);
  // Convert to coordinates in the data map
  const mapPosition = {
    x: Math.floor(p.x / ItemGridSize),
    y: Math.floor(p.y / ItemGridSize),
  };

 // See where the click is located
  const selectMap = this.getEventInWhereMap(mapPosition);
  if(selectMap.index ! = = -1) {
    const info = this.labelQueue[selectMap.index];
    this.props.callback && this.props.callback(info); }});Copy the code

How to fill a specified region block?

The author provides four methods in total:

  • Only the parent map is shown
  • Display submaps horizontally to scale
  • Display submaps to scale vertically
  • Random proportion display submap

The parent map

The simplest way is the same number of parent node and child node grid, according to the corresponding data map filling can directly get the content of the parent region; However, considering that the sub-map display area is very small, there is no need to display a large canvas, so we need to carry out a layer of coordinate data conversion.

if (props.center && props.positions) {
  // Find the center of the submap
  const mapCenterX = Math.floor(this.numXs / 2);
  const mapCenterY = Math.floor(this.numYs / 2);
  const { center, color } = props;
  const xC = center.x - mapCenterX;
  const yC = center.y - mapCenterY;

  // Clear a wave before updating
  this.clearMap();
  // Copy the parent region section to the center of the child map
  for (let i = 0; i < this.numXs; i++) {
    for (let j = 0; j < this.numYs; j++) {
      if (props.positions[`${i + xC}:${j + yC}`]) {
        this.dataMap[i][j].value = 0;
        this.dataMap[i][j].color = color; }}}}Copy the code

Display submaps horizontally to scale

for (let y = 0; y < this.yCount; y++) {
  for (let x = 0; x < this.xCount; x++) {
    if (dataMap[x][y].value === 0 && count <= this.value) {
      this.frontiers[`${x}:${y}`] = true;
      this.changeMapItem(dataMap, x, y); count++; }}}Copy the code

Display submaps to scale vertically

for (let x = 0; x < this.xCount; x++) {
  for (let y = 0; y < this.yCount; y++) {
    if (dataMap[x][y].value === 0 && count <= this.value) {
      this.frontiers[`${x}:${y}`] = true;
      this.changeMapItem(dataMap, x, y); count++; }}}Copy the code

Random proportion display submap

Here logic and parent map logic similar, interested can view the source code

At this point the whole map is roughly generated, of course, there are a lot of boundary cases to deal with.

Encapsulation using

The react component has been packaged as a react component. If you are interested, you can use it as follows:

npm install js-map-generator

Copy the code

Click jS-map-Generator to see how to use it.