LineUtil
# 概述
LineUtil
是 Leaflet 中用于处理线段的工具类,提供了一些常用的线段操作方法,如计算线段长度、计算线段中点、计算线段与矩形的交点等。
# 源码分析
# 源码实现
LintUtil
的源码实现如下:
export function simplify(points, tolerance) {
if (!tolerance || !points.length) {
return points.slice();
}
var sqTolerance = tolerance * tolerance;
// stage 1: vertex reduction
points = _reducePoints(points, sqTolerance);
// stage 2: Douglas-Peucker simplification
points = _simplifyDP(points, sqTolerance);
return points;
}
export function pointToSegmentDistance(p, p1, p2) {
return Math.sqrt(_sqClosestPointOnSegment(p, p1, p2, true));
}
export function closestPointOnSegment(p, p1, p2) {
return _sqClosestPointOnSegment(p, p1, p2);
}
function _simplifyDP(points, sqTolerance) {
var len = points.length,
ArrayConstructor =
typeof Uint8Array !== undefined + "" ? Uint8Array : Array,
markers = new ArrayConstructor(len);
markers[0] = markers[len - 1] = 1;
_simplifyDPStep(points, markers, sqTolerance, 0, len - 1);
var i,
newPoints = [];
for (i = 0; i < len; i++) {
if (markers[i]) {
newPoints.push(points[i]);
}
}
return newPoints;
}
function _simplifyDPStep(points, markers, sqTolerance, first, last) {
var maxSqDist = 0,
index,
i,
sqDist;
for (i = first + 1; i <= last - 1; i++) {
sqDist = _sqClosestPointOnSegment(
points[i],
points[first],
points[last],
true
);
if (sqDist > maxSqDist) {
index = i;
maxSqDist = sqDist;
}
}
if (maxSqDist > sqTolerance) {
markers[index] = 1;
_simplifyDPStep(points, markers, sqTolerance, first, index);
_simplifyDPStep(points, markers, sqTolerance, index, last);
}
}
function _reducePoints(points, sqTolerance) {
var reducedPoints = [points[0]];
for (var i = 1, prev = 0, len = points.length; i < len; i++) {
if (_sqDist(points[i], points[prev]) > sqTolerance) {
reducedPoints.push(points[i]);
prev = i;
}
}
if (prev < len - 1) {
reducedPoints.push(points[len - 1]);
}
return reducedPoints;
}
var _lastCode;
export function clipSegment(a, b, bounds, useLastCode, round) {
var codeA = useLastCode ? _lastCode : _getBitCode(a, bounds),
codeB = _getBitCode(b, bounds),
codeOut,
p,
newCode;
_lastCode = codeB;
while (true) {
if (!(codeA | codeB)) {
return [a, b];
}
if (codeA & codeB) {
return false;
}
codeOut = codeA || codeB;
p = _getEdgeIntersection(a, b, codeOut, bounds, round);
newCode = _getBitCode(p, bounds);
if (codeOut === codeA) {
a = p;
codeA = newCode;
} else {
b = p;
codeB = newCode;
}
}
}
export function _getEdgeIntersection(a, b, code, bounds, round) {
var dx = b.x - a.x,
dy = b.y - a.y,
min = bounds.min,
max = bounds.max,
x,
y;
if (code & 8) {
// top
x = a.x + (dx * (max.y - a.y)) / dy;
y = max.y;
} else if (code & 4) {
// bottom
x = a.x + (dx * (min.y - a.y)) / dy;
y = min.y;
} else if (code & 2) {
// right
x = max.x;
y = a.y + (dy * (max.x - a.x)) / dx;
} else if (code & 1) {
// left
x = min.x;
y = a.y + (dy * (min.x - a.x)) / dx;
}
return new Point(x, y, round);
}
export function _getBitCode(p, bounds) {
var code = 0;
if (p.x < bounds.min.x) {
// left
code |= 1;
} else if (p.x > bounds.max.x) {
// right
code |= 2;
}
if (p.y < bounds.min.y) {
// bottom
code |= 4;
} else if (p.y > bounds.max.y) {
// top
code |= 8;
}
return code;
}
function _sqDist(p1, p2) {
var dx = p2.x - p1.x,
dy = p2.y - p1.y;
return dx * dx + dy * dy;
}
export function _sqClosestPointOnSegment(p, p1, p2, sqDist) {
var x = p1.x,
y = p1.y,
dx = p2.x - x,
dy = p2.y - y,
dot = dx * dx + dy * dy,
t;
if (dot > 0) {
t = ((p.x - x) * dx + (p.y - y) * dy) / dot;
if (t > 1) {
x = p2.x;
y = p2.y;
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}
dx = p.x - x;
dy = p.y - y;
return sqDist ? dx * dx + dy * dy : new Point(x, y);
}
export function isFlat(latlngs) {
return (
!Util.isArray(latlngs[0]) ||
(typeof latlngs[0][0] !== "object" && typeof latlngs[0][0] !== "undefined")
);
}
export function _flat(latlngs) {
console.warn(
"Deprecated use of _flat, please use L.LineUtil.isFlat instead."
);
return isFlat(latlngs);
}
export function polylineCenter(latlngs, crs) {
var i, halfDist, segDist, dist, p1, p2, ratio, center;
if (!latlngs || latlngs.length === 0) {
throw new Error("latlngs not passed");
}
if (!isFlat(latlngs)) {
console.warn("latlngs are not flat! Only the first ring will be used");
latlngs = latlngs[0];
}
var centroidLatLng = toLatLng([0, 0]);
var bounds = toLatLngBounds(latlngs);
var areaBounds =
bounds.getNorthWest().distanceTo(bounds.getSouthWest()) *
bounds.getNorthEast().distanceTo(bounds.getNorthWest());
// tests showed that below 1700 rounding errors are happening
if (areaBounds < 1700) {
// getting a inexact center, to move the latlngs near to [0, 0] to prevent rounding errors
centroidLatLng = centroid(latlngs);
}
var len = latlngs.length;
var points = [];
for (i = 0; i < len; i++) {
var latlng = toLatLng(latlngs[i]);
points.push(
crs.project(
toLatLng([
latlng.lat - centroidLatLng.lat,
latlng.lng - centroidLatLng.lng,
])
)
);
}
for (i = 0, halfDist = 0; i < len - 1; i++) {
halfDist += points[i].distanceTo(points[i + 1]) / 2;
}
// The line is so small in the current view that all points are on the same pixel.
if (halfDist === 0) {
center = points[0];
} else {
for (i = 0, dist = 0; i < len - 1; i++) {
p1 = points[i];
p2 = points[i + 1];
segDist = p1.distanceTo(p2);
dist += segDist;
if (dist > halfDist) {
ratio = (dist - halfDist) / segDist;
center = [p2.x - ratio * (p2.x - p1.x), p2.y - ratio * (p2.y - p1.y)];
break;
}
}
}
var latlngCenter = crs.unproject(toPoint(center));
return toLatLng([
latlngCenter.lat + centroidLatLng.lat,
latlngCenter.lng + centroidLatLng.lng,
]);
}
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# 源码详解
# simplify(points,tolerance)
- 作用:对折线进行简化,减少点数,同时保持形状近似
- 输入:
points
:原始点数组(point
对象或坐标数组)tolerance
:简化阈值,距离大于此值的点会被保留
- 输出:简化后的点数组
- 算法步骤
- 顶点缩减(
reducePoints
)
- 遍历点数组,移除与前一点距离小于
sqTolerance
的点 - 保留首尾点,确保简化后的折线端点不变
Douglas-Peucker
算法(_simplifyDP
)
- 递归寻找偏离基线最远的点,若距离超过阈值则保留,并递归处理字段
- 使用二进制标记数据
markers
记录保留的点
# pointToSegmentDistance(p, p1, p2)
- 作用:计算点
p
到线段p1-p2
的最短距离 - 输入:
p
:目标点p1
、p2
:线段端点
- 输出:最短距离
- 算法步骤
- 调用
_sqClosestPointOnSegment
计算平方距离后开平方 - 计算点
p
到线段p1-p2
的投影点q
- 判断投影点是否在线段上,若在线段上则返回投影点到
p
的距离,否则返回p1
或p2
到p
的距离的最小值
- 调用
# closestPointOnSegment(p,p1,p2)
- 作用:返回线段
p1-p2
上离p
最近的点 - 输入:
p
:目标点p1
、p2
:线段端点
- 输出:离
p
最近的点(Point
对象) - 算法步骤
- 向量投影法
- 计算参数
t
表示投影点在线段上的位置 - 若
t
在[0,1]
范围内,则返回投影点,否则返回p1
或p2
中距离p
最近的点
# clipSegment(a,b,bounds,useLastCode,round)
- 作用:根据矩形边界裁剪线段
a-b
,返回可见部分或false
- 输入:
a
,b
:线段端点bounds
:矩形边界useLastCode
:是否使用上次的边界码round
:是否对结果进行四舍五入
- 输出:裁剪后的线段端点数组或
false
- 算法步骤:
Cohen-Sutherland
裁剪算法- 位编码(
_getBitCode
)
- 4 位二进制码表示线段端点相对于矩形边界的位置(左-1,右-2,下-4,上-8)
- 裁剪逻辑
- 若线段完全在矩形内(
codeA|codeB === 0
),则直接返回 - 若线段完全在矩形外(
codeA & code !==0
),则返回false
- 否则,计算与矩形的交点,替换外侧端点,重复计算直到线段完全在矩形内或矩形外
- 位编码(
# _getEdgeIntersection(a,b,code,bounds,round)
- 作用:计算线段
a-b
与矩形边界的交点 - 输入:
a
,b
:线段端点code
:边界码bounds
:矩形边界
- 输出:交点坐标(
Point
对象) - 实现:根据编码判断与哪条边相交,解直线方程求交点
# polylineCenter(latlngs,crs)
- 作用:计算折线的几何中心点(基于线段长度的中点)
- 输入:
latlngs
:折线的坐标数组crs
:地图坐标参考系统
- 输出:中心点坐标(
LatLng
对象) - 实现:
- 坐标平移:将折线平移到原点附近,减少浮点误差
- 投影转换:将地理坐标转为平面坐标
- 总长度计算:累加所有线段长度,找到中点所在的线段
- 线性插值:在中点所在的线段上按比例计算中心点坐标
# 总结
LineUtil
实现了地图开发中的核心算法:
- 简化:降低数据量,提升渲染性能。
- 裁剪 :优化绘制范围,避免无效渲染。
- 空间关系:支持交互逻辑(如点击检测)。
- 几何计算:支撑标注、居中视图等功能。
每个函数通过数学计算和高效算法,确保了 Leaflet 在大规模地理数据下的流畅性。
编辑 (opens new window)
上次更新: 2025/04/18, 07:17:03