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:
- How to randomly generate maps?
- How to determine which area is clicked?
- 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:
- Select the starting point (default is map center);
- Checks whether the starting point has been drawn
- Drawn: continue the query in a random direction
- Not plotted: Tag data map
- 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.