Winse Blog

走走停停, 熙熙攘攘, 忙忙碌碌, 不知何畏.

斐讯K2刷机记录

很久以前就在JD弄了一个K2,当时没有啥需求,所以也没有折腾 。最近尝试DDNS域名绑定到动态的IP,想在家有一个能提供SSH访问的机器。原来的树莓派被弄坏了,就想着折腾折腾刷刷K2,在上面安装一个SSH。

同时也把官网提供的系统净化净化。

原K2的详细信息

斐讯K2 1200M智能双频无线路由器 WIFI穿墙 PSG1218

了解刷机流程

  • 官方版本可能存在的问题:

http://www.right.com.cn/forum/thread-208302-1-1.html

  • 刷机直接参考

【2017-12-01】斐讯K2 V22.5.9.163官方固件定制版,集成breed,支持官版直刷【V1.8】

详细步骤

  1. 更新版本到 V22.5.9.163

    查看官网提供的软件, 下载对应的版本

    • K2_A2_V21.4.6.12.bin
    • K2_V22.5.9.163.bin
  2. 刷净化版(带Bread)k2_163_v18_breed.rar

    • 下载地址

    • breed刷入第三方固件

      进入Bread方法,这个了解下就行,这里不刷第三方的。

      拔除K2上Wan口的网线,路由器断电,持续按住路由器上的reset按钮,接通路由器电源,3秒后松开reset按钮。 在浏览器地址栏输入 http://192.168.1.1 访问Breed Web。

  3. 启动telnet/手动安装SSH

3.1. 启动telnet

用 高级设置 - 系统设置 - WebShell 执行命令

1
/www/cgi-bin# /usr/sbin/telnetd -l /bin/login.sh

直接连,不用密码!!

1
winse@DESKTOP-ADH7K1Q:~$ telnet 192.168.2.1

同时修改下密码:

1
2
# 更改root密码为 admin
echo -e 'admin\nadmin' | passwd root

3.2. 安装SSH

这个版本没有带opkg,需要首先把opkg安装好。

直接下载 opkg.zip 然后本地起一个 httpserver 提供一个下载的服务。

1
2
winse@DESKTOP-ADH7K1Q:/mnt/e/SOFTWARE/k2$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...

在telnet窗口执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@K2:/www/cgi-bin# cd /bin
root@K2:/bin# wget http://192.168.2.160:8000/opkg
--2018-06-20 22:50:18--  http://192.168.2.160:8000/opkg
Connecting to 192.168.2.160:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 130247 (127K) [application/octet-stream]
Saving to: 'opkg'

opkg                                  100%[=========================================================================>] 127.19K   176KB/s   in 0.7s

2018-06-20 22:50:18 (176 KB/s) - 'opkg' saved [130247/130247]

root@K2:/bin# chmod +x opkg


rm -rf /bin/opkg

注意:用完后就删掉吧,空间不够!!查看安装了那些软件

然后安装ssh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
opkg install http://downloads.openwrt.org/barrier_breaker/14.07/ramips/mt7620a/packages/base/dropbear_2014.63-2_ramips_24kec.ipk
# 开机自启
/etc/init.d/dropbear enable

# https://openwrt.org/docs/guide-user/base-system/ssh_configuration
# https://wiki.openwrt.org/doc/uci/dropbear
vi /etc/config/dropbear
        option GatewayPorts '1'
        
# 启动
/etc/init.d/dropbear start

uci show dropbear

# 如果需要放开防火墙
iptables -I INPUT 1 -p tcp -m tcp --dport 22 -j ACCEPT


vi /etc/firewall.user
# 删除无用文件
rm -rf /etc/dropbear/dropbear_dss_host_key

注意:需要持久化的话,把这句开放22端口的指令写到 /etc/firewall.user 。

客户端登录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
winse@DESKTOP-ADH7K1Q:~$ ssh root@192.168.2.1
The authenticity of host '192.168.2.1 (192.168.2.1)' can't be established.
RSA key fingerprint is SHA256:vuAY65qk3Us4MyjYT8KPT8lYsTSTqru6W4e7My6CRkk.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.2.1' (RSA) to the list of known hosts.
root@192.168.2.1's password:


