Winse Blog

走走停停都是风景, 熙熙攘攘都向最好, 忙忙碌碌都为明朝, 何畏之.

使用注解生成代码

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(电信宽带给的是动态IP,使用动态域名绑定),和花生壳的效果是一样。然后在 github 查到了 aliyun-ddns 可以同时定时检测来更新阿里云上的域名解析。

首先通过域名映射到家里电信宽带的公网IP,ddns用来适配电信IP的动态分配;
然后在家里局域网的一台机器开个SSH的服务;
再在家里路由上做端口转发到ssh主机。这样就可以在公司通过 ssh -p port my-domain 连回家了。

DDNS配置:映射域名到自己的公网IP

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
  • 无秘密登录配置

为了安全,通过公网的SSH访问最好通过秘钥登录,把SSH密码登录的方式给关掉。

1
winse@DESKTOP-ADH7K1Q:~/.ssh$ cat /business/server/id_rsa.pub >>authorized_keys
  • 本机防火墙开放22端口

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

  • 路由器端口映射配置

穿透:配置反向代理

在公司(机器)访问自己的域名,使用ssh的 -R 反向代理参数连接在家里电脑,在家里电脑新建一个5432的端口绑定(数据转发)到服务器的5432端口。这样当你在家电脑连本地的 127.0.0.1:5432 就相当于连接了服务器的 5432 端口。

1
2
/usr/bin/autossh -M 0 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o StrictHostKeyChecking=no \
-N -R 5432:localhost:5432 -i ~/.ssh/id_rsa autossh@myhome.winseliu.com 

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

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

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

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

小结

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

后记

说说 VS Code调试

在写脚本bat/sh的过程中,需要用到nodejs的调试。

使用Windows 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

WebStart的使用以及如何结合JVMTI

当新技术叠加老功能时总能把人折磨一番,新仇加旧恨,原本的老功能也不是熟到透,然后还得去结合新功能,真的简直要人命。

最近有上新功能,把Swing客户端的代码通过webstart的方式发布给客户用,这样用户只需要点击网页上的链接,就可以使用Swing客户端了。感觉体验上还是厉害不少的,只是感觉啊!现实往往更残酷,我们先避开不谈。

首先简单的介绍下webstart、jnlp的一些知识,然后讲讲怎么结合jvmti、以及过程中遇到问题时定位查找解决的一些小知识点。

JNLP

为了便于借鉴参考,我这里用的是 jre1.8.0_162

签名:

1
jarsigner.exe -keystore Keystore application.jar alias

说说调试:

  • 首先你得安装jre,不然Windows的控制面板没有Java这一项!
  • 然后打开 Java控制面板 - 高级 - 调试 的选项。刚开始调试可以同时把 Java控制台 也显示出来
  • 远程调试 选项在 Java控制面板 - Java - Java运行时环境设置 - 运行时参数 添加!

参考

缓冲:

目录

1
C:\Users\winse\AppData\LocalLow\Sun\Java\Deployment

调出 Java高速缓冲查看器 界面

1
javaws -viewer

证书:

证书是用jre对应目录下的: jre1.8.0_162\lib\security\cacerts

结合JVMTI(仇恨点)

既然都是agent,那么加载时机也同样有两种:启动时(Agent_OnLoad)和运行时Attach(Agent_OnAttach)。

动态loadAgent

修改加载 动态链接库dll 的方式:

默认是不能在程序里面动态修改加载库地址的 JDK-4280189 : loadLibrary() fails to load a shared lib whose path is in java.library.path

  1. 修改环境变量PATH,-Djava.library.path
  2. 运行时动态修改java.library.path:usr_paths/sys_paths
  3. 把dll拷贝到环境变量PATH的一个路径下面

参考

解决 DLL依赖 问题的终极完美方法:

  1. The directory where the executable module for the current process is located.
  2. The current directory.
  3. The Windows system directory. The GetSystemDirectory function retrieves the path of this directory.
  4. The Windows directory. The GetWindowsDirectory function retrieves the path of this directory.
  5. The directories listed in the PATH environment variable.

You might need to use something such as Dependency Walker to trace the set of DLL dependencies.

把所有的库全部按依赖顺序执行一遍 System.loadLibrary !!

com.sun.tools.attach.AttachNotSupportedException: no providers installed

  1. 你没有使用sun jdk
  2. 你使用了sun jdk,并且JAVA_HOME指向了这个jdk,但是你的path下的”java”命令不是这个jdk里面的java,而是操作系统给你默认安装的jre下的,如c:\Program Files\java..

小结

最傻瓜式的点击就能运行是最佳体验,我们暂时不能通过控制面板添加 -agentlib:lib 的方式来初始化JVMTI。最终通过以上添加tools.jar的VirtualMachine.loadAgentLibrary运行时attach方式来实现。

–END

Map入门指南

最近了解了一些Map地图相关的知识点,把学习的资料罗列一下:

坐标体系

说明:

  • WGS84:为一种大地坐标系,也是目前广泛使用的GPS全球卫星定位系统使用的坐标系。标准的Web墨卡托投影坐标系。
  • GCJ02:又称火星坐标系,是由中国国家测绘局制定的地理坐标系统,是由WGS84加密后得到的坐标系。指中国国家测绘局制订的加偏Web墨卡托投影,正式名称为GCJ-02,国内可用的地图多数属于这种坐标系。
  • BD09:为百度坐标系,在GCJ02坐标系基础上再次加密。其中bd09ll表示百度经纬度坐标,bd09mc表示百度墨卡托米制坐标。

