The wind direction map of data visualization

Many people have seen the wind direction map, which is an intuitive image, and it is also a good combination of map data and real data in visualization.

0

       This is the first wind direction map I have seen. I remember it was in 2012. I thought it was very interesting at the time. As a technical person, I am naturally curious about how it was done. Is it Canvas or SVG? But didn't look into it at the time. Someone (big brother) just mentioned this recently, so you might as well learn more and find out. Since then, I found that there are so many ways to play, which are similar, such as this, from earth.nullschool.net:

1

      Of course, there are also echarts-x from Du Niang's open source:

2

      Basically, these three renderings basically cover the technical points and functional points of the current wind direction map (my own opinion, because windyty is written based on earth.nullschool, the former has an additional worker thread to process data, while the latter is on github open source). Not sure which one suits you the most? For me, Figure 1 is simple and easy to understand, and can quickly grasp the realization of the wind direction map; Figure 2 is the real-time global wind direction data, and it is in binary format, which is a solution for big data transmission; Figure 3 uses WebGL real-time rendering, which is a A solution for big data rendering, so each has its own merits. It is just that this article will combine these three examples to talk about the good parts, which is also a process from easy to difficult.

principle

      At first glance, it may seem overwhelming. how did you do that? In fact, understanding and not understanding is just that layer of paper, it depends on whether you are willing to pierce it or not. Let's start with the data.

      First, let's introduce the concept of Vector Field. The explanation in Wikipedia is: In vector analysis, a vector field is a mapping that assigns each point in space to a vector. The vector fields in physics include wind field, gravitational field, electromagnetic field, water flow field and so on. As shown in the figure, the following is a two-dimensional vector field, and each point is a vector.

3

      Of course, this is an abstract mathematical concept and expression. It is often used in the electromagnetic field in physics, and it can be seen everywhere in reality. For example, the following interesting vector expression of magnetic field, please skip it for people with intensive phobia~ 

4

      In the same way, the abstract model of the wind field is also a vector field: each point has a wind speed and direction, which can be decomposed into vectors in the XY direction at that point (we simplify it to the XY directions, ignoring Z, so It's a pity that I can't listen to "Tornado"), then the vector represents the speed of the point in the X and Y directions.

5

      As shown above, it is a real wind direction map data. In simple terms, timestamp represents the current data collection time, (x0, y0, x1, y1) are the ranges of latitude and longitude respectively, and grid is the number of rows and columns of the vector field, and field is the velocity value of each point in the vector field, if A value of (0,0) means that this point is calm. There may be some differences in the wind direction map data of different platforms, but they are all the same.

      The vector field and data format, intuitively, we can know that by fitting these vectors into smooth lines, the following real wind direction can be formed.