BusyBox v1.22.1 (2017-02-15 13:52:46 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

    ___  __ _______________  __  _____  ___  ________  ___
   / _ \/ // /  _/ ___/ __ \/  |/  /  |/  / / __/ __ \/ _ \
  / ___/ _  // // /__/ /_/ / /|_/ / /|_/ / _\ \/ /_/ / ___/
 /_/  /_//_/___/\___/\____/_/  /_/_/  /_/ /___/\____/_/
 ----------------------------------------------------------
 Barrier Breaker, unknown
 ----------------------------------------------------------
 PID=K2
 BUILD_TYPE=release
 BUILD_NUMBER=163
 BUILD_TIME=20170215-134532
 ----------------------------------------------------------
 MTK OpenWrt SDK V3.4
 revision : adab2180
 benchmark : APSoC SDK 5.0.1.0
 kernel : 144992
 ----------------------------------------------------------
root@K2:~#

不推荐用密码,最好使用公钥的方式来处理。但公钥访问有点问题,.ssh的目录权限是个麻烦事 (其实文件的位置不对!!)。

参考: Dropbear public-key authentication HowTo

ssh root@192.168.1.1 “tee -a /etc/dropbear/authorized_keys” < ~/.ssh/id_rsa.pub

把 authorized_keys 文件移到 /etc/dropbear 下面就可以了!

1
2
3
4
5
root@K2:~/.ssh# ls -la
drwx------    2 root     root             0 Jun 21 10:35 .
drwx------    1 root     root             0 Jun 21 08:57 ..
-rw-------    1 root     root           397 Jun 21 10:35 authorized_keys
root@K2:~/.ssh# mv authorized_keys /etc/dropbear/

其他拓展

增加空间,挂载windows共享目录

https://blog.vircloud.net/linux/openwrt-psg1218.html

K2 官方版式不带 USB,因此就限制了很多可玩的东西,但是我们可以通过 SMB 挂载的方式来增加存储空间,需要注意的是老毛子挂载 SMB 的方式与其他 OpenWRT 不同,使用 mount 命令是挂载不成功的,正确的方法是:

位置:高级设置 - 自定义设置 - 脚本 - 在路由器启动后执行 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
### SMB资源挂载(局域网共享映射,无USB也能挂载储存空间)
### 说明:共享路径填写时,【\】要写成【\\】。
sleep 10
modprobe des_generic
modprobe cifs CIFSMaxBufSize=64512
#mkdir -p /media/cifs
#mount -t cifs \\\\{host}\\{share} /media/cifs -o username={user},password={pass}
mount -t cifs \\\\192.168.31.100\\移动磁盘-C /mnt -o username=guest,password=guest

sleep 10
mdev -s
sleep 5
stop_ftpsamba
sleep 2
run_ftpsamba
sleep 5

Breed进入方式

  1. 将要刷的第三方固件准备好。
  2. 断电按着reset键不松手,然后通电5秒后再松开reset键。
  3. 打开浏览器输入http://192.168.1.1%E5%8D%B3%E5%8F%AFBreed Web恢复控制台(记得先在Breed Web恢复控制台中的固件备份里备份下EEPROM和编程器固件,以后可能用得着)。
  4. 恢复固件之前最好在Breed Web恢复控制台恢复一下出厂设置,固件类型:Config区(公版)

参考:

其他参考

–END

使用VMWare安装Mac OS X

参考:

实际操作:

  • 安装 VMware-workstation-full-12.5.7-5813279 。
  • 下载 unlocker208.zip 并使用管理员权限安装 win-install.cmd 。
  • 添加虚拟机,选择 Apple Mac OS X(M) - OS X 10.9;然后修改vmx配置,在 smc.present = "TRUE" 后面添加 smc.version = "0"
  • 然后光盘选择 Mavericks_Install_13A603.cdr 安装系统。磁盘格式化:实用工具 - 磁盘工具
  • 安装VMWare Tools。光盘选择 darwiniso.zip 压缩包里面的 darwin6.0.3.iso 。
  • 配置共享文件夹。进入系统后,Finder - 偏好设置 - 已连接的服务器

–END

使用注解生成代码

Java里面随处可见annotation(注解),RetentionPolicy 指示了注解使用的情况:

  • SOURCE,比如 @Override, @SuppressWarnings
  • RUNTIME,最熟悉的莫过于Spring Bean中使用的 @Controller, @Service 一般和反射同时使用。
  • CLASS

而 CLASS 则是用于 compile 编译阶段的注解。一个注解的处理器,以Java代码(或编译过的字节码)作为输入,生成Java文件。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。

可以自己实现一些类似groovy语法糖的功能(lombok框架修改bytecode为类生成新方法getter/setter、或者使用生成新的辅助类等);减少机械的、冗余代码的管理,使得代码更简洁便于阅读。

代码生成

先来了解下整个过程,javac 从 ServiceLoader 获取一个 Processor 标注处理类,判断是否为符合条件的标注,再收集类的相关信息,然后使用 Filer 创建新的类。Java Annotation Processing and Creating a Builderjava annotation processor 主要涉及到如下三部分:

  • Annotation: @BuilderProperty
  • Processor: BuilderProcessor
  • Service:

    通过google的auto-service来注册服务,最终会在 META-INF/services/ 生成名称为 javax.annotation.processing.Processor 的文件,内容为当前被标注的类名。

项目的目录结构如下:

具体实现:

  • BuilderProperty 注解
1
2
3
4
5
6
7
8
9
10
11
package com.github.winse.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuilderProperty {
}
  • BuilderProcessor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package com.github.winse.processor;

import com.github.winse.annotation.BuilderProperty;
import com.google.auto.service.AutoService;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @see BuilderProperty
 */
@SupportedAnnotationTypes("com.github.winse.annotation.BuilderProperty")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement annotation : annotations) {
            Set<? extends Element> annotationElements = roundEnv.getElementsAnnotatedWith(annotation);

            Map<Boolean, List<Element>> annotationMethods = annotationElements.stream()
                    .collect(Collectors.partitioningBy(element -> ((ExecutableType) element.asType()).getParameterTypes().size() == 1 && element.getSimpleName().toString().startsWith("set")));

            List<Element> setters = annotationMethods.get(true);
            List<Element> otherMethods = annotationMethods.get(false);

            otherMethods.forEach(element -> processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@BuildProperty must be applied to a setXxx method with a single argument", element));

            if (setters.isEmpty()) {
                continue;
            }

            String className = ((TypeElement) setters.get(0).getEnclosingElement()).getQualifiedName().toString();

            Map<String, String> setterMap = setters.stream().collect(Collectors.toMap(
                    setter -> setter.getSimpleName().toString(),
                    setter -> ((ExecutableType) setter.asType()).getParameterTypes().get(0).toString()
            ));

            try {
                writeBuilderType(className, setterMap);
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
            }
        }
        return true;
    }

    private void writeBuilderType(String className, Map<String, String> setterMap) throws IOException {
        String packageName = null;
        int lastDot = className.lastIndexOf(".");
        if (lastDot > 0) {
            packageName = className.substring(0, lastDot);
        }

        String simpleClassName = className.substring(lastDot + 1);
        String builderClassName = className + "Builder";
        String builderSimpleClassName = builderClassName.substring(lastDot + 1);

        JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName);
        try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
            if (packageName != null) {
                out.printf("package %s;\n", packageName);
                out.println();
            }

            out.printf("public class %s {\n", builderSimpleClassName);
            out.println();
            out.printf("  private %s object = new %s();\n", simpleClassName, simpleClassName);
            out.println();
            out.printf("  public %s build() {\n", simpleClassName);
            out.printf("    return object;\n");
            out.printf("  }\n");
            out.println();

            setterMap.entrySet().forEach(setter -> {
                String methodName = setter.getKey();
                String argumentType = setter.getValue();

                out.printf("  public %s %s(%s value){\n", builderSimpleClassName, methodName, argumentType);
                out.printf("    object.%s(value);\n", methodName);
                out.printf("    return this;\n");
                out.printf("  }\n");
                out.println();
            });

            out.printf("}\n");

        }
    }

}

