Content Table

Fabric 笔记

Fabric 是基于 HTML5 canvas 的图形库,通过操作对象的属性来操作 canvas (叫 object model),已经提供了一些基本的图元,如 Rect, Circle, Ellipse, Line, Polygon, Image, SVG, Text, Textbox, 阴影等,支持动画、拖动放大缩小旋转等,还支持自由绘制,能够序列化为字符串方便保存,需要的时候再把字符串反序列化为 fabric.Canvas 对象。

Fabric.js is a powerful and simple Javascript HTML5 canvas library. Fabric provides interactive object model on top of canvas element. Fabric also has SVG-to-canvas (and canvas-to-SVG) parser.

下面列举一些可能会常用到的知识点:

  • 全局设置默认值: fabric.Object.prototype.transparentCorners = false (cornerSize: 7)
  • 导出时包含指定 / 自定义属性: canvas.toJSON(['name', 'lockScalingY'],默认很多属性都没有到处,根据自己的需要在这里指定
  • 设置选中 item 的属性: canvas.getActiveObject().set("fontFamily", 'Monaco')
  • 修改整个 canvas 的鼠标: canvas.defaultCursor = 'move'
  • <canvas width="100" height="100"> 的大小被 Canvas 中指定的值覆盖: new fabric.Canvas('c', { width: 600, height: 600 })
  • 控制点 controls 指的是选中图形后在图形边框上出现的 8 个用来控制图形大小的小方框,名字为 tl, tr, br, bl, ml, mt, mr, mb
  • canvas.renderAll() 会立即重绘,canvas.requestRenderAll() 则是把重绘的命令放到事件队列里,如果准备放入时已经存在则丢弃,不重复放入队列中
  • 常用属性: Well, as you would expect there are those related to
    • positioning: left, top
    • dimension: width, height
    • rendering: fill, opacity, stroke, strokeWidth
    • scaling and rotation: scaleX, scaleY, angle
    • flipping: flipX, flipY
    • skewing: skewX, skewY

Fabric Demos 里有很多例子,对学习 Fabric 非常有帮助。

微信网页中精确定位

支持 H5 的浏览器提供了获取 GPS 的接口 navigator.geolocation.getCurrentPosition(success, error),由于各种原因,国内得到的定位都是不准确的,百度地图 API 也提供了定位接口 new BMap.Geolocation().getCurrentPosition(callback),不过也不准确,有时候会偏离几十公里,为了能够在浏览器里精确的定位,可以在微信浏览器中打开网页,使用微信的 JS SDK 中的 wx.getLocation() 进行定位得到大地坐标系 WGS84 的坐标,然后转换为百度 BD09 坐标,使用百度 API 获取此坐标对应的中文地址。

动态切换数据源

动态切换数据源的原理为使用 AbstractRoutingDataSource,根据数据源的名字查找数据源:

  1. 在 Spring IoC 容器中创建一个 AbstractRoutingDataSource 实现的对象 routingDataSource (Spring 默认没有使用此数据源)
  2. 创建一个数据源的 Map dataSourceMap,key 为 DataSource 的名字,value 为 DataSource 的对象
  3. 调用 routingDataSource.setTargetDataSources(dataSourceMap) 设置可供使用的数据源
  4. 在需要使用数据源的时候,Spring JDBC 会调用 routingDataSource.determineTargetDataSource() 获取数据源,而要返回哪个数据源,由 routingDataSource.determineCurrentLookupKey() 返回的数据源的名字决定

下面我们从最简单的方式实现数据源切换,然后一步一步的深入优化基于 AbstractRoutingDataSource 的数据源。

Promise 知识点

可以访问 Promise 对象基础 对 Promise 进行了解,这里就不再赘述了。

Promises 承诺,你将会得到延期或长期运行任务的未来结果。承诺有两个渠道:第一个为结果,第二个为潜在的错误。要获取结果,您将回调函数作为 then 函数参数,要处理错误,您将回调函数提供为 catch 函数参数。Promise 的状态的改变是单向的,一次性的,一旦改变,状态就会凝固了。

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
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
</head>

<body>
<script type="text/javascript">
let p = new Promise((resolve, reject) => {
setTimeout(function() {
const result = Math.random();
result > 0.5 ? resolve(result) : reject('Lower than 0.5');
}, 2000);
});

p.then(result => {
console.log('success', result);
}).catch(result => {
console.log('fail', result);
});
</script>
</body>

</html>

Html 转为 Pdf

使用 html2pdf 把 Html 转为 Pdf,实际项目中可以先借助 Freemarker 模板生成 Html,然后再转换为 Pdf。

缺点: html2pdf 不支持 CSS 的 flex 和 grid 布局。

依赖

1
implementation "com.itextpdf:html2pdf:2.1.7"

转换

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
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.layout.font.FontProvider;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Html2Pdf {
public static void main(String[] args) throws IOException {
File htmlSrc = new File("/Users/Biao/Desktop/x.html");
File pdfDest = new File("/Users/Biao/Desktop/x.pdf");

// 使用中文字体解决不显示中文问题: 字体文件可以从系统中找,也可以从网上下载
FontProvider fontProvider = new FontProvider();
fontProvider.addStandardPdfFonts();
fontProvider.addFont("/Users/Biao/Desktop/Yahei.ttf");
// fontProvider.addDirectory("..."); // 添加文件夹下的所有字体

ConverterProperties converterProperties = new ConverterProperties();
converterProperties.setCharset("UTF-8");
converterProperties.setFontProvider(fontProvider);

// HTML 中本地图片要使用 file:// 的格式,如 file:///Users/Biao/Desktop/shot.png
HtmlConverter.convertToPdf(htmlSrc, pdfDest, converterProperties);
}
}

图-最短路径-Floyd

使用 Floyd 算法求任一点到其他所有点 (任意两点) 之间的最短距离: 对于每个顶点 v,和任一顶点对 (i, j), i!=j, v!=i, v!=j,如果 A[i][j] > A[i][v]+A[v][j],则将 A[i][j] 更新为 A[i][v]+A[v][j] 的值,并且将 Prev[i][j] 改为 Prev[v][j]:

  • 距离表 (初始化为图的邻接矩阵)
  • 前驱表 (初始化为每点到其他任一点的前驱为自己)
  • 三层循环:
    • 第一层: 中间点 [A, B, C, D, E, F, G]
    • 第二层: 出发点 [A, B, C, D, E, F, G]
    • 第三层: 终结点 [A, B, C, D, E, F, G]
    • 出发点通过中间点到终结点的距离、出发点直连终结点的距离选最小值更新距离表: min(Lik+Lkj, Lij),同时更新前驱表

第一轮: 以 A 为中间节点,点 X 通过 A 到点 Y 的距离为 min(XA+AY, XY): BAA, BAB, BAC, BAD, BAE, BAF, BAG, CAA, CAB, …
第二轮: 以 B 为中间节点,…

第七轮: 以 G 为中间节点,…

1
2
3
4
5
6
7
8
9
10
11
12
// 核心: Floyd 算法计算任意 2 点之间的最短距离
final int len = distance.length;
for (int v = 0; v < len; v++) { // 第一层: 中间点
for (int i = 0; i < len; i++) { // 第二层: 出发点
for (int j = 0; j < len; j++) { // 第三层: 终结点
if (distance[i][v] + distance[v][j] < distance[i][j]) {
distance[i][j] = distance[i][v] + distance[v][j];
path[i][j] = path[v][j];
}
}
}
}

图-最短路径-Dijkstra

求图中一点到其他点的最短路径可使用 Dijkstra 算法 (使用广度优先策略):

  • 使用优先级队列实现找最小权重的点 (数组遍历也可以)
  • 前驱节点数组
  • 权重节点数组 (已访问节点)
  • 连接表的存储: Map<String, List<String>>: key 为节点名字,List 为邻接表

图-最小生成树-Kruskal

最小生成树 (Minumum Cost Spanning Tree,简称 MST) 的 2 个经典算法:

  • Prim (普里姆算法): 从顶点出发
  • Kruskal (克鲁斯卡尔算法): 从边出发

网: 带权无向图
最小生成树: 在包含 n 个顶点的连通图中,找出只有 (n-1) 条边,包含所有 n 个顶点的连通子图,也就是所谓的极小连通子图

下面图解 Prim 算法生成最小生成树的过程:

图-最小生成树-Prim

最小生成树 (Minumum Cost Spanning Tree,简称 MST) 的 2 个经典算法:

  • Prim (普里姆算法): 从顶点出发
  • Kruskal (克鲁斯卡尔算法): 从边出发

网: 带权无向图
最小生成树: 在包含 n 个顶点的连通图中,找出只有 (n-1) 条边,包含所有 n 个顶点的连通子图,也就是所谓的极小连通子图

普里姆 (Prim) 算法求最小生成树算法如如下:

  1. 设 G = (V, E) 是联通网,T = (U, D) 是最小生成树,V, U 是顶点集合,E, D 是边的集合
  2. 若从顶点 u 开始构造最小生成树,则从集合 V 中取出顶点 u 放入集合 U 中,标记顶点 u 被访问过了: visited[u] = 1
  3. 若集合 U 中顶点 ui 与集合 V-U 中的顶点 vj 之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点 vj 加入集合 U 中,将边 (ui, vj) 加入集合 D 中,标记 visited[vj] = 1
  4. 重复步骤 3,直到 U 与 V 相等,即所有顶点都被标记为访问过,此时 D 中有 n-1 条边

不管从哪一个顶点开始构建最小生成树,最后得到的最小生成树的边的权值加起来都相等。

下面图解 Prim 算法生成最小生成树的过程,其中:

  • 黑色节点表示未访问过节点
  • 黄色节点表示已访问过节点
  • 红色节点表示未访问过,但是将选择为访问的节点
  • 红色的边为最小生成树中的边
  • 灰色的边为不需要在判断的边,因为它的 2 个顶点都访问过了
  • 黄色的边,其有 1 个顶点被访问过了,另一个顶点未被访问

图-创建图

为了方便创建图,可以把图的边按照格式 startVertex1-endVertex1:weight1,startVertex2-endVertex2:weight2 保存为一个字符串,例如 A-B:16,B-C:10,C-D:3,D-E:4,E-F:8,F-A:14,B-G:7,C-G:6,E-G:2,F-G:9,A-G:12,C-E:5,解析字符串得到图的所有边,使用邻接表存储图的数据。

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package graph;

import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.StringUtils;

import java.util.*;
import java.util.stream.Collectors;

/**
* 图
*/
@Getter
@Setter
@Accessors(chain = true)
public class Graph {
Map<String, List<Edge>> adjacentList = new HashMap<>(); // 邻接表存储图: key 为顶点,Edge 为此顶点和临接点构成的边

/**
* 获取图的顶点
*
* @return 返回顶点的集合
*/
public Set<String> getVertices() {
return adjacentList.keySet();
}

/**
* 获取图的边,无向图中 2 条相同的边只输出一条
*
* @return 返回边的集合
*/
public Set<Edge> getEdges() {
return adjacentList.values().stream().flatMap(List::stream).collect(Collectors.toSet());
}

/**
* 获取传入的顶点的所有边
*
* @param vertex 顶点
* @return 返回边的数组
*/
public List<Edge> getVertexEdges(String vertex) {
return adjacentList.get(vertex);
}

/**
* 使用图的边构建图,边的格式为 start-end:weight,边之间使用逗号分隔,例如 A-B:10,A-G:5
*
* @param edges 图的所有边
* @return 返回图的对象
*/
public static Graph build(String edges) {
Graph graph = new Graph();

for (String edgeContent : StringUtils.split(edges, ",")) {
// 边: A-B:10
int indexOfDash = edgeContent.indexOf("-");
int indexOfColon = edgeContent.indexOf(":");
String vertex1 = edgeContent.substring(0, indexOfDash);
String vertex2 = edgeContent.substring(indexOfDash+1, indexOfColon);
double weight = Double.parseDouble(edgeContent.substring(indexOfColon+1));

// 找到顶点 vertex1 的边集,添加它的边
graph.adjacentList.putIfAbsent(vertex1, new LinkedList<>());
graph.adjacentList.get(vertex1).add(new Edge(vertex1, vertex2, weight));

// 找到顶点 vertex2 的边集,添加它的边
graph.adjacentList.putIfAbsent(vertex2, new LinkedList<>());
graph.adjacentList.get(vertex2).add(new Edge(vertex2, vertex1, weight));
}

return graph;
}

/**
* 图的边,由起点、终点和权重构成
*/
@Getter
@Setter
@Accessors(chain = true)
public static class Edge {
String start; // 边的起点
String end; // 边的终点
double weight; // 边的权重

public Edge(String start, String end, double weight) {
this.start = start;
this.end = end;
this.weight = weight;
}

/**
* 2 个顶点相同的边则为同一条边
*/
@Override
public boolean equals(Object obj) {
if (obj.getClass() != getClass()) {
return false;
}

Edge other = (Edge) obj;

if (this.start.equals(other.start) && this.end.equals(other.end)) {
return true;
}

if (this.start.equals(other.end) && this.end.equals(other.start)) {
return true;
}

return false;
}

@Override
public int hashCode() {
// start, end 从小到大排序
if (start.compareTo(end) < 0) {
return Objects.hash(start, end);
} else {
return Objects.hash(end, start);
}
}
}

public static void main(String[] args) {
Graph graph = Graph.build("A-B:16,B-C:10,C-D:3,D-E:4,E-F:8,F-A:14,B-G:7,C-G:6,E-G:2,F-G:9,A-G:12,C-E:5");
System.out.println(JSON.toJSONString(graph.getAdjacentList()));
System.out.println(graph.getVertices());
System.out.println(JSON.toJSONString(graph.getEdges()));
System.out.println(JSON.toJSONString(graph.getVertexEdges("A"), true));
}
}

下面是顶点 A 的所有边:

1
2
3
4
5
6
7
8
9
10
11
12
13
[{
"end":"B",
"start":"A",
"weight":16.0
},{
"end":"F",
"start":"A",
"weight":14.0
},{
"end":"G",
"start":"A",
"weight":12.0
}]