6

      How the lines are formed, and it seems that there is always a gust of wind around the world (reminds me of Jupiter's Great Red Spot, this wind has been blowing on Jupiter for at least 200 years and never stopped), which revealed two problems, 1. The vector field is a discrete point, and the line is smooth. There is an interpolation problem. 2. More troublesome is that there are many ways to connect these lines, and they can be connected into lines. It is similar to the algorithm of contour lines. , it seems impossible to start.

      This is the problem that I feel that I need to solve when I want to achieve the wind direction effect after reading the data. It feels so difficult. I fell asleep with this question in mind. The next day I formatted the js script. After local debugging, I found that my question was right, but the idea was wrong. Don't consider so many factors at the beginning, but solve the current problem based on the current state, just like a very complex algebraic problem, perhaps it can be solved very simply by geometric methods.

      Not much nonsense, although I think these nonsense are the most valuable to improve ability, solving problems is just an inevitable process of perception. Well, with the data, let's see how the "God Zhuge" winds up.

      For example, if you are given a Go board (vector field), each square is a vector. You take a piece at random and place it on a square at random (randomly), which is the starting point of the wind. In the next round (the next frame or the next second), you move the piece according to the vector value (X value and Y value) of the current grid, which is the effect of the wind dragging its usual tail and jumping to the next grid at the current wind speed . In this way, the pawn will keep moving according to the vector value of the square it is in, until the vector value of the square is zero (the wind stops).

      That is to say, as long as I give a starting point, I can blow a wind. That gives you 5,000 pieces (the starting point) and you can blow 5,000 winds. Of course, the two airflows may overlap, which may not conform to the laws of physics at this time, because our thinking is to blow each other, but who cares. Therefore, based on the management of the state of each frame, we can easily simulate the effect of the wind direction map. Simple and ingenious.

How to achieve

      Well, in theory we know what to do, let's see how the code is implemented. Let's also sort out this process and modularize them.

      Today, I have been working with Go, and this is the example. First of all, it is the data, that is, the chessboard and the grid, that is, the two objects of Vector and Vector Field to facilitate the reading and management of data; secondly, of course, it is the chess piece. Record the life cycle of each piece, the current position, the position of the next step, that is, the position information of each frame corresponding to the wind. This is the Particle class to record this information; finally, with the chessboard and pieces, you also need a Pushing hands to move pieces, here called MotionDisplay handle, is responsible for managing the position of the pieces corresponding to the chessboard in each round (frame). This class has a lot to do: how many pieces are there, which ones are retracted, and how many pieces need to be added ( Management of wind particles), how to place (render) on the chessboard; wait, there is one less in the end, that is, the clock, which counts the seconds for each round, which is Animation.

      Still have to code, otherwise it looks unprofessional. Let's first explain some key attributes and methods of the objects mentioned above. You can know which key attributes are managed by which classes, and explain some key methods. You can focus on the classes and functions themselves first. content to learn about parts of this puzzle. Finally, there will be an initialization function to introduce the overall process, and then everyone will understand the appearance of the entire puzzle.

      The vector is relatively simple, it is the two components of X and Y, and other methods such as length and angle will not be repeated here:

var Vector = function(x, y) {
   this.x = x;
   this.y = y;
}

      Below is the vector field class that reads JSON data and parses it:

VectorField.read= function(data, correctForSphere) {
    var field = [];
    var w = data.gridWidth;
    var h = data.gridHeight;
    for (var x = 0; x < w; x++) {
        field[x] = [];
        for (var y = 0; y < h; y++) {
            var vx = data.field[i++];
            var vy = data.field[i++];
            varv = new Vector(vx,vy);
            ……
            field[x][y] = v;
        }
    }
    var result = newVectorField(field,data.x0,data.y0,data.x1,data.y1);
    return result;
};

      In this way, the vector field has been well laid out. Of course, if you look at the code carefully against the JSON data, the range, rows and columns of latitude and longitude are saved. Of course, there are several other functions in this class that are not listed here, such as judging a Whether the point is in the chessboard or not, there is also interpolation, because each grid position is discrete, the row and column are integers, and the direction of the actual stroke is continuous, and the position at the current moment may be a fraction. The value interpolation of the adjacent integer points obtains an approximate value of the current point. Here, bilinear interpolation is used, and the surrounding four points are taken:

VectorField.prototype.bilinear= function(coord, a, b) {
    var na = Math.floor(a);    
    var nb = Math.floor(b);    
    var ma = Math.ceil (a);   
    var mb = Math.ceil (b);    
    var fa = a - na;    
    var fb = b - nb;    
    return this.field[na][nb][coord] * (1 - fa)* (1 - fb) +
    this.field[ma][nb][coord] * fa * (1 - fb) +    this.field[na][mb][coord] * (1 - fa) * fb +    this.field[ma][mb][coord] * fa * fb;
};

      Above are some key functions and properties of vectors and vector fields. It realizes reading data, and obtains the X and Y components of the speed of any position (can make decimals) through the getValue function.

      The following are the pieces, and the position of the pieces in each round is the position of the wind in each frame:

var Particle =function(x, y, age) {
    this.x = x;    
    this.y = y;    
    this.oldX = -1;    
    this.oldY = -1;    
    this.age = age;
}

      As above, XY is the current position, old is the position of the previous frame, age is its life cycle, sometimes the pieces will be eaten, and the moment the wind stops, it is recorded by age. How long to live (minus one per frame).

      Now let's introduce this chess hand and see how the wind blows.

varMotionDisplay = function(canvas, imageCanvas, field, numParticles,opt_projection) {    
    this.field = field;    
    this.numParticles = numParticles;    
    this.x0 = this.field.x0;    
    this.x1 = this.field.x1;    
    this.y0 = this.field.y0;    
    this.y1 = this.field.y1;    
    this.makeNewParticles(null, true);    
};

      This is its constructor, which records the information of the vector field (range and velocity vector), while numParticles represents the number of particles, that is, how many wind lines are displayed on the map at the same time. Projection is used for mapping conversion between latitude and longitude and vector fields. Finally, makeNewParticles will build numParticles and randomly assign them a starting point and life cycle. The code is as follows:

MotionDisplay.prototype.makeNewParticles= function(animator) {
   this.particles = [];    
   for (var i = 0; i < this.numParticles;i++) {        
       this.particles.push(this.makeParticle(animator));
    }
};

MotionDisplay.prototype.makeParticle= function(animator) {
       var a = Math.random();
       var b = Math.random();        
       var x = a * this.x0 + (1 - a) *this.x1;        
       var y = b * this.y0 + (1 - b) * this.y1;
       return new Particle(x,y,1 + 40 * Math.random());       
};

     The above is a simple process of creating particles: randomly create a wind that can survive 1 + 40 *Math.random() frames at latitude and longitude (x, y), creating a total of numParticles such random winds. Of course this is for simplicity. It does not consider special cases such as whether the random number will be out of range.

     The objects are all constructed, so how does this hand manage the overall situation every frame? Two things: Update and Render.

MotionDisplay.prototype.animate= function(animator) {
    this.moveThings(animator);//update
    this.draw(animator); // render
}

      First look at how to update:

MotionDisplay.prototype.moveThings= function(animator) {
    var speed = .01 * this.speedScale /animator.scale;    
    for (var i = 0; i <this.particles.length; i++) {    
        var p = this.particles[i];        
        if (p.age > 0 &&this.field.inBounds(p.x, p.y)) {       
            var a = this.field.getValue(p.x,p.y);
            p.x += speed * a.x;
            p.y += speed * a.y;
            p.age--;
        } else {        
            this.particles[i] = this.makeParticle(animator);
        }
    }
};

      As above, every frame updates all wind particle positions according to speed * time (frame) = distance, and at the same time detects that if age is negative, create a new one to replace.

MotionDisplay.prototype.draw= function(animator) {
    var g = this.canvas.getContext('2d');    
    var w = this.canvas.width;    
    var h = this.canvas.height;    
    if (this.first) {
        g.fillStyle = this.background;        
        this.first = false;
    } else {
        g.fillStyle = this.backgroundAlpha;
    }

    g.fillRect(dx, dy, w , h );    
    for (var i = 0; i <this.particles.length; i++) {        
        var p = this.particles[i];        
        if (p.oldX != -1) {
            g.beginPath();
            g.moveTo (proj.x, proj.y);
            g.lineTo(p.oldX, p.oldY);
            g.stroke();
        }
        p.oldX = proj.x;
        p.oldY = project y;
    }
};

     Because the code is too long, the key steps are given. Let’s first look at the stroke process. It is clear that in the function of moveThings, we can get the position of the previous frame and the position of the wind particle of the current frame, which are connected here to form a line. It is conceivable that with the increase of the number of frames, in the limited life cycle, this polyline grows like a snake: 0-1-2-3-4...-n, then simulate the effect of wind The picture is a screenshot comparison of the first frame and the second frame. Carefully observe the wind above the red line. This is the length comparison of the first two frames, or look at the wind near Los Angeles. The increase is more obvious. The wind is relatively strong, if you don’t believe me, go to the weather forecast:

 7

frame one

8

frame two

      It seems like this is perfect, but it's not. Think again, this wind has a life cycle, how will this wind be erased from the map at that time? If you don't erase it, it will be piled up like gray, and this wind obviously has a gradient effect. How is this done?

      Here is a great trick, the transparency backgroundAlpha, which uses the same RGB as the background color, but adds a transparency of 0.02. The effect of fillRect is like sticking a layer of such paper on each frame, and then drawing on it. For the new one, the previous one becomes a little darker, and the old one gets darker and darker, achieving a realistic effect, and at the same time, it also handles the alternation of old and new very well.

      In this way, a basic wind direction map is completed. Again, when you think everything is clear, the problems are just beginning. Briefly talk about the following two points: real-time data and WebGL rendering. There are some entry requirements for WebGL introduction, which may not be easy to understand, mainly temperament (idea).

Real-time data

      After reading a lot of code, let’s take a look at the atmosphere in the last paragraph. The author of the above example is a self-proclaimed artist who wants to think about data in a new way and feel the beauty and fun of data. So with this wind direction map, it is indeed a very interesting effect, but it has a little disadvantage. The author is mainly looking for the beauty of data, and does not provide an effective real-time solution for big data. In other words, this paradigm is still at the level of seeing. A wind direction map, of course, you want to be able to see the wind direction of a specific area and the overall global effect on the map in real time, which requires efficient data transmission.

      The following example is a good consideration of this problem. The author of windytv is a skydiving enthusiast. Before each skydiving, he must observe the weather conditions, especially the wind direction, so he thought of the application of such a wind direction map.

9

     It is a function list of the website, and the data is still very complete. The data source is GFS / NCEP / US National Weather Service. I found that the weather data in it is still very complete, and the wind direction is only one part (I believe that the future will be more open abroad. There will be many services gradually popularized under the data + HTML5, don't miss it). In the program, the data format of the wind direction map is the binary format of epak, which is also transmitted and parsed using the ArrayBuffer method. If you are interested in this, you can read the "ArrayBuffer Brief Analysis" written earlier.

      A good way to do this is with pictures:

10

      Note that the black bar above is actually a redundancy of eight pixels, which mainly contains information such as height and width, data collection time, etc. The rest is a nationwide 360*180 wind vector data. Although the data is not real-time, it can be updated in six hours, and the key is to perform efficient data transmission and analysis.

      In addition, the advantage of using pictures is that they can be sliced. For example, if the accuracy is not high, the global wind direction data can be used. When the accuracy is high, the local slice data can be updated. The idea is exactly the same as the map slice, that is, to avoid the workload of interpolation, The data can also be displayed more clearly. Therefore, this can be regarded as a good optimization of the first example. In addition, it is still open source, you can find it yourself.

WebGL

      Although Baidu's wind direction map is very performance-intensive, it does have a lot to learn technically. After all, it uses WebGL to render, and there are still many innovations in how it realizes the calculation of the life cycle and vector field. To briefly talk about a few key points, the ability is limited, and it is necessary to have a certain understanding of WebGL and OpenGL, so I hope not to delve into it, but pay attention to other people's ideas and methods.

      Let's take a look at the interface usage provided by Baidu:

surfaceLayers:[{
                    type: 'particle',
                    distance: 3,
                    size: [4096, 2048],
                    particle: {
                        vectorField: field,
                        color: 'white',
                        speedScaling: 1,
                        sizeScaling: 1,
                        number: 512 * 512,
                        motionBlurFactor: 0.99
                    }
                }]

      The usage is relatively simple, and it is also to formulate a particle, which passes in the vector field data, number is the maximum number of strokes in a frame, and the latter is controlled internally. The code of Echart-x is a little messy, and finally I found the implementation code by global search.

Map3d

      Responsible for layer creation and initialization.

      First, when the vector data is input, a texture vectorFieldTexture of equal width and height is generated, and each vector (X, Y) is a point (RGBA) on the texture, where X = R, Y = G, B=0 , A=255.. Then each pixel in the texture can get its velocity vector.

      Then every frame will call the layer's UpDate to update the rendering.

      VectorFieldParticleSurface This is a wind direction map layer, which records the key attributes in the wind direction map layer, the key is the update function, and each frame is responsible for driving the state update.

update: function(deltaTime) {                     
    this._particlePass.setUniform('velocityTexture',this.vectorFieldTexture);

    particlePass.attachOutput(this._particleTexture1);
    particlePass.setUniform('particleTexture', this._particleTexture0);
    particlePass.setUniform ('deltaTime', deltaTime);
    particlePass.setUniform('elapsedTime', this._elapsedTime);
    particlePass.render(this.renderer,frameBuffer);           
    this._particleMesh.material.set('particleTexture',this._particleTexture1);

    frameBuffer.attach(this.renderer.gl, this._thisFrameTexture);
    frameBuffer.bind(this.renderer);            
    this.renderer.render(this._scene,this._camera);
}

      It can be seen that Render is done twice, the first time is to render to texture (Render To Texture), which also has some time parameters, and the second time is to render to the scene.

      This is updating the data, passing the speed vector and position parameters corresponding to each point to the shader, and the real operation is done through the shader, directly operating the graphics card to complete the rendering process. The parameters are prepared, and the following rendering process is used to understand them.

Shader

      First in the ecx.vfParticle.particle.fragment fragment shader:

vec4 p =texture2D(particleTexture, v_Texcoord);    
    if (p.w > 0.0) {
        vec4 vTex = texture2D(velocityTexture,p.xy);
        vec2 v = vTex.xy;
        v = (v - 0.5) * 2.0;
        p.z = length(v);
        p.xy += v * deltaTime / 50.0 *speedScaling;        // Make the particle surface seamless
        p.xy = fract(p.xy);
        pw - = deltaTime;
    }
    
    gl_FragColor = p;

      You will see that the idea is the same except for the syntax and JS. First get xy from 'velocityTexture', the texture is the information in the vector field, each point corresponds to the velocity vector, and w represents The life cycle. After calculation, the value is assigned to particleTexture

      Then, if you understand it, it is time to wake up like a dream. It turns out that in each frame, each point in the particleTexture corresponds to the current position of the wind, and the position of each point is updated in particle.fragment, and finally Rendered in the scene.

voidmain ()
{
    vec4 p = texture2D(particleTexture,texcoord);
    gl_Position = worldViewProjection *vec4(p.xy * 2.0 - 1.0, 0.0, 1.0);    
}

      The general idea of ​​a WebGL rendering wind direction map is not detailed, the key is the idea. The study of technology, as long as we strive for perfection, will always yield something. During this process, I first thought about how the wind direction map was implemented. After I understood it, I wanted to see the difference between other scripts. I discovered the real-time data, and also saw the way Baidu's WebGL renders. There may also be There are omissions, but the overall feeling is that I have gained a lot. After the veil is lifted, it is no longer mysterious. Or in other words, the wind field, water flow, and gravity field can all be implemented in this way, with just a little adjustment in the calculation formula.

      It's not easy for people to see this, I hope it's something for you as well.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324982185&siteId=291194637