百度热力图的实现方式调研

557 阅读3分钟

起因

热力图原本的样式不太美观,热力图是由各种颜色色块按照不同的比例渲染出一定的范围区域的,但是从颜色到其它颜色之间是通过渐变的方式过渡的。而百度热力图是通过锯齿线进行过渡,相对来说美观很多。

具体实现

动态热力图,涉及heatmap.js的函数或者canvas函数

  • globalAlpha透明度设置,通过透明度的相间覆盖来实现不同的渐变区域之间的层叠效果,主要目前就在于解决这个问题
// 每个点的值 与最大值之间的计算, 点的值-最小值/最大值-最小值来做透明度?
var value = Math.min(point.value, max);
var templateAlpha = (value - min) / (max - min);
// this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
shadowCtx.globalAlpha = templateAlpha < 0.01 ? 0.01 : templateAlpha;
  • createRadialGradient 主要函数,用于生成径向/圆渐变
  • 还有一种image值的处理比较重点,但是目前尚未理解透彻
_colorize: function () {
  var x = this._renderBoundaries[0];
  var y = this._renderBoundaries[1];
  var width = this._renderBoundaries[2] - x;
  var height = this._renderBoundaries[3] - y;
  var maxWidth = this._width;
  var maxHeight = this._height;
  var opacity = this._opacity;
  var maxOpacity = this._maxOpacity;
  var minOpacity = this._minOpacity;
  var useGradientOpacity = this._useGradientOpacity;

  if (x < 0) {
    x = 0;
  }
  if (y < 0) {
    y = 0;
  }
  if (x + width > maxWidth) {
    width = maxWidth - x;
  }
  if (y + height > maxHeight) {
    height = maxHeight - y;
  }

  var img = this.shadowCtx.getImageData(x, y, width, height);
  var imgData = img.data;
  var len = imgData.length;
  var palette = this._palette;

  for (var i = 3; i < len; i += 4) {
    var alpha = imgData[i];
    var offset = alpha * 4;

    if (!offset) {
      continue;
    }

    var finalAlpha;
    if (opacity > 0) {
      finalAlpha = opacity;
    } else {
      if (alpha < maxOpacity) {
        if (alpha < minOpacity) {
          finalAlpha = minOpacity;
        } else {
          finalAlpha = alpha;
        }
      } else {
        finalAlpha = maxOpacity;
      }
    }
    imgData[i - 3] = palette[offset];
    imgData[i - 2] = palette[offset + 1];
    imgData[i - 1] = palette[offset + 2];
    imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha;
  }
  console.log('imgData', imgData)
  img.data = imgData;
  this.ctx.putImageData(img, x, y);

  this._renderBoundaries = [1000, 1000, 0, 0];
},

静态热力图,涉及克里金统计方式生成图【百度地图目前的方案】

涉及主要的克里金js,什么是克里金插值

克里金插值又称空间局部插值法,是以半变异函数理论和结构分析为基础,在有限区域内对区域化变量进行无偏最优估计的一种方法,是地统计学的主要内容之一。南非矿产工程师D.R.Krige在寻找金矿时首次运用这种方法,法国著名统计学家G.Matheron随后将该方法理论化、系统化,并命名为Kriging,即克里金方法。——引自《地理信息系统空间分析实验教程》

克里金的适用条件

区域化变量要存在空间相关性,即半变异函数和结构分析的结果表明区域化变量存在空间相关性。

kriging.js github地址

github.com/oeo4b/krigi…

克里金插值需要注意的点?

数据应符合前提假设。例如,普通克里金要求数据变化呈正态分布。数据应尽量充分,样本数尽量大于80,每一种距离间隔分类中的样本对数尽量多余10对。在具体建模过程中,很多参数是可调的,且每个参数对结果的影响不同。当数据足够多时,各种插值方法的效果基本相同。(我理解的数据足够多,是指数据量在500以上)

克里金更详细的说明

ArcGIS-克里金插值教学

还有查看arcGIS的官方手册

百度热力图的相关信息

11年推出的大数据可视化项目,基于手机的热力图应用,按照位置聚类,计算人群密度,很强的时效性15分钟更新一次。

颜色层级