地图API

百度

腾讯

高德

国内其他

Google

NOTE: Google的在Java里面用需要指定证书和代理:

网页访问一次,把geo的CA证书保存到本地,然后导入到本地的证书库,加入到应用得启动参数里面:

1
2
3
keytool -import -rfc -v -alias geo_ca -keystore truststore -file geo.cer

java -Djava.net.useSystemProxies=true -Djavax.net.ssl.trustStore=.\security\truststore 

各坐标系间的转换

Example:

坐标转换代码:

比例尺

百度

1
2
3
map.addControl(new BMap.ScaleControl());
map.enableScrollWheelZoom();   //启用滚轮放大缩小,默认禁用
map.enableContinuousZoom();    //启用地图惯性拖拽,默认禁用

左下角标注的尺寸包括一个数字加一条线段,就是地图上与那条线等长的距离的实际距离为数字表示的长度。假设长度为一厘米,那就是说那一厘米在地图上同等长度实际是20m的距离,比例为1:2000。

在百度地图API中,平面坐标是以最大级别18级为基准的。就是说在18级平面坐标的一个单位就代表了屏幕上的1个像素 (详细的内容后面讲,可以参考百度地图API详解之地图坐标系统)。

Android里面计算百度比例尺的方式:取两个点获取它们的经纬度,然后算两个点之间的距离。

NOTE: 百度地图SDK还提供了标注工具(PushpinTool),测距工具(DistanceTool)。

Google

Bing

深入了解地图 - 瓦片

各种tile的地址路径

腾讯”矢量”地图 - 通过JSON传数据画Canvas

国内百度腾讯网页端的实现

现在的地图基本都是使用瓦片技术,计算步骤如下:

  • 首先,根据投影(墨卡托投影)把 经纬度(度) 转成 平面坐标(m);
  • 然后,更具比例尺把 平面坐标 转成 像素坐标;
  • 最后,根据坐标的平移把窗口内的瓦片从服务端下载并进行展示。

通过JS代码了解地图的实现:

百度

打开一个百度的应用 http://api.map.baidu.com/lbsapi/getpoint/index.html 然后在调试窗口运行转换经纬度的代码,然后进到对应的代码,打断点,然后艰辛进行与混淆的代码死磕!

1
2
3
4
5
6
7
8
9
var projection =new BMap.MercatorProjection();
var point = projection.lngLatToPoint(new BMap.Point(113.338191,23.138992));
point
Q {x: 12616886.99, y: 2631894}

var projection =new BMap.MercatorProjection();
var point = projection.pointToLngLat(new BMap.Pixel(12616886.99,2631894));
point
H {lng: 113.338191, lat: 23.138992}

从代码上看还是不难的,但是里面有一堆魔法数字完全不懂。

如果仅仅获取瓦片 https://github.com/CntChen/tile-lnglat-transform/ 推荐使用这个项目。

这里仅仅是经纬度转换为平面坐标(m)的过程。我们在源码中查找 getTilesUrl 在5901行打个断点,然后在回到网页,移动一下地图。接下来,就可以调试整个过程了。

注意标识的两处,是进行层级缩放、计算出瓦片编号的代码。

腾讯

看过了百度的,再看腾讯的。然鹅并没有觉得轻松啊,两种不同的坐标系,做法差别还是挺大的。不过从命名上看腾讯算学术派的了。

打开 http://lbs.qq.com/javascript_v2/case-run.html#sample-geocoding-reverse ,在 map.qq.com/js/v2.js 的 apiLoad 处打断点进行到真正的map的js文件。

然后查找 fromLatLngToPoint ,再在界面动一下,就可以调试整个过程:

  • fromLatLngToPoint
  • fromPointToLatLng

调式的时刻可以顺便看看整个调用链,会发现:

  • fromDivPixelToLatLng
  • fromLatLngToDivPixel

fromDivPixelToLatLng的条用链,以及数据的传递如下:

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
fromDivPixelToLatLng 
=>

->Rh 转成相对位置转成绝对坐标后,传入到Mc
[
a
P {x: 930, y: -471}
c
ia {width: 1725872.4160540444, height: 794188.9248479041}
b
true
]

->Mc(g, a, h, f)  <->  Gf 缩放后,调用fromPointToLatLng
[
g
Sh {a: P, b: 0.7111111111111111, c: 40.74366543152521, d: true}
a
P {x: 1726802, y: 793718}
h
13
f
true
]

->fromPointToLatLng(Ad, e)
[
Ad
P {x: 210.791259765625, y: 96.889404296875}
e
true
]

-> Return value : u
    lat:40.02892889530204
    lng:116.42520904541016

从 坐标计算 经纬度 反过来了:

腾讯的计算过程直接把 转平面坐标和转像素坐标 两个过程合并了。通过 fromLatLngToPoint 得到就是一个 像素坐标 的值,然后通过缩放就可以得到当前层级级别像素坐标

查找瓦片地址的代码,直接在代码里面查找 x= 在37823处代码都打断点,刷新重新加载瓦片就会进到断点。

然后查看调用链,

详细跟踪的话,会发现,每次加载都会计算左上角和右下角两个点的像素坐标 (窗口的bounds)。计算要加载的瓦片时,直接用最大减最小除以256(每个瓦片的像素),就得到要加载瓦片的编号了。

用了几天比较肤浅的跟了下QQ地图的功能,如果没有混淆应该看起来会爽很多。。。没有很深层次的东西,仅仅是一个源码调试过程的记载,一些理论原理的知识请查完文章中的链接。

其他

–END