测试使用:

  • build.gradle

我使用的是4.7的版本,4.7以上版本可以直接使用 annotationProcessor 来添加标注处理器。(其他版本可以使用 apt 来处理)

1
2
3
4
5
6
7
8
9
10
plugins {
    id "net.ltgt.apt" version "0.10"
}

sourceSets.main.java.srcDirs += ['build/generated/source/apt/main']

dependencies {
    compile rootProject
    annotationProcessor project(':compiler')
}
  • Person

这是一个POJO类,BuilderProcessor处理器会根据BuilderProperty注解来生成PersonBuilder类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.github.winse.example;

import com.github.winse.annotation.BuilderProperty;

public class Person {
    private int age;
    private String name;

    @BuilderProperty
    public void setAge(int age) {
        this.age = age;
    }

    @BuilderProperty
    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

生成代码效果

在 gradle 面板中选择子项目 :example ,然后选择 Tasks 下的 build 任务进行构建。构建完后在 example/build/generated/source/apt 目录下生成了对应的 Builder 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.github.winse.example;

public class PersonBuilder {

  private Person object = new Person();

  public Person build() {
    return object;
  }

  public PersonBuilder setName(java.lang.String value){
    object.setName(value);
    return this;
  }

  public PersonBuilder setAge(int value){
    object.setAge(value);
    return this;
  }

}

注解处理器调试

不会调试说明还没有真正的入门。并且没有调试的情况下,解决异常、错误也是一件异常痛苦的事情。注解处理器生成代码是在编译阶段来生成代码的,所以调试的选项配置添加到 javac 。而 gradle 提供了一种相对简单的方式来进行。

参考

具体步骤如下:

  1. 在命令行运行构建

    添加调试参数后,gradle 会 暂停等待远程调试 ,相当于添加了 JVM 调试参数。Gradle properties

    hello-annotation-processor\example>gradle clean build --no-daemon -Dorg.gradle.debug=true
    或者
    hello-annotation-processor>gradle example:clean example:compileJava --no-daemon -Dorg.gradle.debug=true
    

    注: –no-daemon 不加也是可以的,但是运行该次构建后不会停止。

  2. 远程调试

其他调试配置方式

  • 通过环境变量

    example>set GRADLE_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005
    
    example>gradle clean build
    Listening for transport dt_socket at address: 5005
    
  • 修改 ~/.gradle/gradle.properties

    这种方式不推荐,因为它是全局的。

    org.gradle.daemon=false
    org.gradle.debug=true
    

    或者

    org.gradle.daemon=true
    org.gradle.jvmargs=-XX:MaxPermSize=4g -XX:+HeapDumpOnOutOfMemoryError -Xmx4g -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006
    
    $ gradle --daemon
    

    Then attach your debugger client to port 5006, set your breakpoint, then run your test.

    注:该配置放到项目目录下没用。

其他

–END

科学上网(续)

到新的环境就会遇到新的问题,需要不断的学习更新来适应新的环境。上网也是一样,工作地点和家里存在了一道鸿沟。过去断断续续的有一些解决的方式,但是总是有点间接。

上周和同事讨论到在家访问公司服务器的方式时,可以通过花生壳的DDNS来实现域名动态绑定,相当于了把家里的宽带看做一个公网IP,花生壳实时的把域名解析更新为最新的IP。

有了公网IP后,就可以在公司访问自己的域名(绑定到了家里的IP),然后 反向代理 就可以在家访问公司环境了。

但是查了下对于花生壳的口碑都不咋的,其实只要能自动的更新绑定域名和宽带的IP,和花生壳的效果是一样。然后在 github 查到了 aliyun-ddns 定时检测和更新阿里云上的域名解析。

配置公网域名

我在此基础上调整了一个本地命令行版本 ,直接运行一个脚本就可以更新域名解析了:

1
./client.sh myhome.winseliu.com

注:默认电信宽带给你分配的内网IP的,你可以打10000号要他们给你分配改成外网IP。

本地环境配置