代码如下
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>leaflet克里金空间插值</title>
  <style>
    html,
    body,
    #map {
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
      overflow: hidden;
      cursor: default;
    }
  </style>
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" />
  <script src="https://cdn.bootcss.com/jquery/3.0.0/jquery.min.js"></script>
  <script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"></script>
  <script src="https://unpkg.com/esri-leaflet@2.2.4/dist/esri-leaflet.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
  <script src="kriging.js"></script>
  <script src="point.js"></script>
  <script src="world.js"></script>
  <script src="tifData.js"></script>
</head>

<body>
  <canvas id="canvasMap" style="display: none;"></canvas>
  <div id="map"></div>
  <script>
    function getRandomArrayElements(arr, count) {
      var shuffled = arr.slice(0), i = arr.length, min = i - count, temp, index;
      while (i-- > min) {
        index = Math.floor((i + 1) * Math.random());
        temp = shuffled[index];
        shuffled[index] = shuffled[i];
        shuffled[i] = temp;
      }
      return shuffled.slice(min);
    }
    //  找出一个圈子范围的点  1公里
    tempEndData = _.orderBy(endData, ['y', 'x'], ['asc', 'asc']);
    endData = [];
    tempEndData.forEach(e => {
      let one = L.latLng(e.y, e.x);
      let two = L.latLng(tempEndData[0].y, tempEndData[0].x);
      let tempDistance = one.distanceTo(two);
      if (tempDistance < 3000) {
        endData.push(e);
      }
      // if (tempDistance < 3000) {
      //   endData.push(e);
      // }
    });
    // .slice(0, 170);
    // endData = endData.slice(0, 2000);
    // endData = getRandomArrayElements(endData, 500);
    console.info(points);
    console.info(points.map(e => e.attributes.TN_))
    console.info(endData);
    points = []
    world = [[]];
    // 抽稀,过滤点的函数
    function chouXi(data) {
      let newData = [];
      newData.push(data[0]);
      for (let index = 0; index < data.length; index++) {
        const e = data[index];
        const e2 = data[index + 1];
        if ((e2 && e2.y) && (e2.y === e.y) && (Number(e2.ColorValue) !== Number(e.ColorValue))) {
            newData.push(e2);
          }
      }
      return newData;
    }
    // endData = chouXi(endData);
    // debugger;
    endDataIndex = 0;
    endData.forEach(e => {
      points.push({
        "attributes": {
          "FID": endDataIndex,
          "NAME": "",
          "TN_": Number(e.ColorValue),
        },
        "geometry": {
          "x": Number(e.x),
          "y": Number(e.y)
        }
      });
      endDataIndex++;
    })

    // 找出四个角的点
    maxY = endData.map(e => Number(e.y)).reduce((xx, yy) => {
      return xx > yy ? xx : yy;
    })
    maxX = endData.map(e => Number(e.x)).reduce((xx, yy) => {
      return xx > yy ? xx : yy;
    })
    minY = endData.map(e => Number(e.y)).reduce((xx, yy) => {
      return xx < yy ? xx : yy;
    })
    minX = endData.map(e => Number(e.x)).reduce((xx, yy) => {
      return xx < yy ? xx : yy;
    })
    console.log(maxY, minY, maxX, minX)
    world[0].push([minX, maxY]);

    world[0].push([maxX, maxY]);

    world[0].push([maxX, minY]);

    world[0].push([minX, minY]);
    // 回归
    world[0].push([minX, maxY]);

    console.info(points);
    var map = L.map('map', {
      // center: [38.65953686, 120.8696333],
      // center: [23.04972597, 113.3160423],

      center: [23.08139498, 113.3289766],
      zoom: 14
    });

    L.esri.tiledMapLayer({
      url: 'http://map.geoq.cn/ArcGIS/rest/services/ChinaOnlineCommunity/MapServer/tile/{z}/{y}/{x}'
    }).addTo(map);
    //world.js,是插值之后需要裁切的图形边界信息【之前的案例,目前没用到】
    //point.js,是要插值的离散的点【之前的案例,目前没用到】
    //tifData.js 最终要处理的值

    //遍历world边界数据,生成scope边界线
    var positions = [];
    world[0].forEach(function (point) {
      positions.push([point[1], point[0]]);
    });
    var scope = L.polyline(positions, { color: 'red' }).addTo(map);
    map.fitBounds(scope.getBounds());

    //根据scope边界线的范围,计算范围变量
    var xlim = [scope.getBounds()._southWest.lng, scope.getBounds()._northEast.lng];
    var ylim = [scope.getBounds()._southWest.lat, scope.getBounds()._northEast.lat];
    console.info(xlim, ylim);

    //进行克里金插值
    function loadkriging() {
      var canvas = document.getElementById("canvasMap");
      canvas.width = 1000;
      canvas.height = 1000;
      var n = points.length;
      var t = [];//数值
      var x = [];//经度
      var y = [];//纬度
      for (var i = 0; i < n; i++) {
        t.push(points[i].attributes.TN_);
        x.push(points[i].geometry.x);
        y.push(points[i].geometry.y);
        // 打上点
        L.circle([y[i], x[i]], { radius: 1 }).addTo(map);
      }

      //对数据集进行训练
      var variogram = kriging.train(t, x, y, "exponential", 0, 100);

      //使用variogram对象使polygons描述的地理位置内的格网元素具备不一样的预测值,最后一个参数,是插值格点精度大小
      // var grid = kriging.grid(world, variogram, (ylim[1] - ylim[0]) / 150);
      var grid = kriging.grid(world, variogram, (ylim[1] - ylim[0]) / 500);

      var colors = [
        // "#006837",
        // "#1a9850",
        // "#66bd63",
        // "#a6d96a",
        // "#d9ef8b",
        // "#ffffbf",
        // "#fee08b",
        // "#fdae61",
        // "#f46d43",
        // "#d73027",
        // "#a50026"
        'rgb(142,142,232)',
        'rgb(131,161,261)',
        'rgb(141,219,223)',
        'rgb(132,222,118)',
        'rgb(221,223,110)',
        'rgb(209, 127, 65)',
        'rgb(208, 33, 0)'
        // 'rgb(115,118,221)',
        // 'rgb(138,226,231)',
        // 'rgb(123,225,126)',
        // 'rgb(222,222,121)',
        // 'rgb(217,127,74)',
        // 'rgb(213,60,62)',
      ];


      //将得到的格网grid渲染至canvas上
      kriging.plot(canvas, grid, [xlim[0], xlim[1]], [ylim[0], ylim[1]], colors);
    }

    //将canvas对象转换成image的URL
    function returnImgae() {
      var mycanvas = document.getElementById("canvasMap");
      return mycanvas.toDataURL("image/png");
    }

    loadkriging();

    var imageBounds = [[ylim[0], xlim[0]], [ylim[1], xlim[1]]];
    L.imageOverlay(returnImgae(), imageBounds, { opacity: 0.8 }).addTo(map);


  </script>