  • 本机SSHD配置
1
2
winse@DESKTOP-ADH7K1Q:~$ sudo dpkg-reconfigure openssh-server
winse@DESKTOP-ADH7K1Q:~$ sudo service ssh start

注:启动后,wsl shell窗口不能关!!窗口关闭后,wsl的所有服务都会停掉!

  • 无秘钥登录

为了安全,最好通过秘钥登录,把使用SSH的密码登录关掉。

1
winse@DESKTOP-ADH7K1Q:~/.ssh$ cat /business/server/id_rsa.pub >>authorized_keys
  • 路由器配置

  • 本机防火墙

参考 开放windows服务器端口—–以打开端口8080为例

配置反向代理

服务端访问自己域名,使用 -R 参数在本地创建一个10022的端口,数据转发到服务器的22端口。当你连本地的 127.0.0.1:10022 就相当于连接服务器的 22 端口。

1
/usr/bin/autossh -M 0 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o StrictHostKeyChecking=no -NR 10022:localhost:22 autossh@myhome.winseliu.com -i ~/.ssh/id_rsa

当连接太慢、SSH提示信息一直不出来,你完全有理由怀疑本地端口被占用了!!查看本地端口状态:

1
2
3
C:\Users\winse>netstat /?

C:\Users\winse>netstat -ano |findstr 10022

如果端口被占用了,需要去任务管理器中关掉对应的PID的程序。

小结

速度比 teamviewer vpn 的方式快狠多狠多!!这个10000号值得打,这个ddns值得一试。

后记

说说 VS Code调试

使用Ubuntu中安装的Node:

1
"useWSL": true

https://code.visualstudio.com/docs/nodejs/nodejs-debugging

注意:这种外部启动的方式,会通过bash.sh运行node,所以就算停止调试后,Node进程还是一直存在的!!!需要通过任务管理器关闭。

–END

解读百度的Heatmap

前面通过Map的学习,了解到了瓦片的一些知识点。地图里面热图是一个比较典型的功能。通过对聚集数据不同颜色显示,直观形象的洞察数据的规律,比如说高危区等的热点分析,有点类似于arcgis的核密度。接下来结合百度里面的热图分析下它的实现。

1
2
3
4
5
6
7
8
9
var points =[
{"lng":116.418261,"lat":39.921984,"count":50},
...
]

//详细的参数,可以查看heatmap.js的文档 https://github.com/pa7/heatmap.js/blob/master/README.md
heatmapOverlay = new BMapLib.HeatmapOverlay({"radius":20});
map.addOverlay(heatmapOverlay);
heatmapOverlay.setDataSet({data:points,max:100});

setDataSet

把经纬度数据先转成界面的坐标(不在界面bounds内的点会被忽略掉),然后调用setData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
HeatmapOverlay.prototype.setDataSet = function(data) {
    this.data = data;
    ...
    var currentBounds = this._map.getBounds();
    var mapdata = {
        max: data.max,
        data: []
    };
    var d = data.data,
        dlen = d.length;
        
    while (dlen--) {
        ...
        if (!currentBounds.containsPoint(latlng)) {
            continue;
        }            
        ...
        mapdata.data.push({
            x: point.x,
            y: point.y,
            count: d[dlen].count
        });
    }
    this.heatmap.setData(mapdata);
}

setData

计算最大最小,合并(对同一坐标的对应的count值求和),其中 _organiseData 根据坐标构建一个稀疏矩阵,最后emit给renderall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
setData: function(data) {
  var dataPoints = data.data;
  var pointsLen = dataPoints.length;


  // reset data arrays
  this._data = [];
  this._radi = [];

  for(var i = 0; i < pointsLen; i++) {
    this._organiseData(dataPoints[i], false);
  }
  this._max = data.max;
  this._min = data.min || 0;
  
  this._onExtremaChange();
  this._coordinator.emit('renderall', this._getInternalData());
  return this;
},

_organiseData: function(dataPoint, forceRender) {
    var x = dataPoint[this._xField];
    var y = dataPoint[this._yField];
    var radi = this._radi;
    var store = this._data;
    var max = this._max;
    var min = this._min;
    var value = dataPoint[this._valueField] || 1;
    var radius = dataPoint.radius || this._cfgRadius || defaultRadius;
    
    ...
    
    if (!store[x][y]) {
      store[x][y] = value;
      radi[x][y] = radius;
    } else {
      store[x][y] += value;
    }
...

_getInternalData: function() {
  return { 
    max: this._max,
    min: this._min, 
    data: this._data,
    radi: this._radi 
  };
},

renderall 渲染

这个是重点,下面一个步骤一个步骤的讲。

1
2
3
4
5
6
renderAll: function(data) {
  // reset render boundaries
  this._clear();
  this._drawAlpha(_prepareData(data));
  this._colorize();
},

_prepareData

把上面合并数据创建的稀疏矩阵,再转回成对象 { x: ,y: ,value: , radius: } ,然后交给 _drawAlpha 进行画图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  var _prepareData = function(data) {
    var renderData = [];
    var min = data.min;
    var max = data.max;
    var radi = data.radi;
    var data = data.data;
    
    var xValues = Object.keys(data);
    var xValuesLen = xValues.length;

    while(xValuesLen--) {
      var xValue = xValues[xValuesLen];
      var yValues = Object.keys(data[xValue]);
      var yValuesLen = yValues.length;
      while(yValuesLen--) {
        var yValue = yValues[yValuesLen];
        var value = data[xValue][yValue];
        var radius = radi[xValue][yValue];
        renderData.push({
          x: xValue,
          y: yValue,
          value: value,
          radius: radius
        });
      }
    }

    return {
      min: min,
      max: max,
      data: renderData
    };
  };

_drawAlpha

然后根据处理整合后的数据画alpha的圆(由于透明度可以进行叠加处理,shadowCtx.globalAlpha = (value-min)/(max-min); ),同时统计会有数据的最大边界rect。

特定半径的密度衰减圆通过 _getPointTemplate 获得,每个数据以其x,y的坐标为圆心,根据count的百分比叠加模板密度圆的透明度进行绘制。由于透明度的叠加,起到 被影响的点 密度相加的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
_drawAlpha: function(data) {
  var min = this._min = data.min;
  var max = this._max = data.max;
  var data = data.data || [];
  var dataLen = data.length;
  // on a point basis?
  var blur = 1 - this._blur;

  while(dataLen--) {

    var point = data[dataLen];

    var x = point.x;
    var y = point.y;
    var radius = point.radius;
    // if value is bigger than max
    // use max as value
    var value = Math.min(point.value, max);
    var rectX = x - radius;
    var rectY = y - radius;
    var shadowCtx = this.shadowCtx;

    var tpl;
    if (!this._templates[radius]) {
      this._templates[radius] = tpl = _getPointTemplate(radius, blur);
    } else {
      tpl = this._templates[radius];
    }
    // value from minimum / value range
    // => [0, 1]
    shadowCtx.globalAlpha = (value-min)/(max-min);

    shadowCtx.drawImage(tpl, rectX, rectY);

    // update renderBoundaries
    if (rectX < this._renderBoundaries[0]) {
        this._renderBoundaries[0] = rectX;
      } 
      if (rectY < this._renderBoundaries[1]) {
        this._renderBoundaries[1] = rectY;
      }
      if (rectX + 2*radius > this._renderBoundaries[2]) {
        this._renderBoundaries[2] = rectX + 2*radius;
      }
      if (rectY + 2*radius > this._renderBoundaries[3]) {
        this._renderBoundaries[3] = rectY + 2*radius;
      }

  }
},

_colorize

最后根据rect的边界范围,然后结合palette的颜色条进行染色(palette 是一个 256 * 4(rgba) 的数组)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

_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;

  }

  img.data = imgData;
  this.ctx.putImageData(img, x, y);

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

},

最终绘制到canvas上,呈现热图效果。

–END