</body>

</html>

涉及知识点

  • 目前市面上的热力图基本都是用heatmap.js来实现,或者是第三方地图自己写的一套热力图方案,不过第三方地图也有可能是引用heatmap.js来实现的,基本原理是一致的。
  • 要实现这种自动绘制的方式,需要用到地图的自定义canvas绘图
  • 喷漆效果的实现,通过调研知道实际上使用的克里金生成地图的方式
  • 传统热力图主函数用两种渐变方式去处理的。两种渐变的理解,一种是线性渐变,另一种是径向/圆渐变。
  • 传统的动态热力图的不同,百度用得是静态热力图
  • 什么是geojson格式,基于json的地理空间信息数据交换格式

GeoJSON是一种对各种地理数据结构进行编码的格式,基于Javascript对象表示法(JavaScript Object Notation, 简称JSON)的地理空间信息数据交换格式。GeoJSON对象可以表示几何、特征或者特征集合。GeoJSON支持下面几何类型:点、线、面、多点、多线、多面和几何集合。GeoJSON里的特征包含一个几何对象和其他属性,特征集合表示一系列特征。

参考资料

canvas多个示例

canvas高级示例

gis开发者-关于克里金插值渲染图的实现

地图轨迹点抽稀

marker坐标点的高级抽稀算法-

arcGIS api for js

制作平均气温空间分布图-静态的热力图类型-百度地图正在使用的方式

ArcGis绘制地图温度变化图

ArcGIS-克里金插值教学

绘制温度场图