@aibee/crc-bmap 0.8.39 → 0.8.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bmap.cjs.min.js +375 -375
- package/lib/bmap.esm.js +321 -4
- package/lib/bmap.esm.min.js +375 -375
- package/lib/bmap.min.js +375 -375
- package/lib/src/plugins/car-inertial-position/car-inertial-position.d.ts +33 -0
- package/lib/src/plugins/car-inertial-position/car-inertial-position.js +90 -0
- package/lib/src/plugins/car-inertial-position/compass.d.ts +44 -0
- package/lib/src/plugins/car-inertial-position/compass.js +119 -0
- package/lib/src/plugins/car-inertial-position/index.d.ts +2 -0
- package/lib/src/plugins/car-inertial-position/index.js +3 -0
- package/lib/src/plugins/car-inertial-position/utils.d.ts +41 -0
- package/lib/src/plugins/car-inertial-position/utils.js +150 -0
- package/lib/src/plugins/index.d.ts +1 -0
- package/lib/src/plugins/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { EventDispatcher } from "three";
|
|
2
|
+
import { Compass } from "./compass";
|
|
3
|
+
export interface CarPosition {
|
|
4
|
+
position: [number, number];
|
|
5
|
+
time: number;
|
|
6
|
+
type: "vision" | "beacon";
|
|
7
|
+
}
|
|
8
|
+
export interface CarPosInfo {
|
|
9
|
+
success: boolean;
|
|
10
|
+
pos: [number, number];
|
|
11
|
+
compass: null | number;
|
|
12
|
+
speed: number;
|
|
13
|
+
}
|
|
14
|
+
interface CarInertialPositionEventMap {
|
|
15
|
+
'change-compass': {
|
|
16
|
+
value: number;
|
|
17
|
+
};
|
|
18
|
+
'change-pos': {
|
|
19
|
+
value: CarPosInfo;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare class CarInertialPosition extends EventDispatcher<CarInertialPositionEventMap> {
|
|
23
|
+
history: CarPosition[];
|
|
24
|
+
compass: Compass;
|
|
25
|
+
compassAngle: number;
|
|
26
|
+
constructor();
|
|
27
|
+
startCompass(): void;
|
|
28
|
+
setPosition(position: CarPosition['position'], time: number): void;
|
|
29
|
+
setBeaconPosition(position: CarPosition['position'], time: number): void;
|
|
30
|
+
getPosition(): CarPosInfo;
|
|
31
|
+
dispose(): void;
|
|
32
|
+
}
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// 车辆惯性导航
|
|
2
|
+
import { EventDispatcher } from "three";
|
|
3
|
+
import { Compass } from "./compass";
|
|
4
|
+
import { calculateLineDirection, predictFuturePosition, predictFutureSpeed, transformSpeed } from "./utils";
|
|
5
|
+
export class CarInertialPosition extends EventDispatcher {
|
|
6
|
+
startCompass() {
|
|
7
|
+
this.compass.start();
|
|
8
|
+
}
|
|
9
|
+
setPosition(position, time) {
|
|
10
|
+
this.history.push({
|
|
11
|
+
position,
|
|
12
|
+
time,
|
|
13
|
+
type: "vision"
|
|
14
|
+
});
|
|
15
|
+
// 保留最近的6条数据
|
|
16
|
+
while(this.history.length > 6){
|
|
17
|
+
this.history.shift();
|
|
18
|
+
}
|
|
19
|
+
const posInfo = this.getPosition();
|
|
20
|
+
this.dispatchEvent({
|
|
21
|
+
type: "change-pos",
|
|
22
|
+
value: posInfo
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
setBeaconPosition(position, time) {
|
|
26
|
+
this.history.push({
|
|
27
|
+
position,
|
|
28
|
+
time,
|
|
29
|
+
type: "beacon"
|
|
30
|
+
});
|
|
31
|
+
const posInfo = this.getPosition();
|
|
32
|
+
this.dispatchEvent({
|
|
33
|
+
type: "change-pos",
|
|
34
|
+
value: posInfo
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
getPosition() {
|
|
38
|
+
if (this.history.length === 0) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
pos: [
|
|
42
|
+
0,
|
|
43
|
+
0
|
|
44
|
+
],
|
|
45
|
+
compass: null,
|
|
46
|
+
speed: 0
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const lastHistory = this.history.slice(-1)[0];
|
|
50
|
+
if (this.history.length < 2) {
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
pos: lastHistory.position,
|
|
54
|
+
compass: null,
|
|
55
|
+
speed: 0
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const visionHistory = this.history.filter((item)=>item.type === "vision");
|
|
59
|
+
// 推导到当前时间距离最后一个定位点的时间
|
|
60
|
+
const angle = calculateLineDirection(visionHistory.map((history)=>history.position));
|
|
61
|
+
if (angle !== null) {
|
|
62
|
+
this.compass.setAbsoluteCompass(angle, lastHistory.time);
|
|
63
|
+
}
|
|
64
|
+
const lastTime = lastHistory.time;
|
|
65
|
+
const deltaTime = Date.now() - lastTime;
|
|
66
|
+
// 预估后的速度
|
|
67
|
+
const speed = predictFutureSpeed(this.history, deltaTime);
|
|
68
|
+
// 根据角度和速度时间计算预估后的位置
|
|
69
|
+
const pos = this.compassAngle ? predictFuturePosition(lastHistory.position, speed, 360 - this.compassAngle, deltaTime) : lastHistory.position;
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
pos,
|
|
73
|
+
compass: this.compassAngle,
|
|
74
|
+
speed: transformSpeed(speed)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
dispose() {
|
|
78
|
+
this.compass.stop();
|
|
79
|
+
}
|
|
80
|
+
constructor(){
|
|
81
|
+
super(), this.history = [], this.compass = new Compass(), this.compassAngle = 0;
|
|
82
|
+
this.compass.addEventListener("compass", ({ value })=>{
|
|
83
|
+
this.compassAngle = value;
|
|
84
|
+
this.dispatchEvent({
|
|
85
|
+
type: "change-compass",
|
|
86
|
+
value
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EventDispatcher } from "three";
|
|
2
|
+
interface SensorEventMap {
|
|
3
|
+
start: {};
|
|
4
|
+
stop: {};
|
|
5
|
+
compass: {
|
|
6
|
+
value: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
interface CompassData {
|
|
10
|
+
timestamp: number;
|
|
11
|
+
res: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 监听compass 在获取到视觉定位的角度之后,计算视觉和compass的误差,保存一下这个误差,在后续定位中不断的调整这个误差
|
|
15
|
+
*/
|
|
16
|
+
export declare class Compass extends EventDispatcher<SensorEventMap> {
|
|
17
|
+
compassData: CompassData[];
|
|
18
|
+
absoluteCompass: null | {
|
|
19
|
+
time: number;
|
|
20
|
+
compass: number;
|
|
21
|
+
};
|
|
22
|
+
delta: number;
|
|
23
|
+
deltas: number[];
|
|
24
|
+
constructor();
|
|
25
|
+
start(): void;
|
|
26
|
+
deviceOrientationAbsHandler: (e: DeviceOrientationEvent) => void;
|
|
27
|
+
/**
|
|
28
|
+
* 设置一个根据视觉定位返回的绝对角度 校准delta
|
|
29
|
+
* @param compass
|
|
30
|
+
* @param time
|
|
31
|
+
*/
|
|
32
|
+
setAbsoluteCompass(compass: number, time: number): void;
|
|
33
|
+
emitCompass(compass: number): void;
|
|
34
|
+
/**
|
|
35
|
+
* 获取 compass 和 deviceMotion
|
|
36
|
+
*/
|
|
37
|
+
listenDeviceOrientation(): void;
|
|
38
|
+
checkSensor(): Promise<{
|
|
39
|
+
deviceOrientation: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
checkDeviceOrientation(): Promise<boolean>;
|
|
42
|
+
stop(): void;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { _ as _async_to_generator } from "@swc/helpers/_/_async_to_generator";
|
|
2
|
+
import { isNil } from "lodash";
|
|
3
|
+
import { isIphone } from "../../utils";
|
|
4
|
+
import { EventDispatcher } from "three";
|
|
5
|
+
import { removeOutliers } from "./utils";
|
|
6
|
+
/**
|
|
7
|
+
* 监听compass 在获取到视觉定位的角度之后,计算视觉和compass的误差,保存一下这个误差,在后续定位中不断的调整这个误差
|
|
8
|
+
*/ export class Compass extends EventDispatcher {
|
|
9
|
+
start() {
|
|
10
|
+
this.listenDeviceOrientation();
|
|
11
|
+
this.dispatchEvent({
|
|
12
|
+
type: "start"
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 设置一个根据视觉定位返回的绝对角度 校准delta
|
|
17
|
+
* @param compass
|
|
18
|
+
* @param time
|
|
19
|
+
*/ setAbsoluteCompass(compass, time) {
|
|
20
|
+
if (!this.compassData.length) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this.absoluteCompass = {
|
|
24
|
+
compass: compass,
|
|
25
|
+
time
|
|
26
|
+
};
|
|
27
|
+
let cutTimeCompassIndex = this.compassData.findIndex((item)=>item.timestamp >= time);
|
|
28
|
+
if (cutTimeCompassIndex === -1) {
|
|
29
|
+
cutTimeCompassIndex = this.compassData.length - 1;
|
|
30
|
+
}
|
|
31
|
+
const prevCompass = this.compassData[cutTimeCompassIndex - 1];
|
|
32
|
+
const nextCompass = this.compassData[cutTimeCompassIndex];
|
|
33
|
+
let curCompass;
|
|
34
|
+
if (!prevCompass) {
|
|
35
|
+
curCompass = nextCompass;
|
|
36
|
+
} else if (!nextCompass) {
|
|
37
|
+
return;
|
|
38
|
+
} else {
|
|
39
|
+
curCompass = nextCompass.timestamp - time > prevCompass.timestamp - time ? prevCompass : nextCompass;
|
|
40
|
+
}
|
|
41
|
+
const delta = compass - curCompass.res;
|
|
42
|
+
this.deltas.push(delta);
|
|
43
|
+
const deltas = removeOutliers(this.deltas);
|
|
44
|
+
this.delta = deltas.reduce((sum, cur)=>sum + cur, 0) / deltas.length;
|
|
45
|
+
}
|
|
46
|
+
emitCompass(compass) {
|
|
47
|
+
// 把 compass 限制在 0 ~ 360
|
|
48
|
+
this.dispatchEvent({
|
|
49
|
+
type: "compass",
|
|
50
|
+
value: (compass + this.delta + 360) % 360
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 获取 compass 和 deviceMotion
|
|
55
|
+
*/ listenDeviceOrientation() {
|
|
56
|
+
if (isIphone) {
|
|
57
|
+
window.addEventListener("deviceorientation", this.deviceOrientationAbsHandler, false);
|
|
58
|
+
} else {
|
|
59
|
+
window.addEventListener("deviceorientationabsolute", this.deviceOrientationAbsHandler, {
|
|
60
|
+
absolute: true
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
checkSensor() {
|
|
65
|
+
var _this = this;
|
|
66
|
+
return _async_to_generator(function*() {
|
|
67
|
+
const deviceOrientation = yield _this.checkDeviceOrientation();
|
|
68
|
+
return {
|
|
69
|
+
deviceOrientation
|
|
70
|
+
};
|
|
71
|
+
})();
|
|
72
|
+
}
|
|
73
|
+
checkDeviceOrientation() {
|
|
74
|
+
return _async_to_generator(function*() {
|
|
75
|
+
var _window_DeviceOrientationEvent;
|
|
76
|
+
if (!isIphone) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (typeof window.DeviceOrientationEvent !== "undefined" && typeof ((_window_DeviceOrientationEvent = window.DeviceOrientationEvent) == null ? void 0 : _window_DeviceOrientationEvent.requestPermission) === "function") {
|
|
80
|
+
try {
|
|
81
|
+
var _window_DeviceOrientationEvent1;
|
|
82
|
+
const permission = yield (_window_DeviceOrientationEvent1 = window.DeviceOrientationEvent) == null ? void 0 : _window_DeviceOrientationEvent1.requestPermission();
|
|
83
|
+
return permission === "granted";
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
})();
|
|
90
|
+
}
|
|
91
|
+
stop() {
|
|
92
|
+
if (isIphone) {
|
|
93
|
+
window.removeEventListener("deviceorientation", this.deviceOrientationAbsHandler, false);
|
|
94
|
+
} else {
|
|
95
|
+
window.removeEventListener("deviceorientationabsolute", this.deviceOrientationAbsHandler, {
|
|
96
|
+
absolute: true
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
this.compassData = [];
|
|
100
|
+
this.dispatchEvent({
|
|
101
|
+
type: "stop"
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
constructor(){
|
|
105
|
+
super(), this.compassData = [], this.absoluteCompass = null, this.delta = 0, this.deltas = [], this.deviceOrientationAbsHandler = (e)=>{
|
|
106
|
+
const currTime = Date.now();
|
|
107
|
+
const { alpha, beta, gamma } = e;
|
|
108
|
+
if (isNil(alpha) || isNil(beta) || isNil(gamma)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const compass = isIphone ? e.webkitCompassHeading : 360 - alpha;
|
|
112
|
+
this.compassData.push({
|
|
113
|
+
timestamp: currTime,
|
|
114
|
+
res: compass
|
|
115
|
+
});
|
|
116
|
+
this.emitCompass(compass);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CarPosition } from "./car-inertial-position";
|
|
2
|
+
/**
|
|
3
|
+
* 计算一组点位的斜率和方向
|
|
4
|
+
* @param points
|
|
5
|
+
* @returns
|
|
6
|
+
*/
|
|
7
|
+
export declare function calculateLineDirection(points: [number, number][]): number | null;
|
|
8
|
+
/**
|
|
9
|
+
* 计算速度,单位是m/ms km/s
|
|
10
|
+
* @param positions
|
|
11
|
+
* @returns
|
|
12
|
+
*/
|
|
13
|
+
export declare function calculateInstantaneousSpeed(positions: CarPosition[]): number[];
|
|
14
|
+
/**
|
|
15
|
+
* 预测一段时间后的速度
|
|
16
|
+
* @param speeds
|
|
17
|
+
* @param timeIntervalMs
|
|
18
|
+
* @returns
|
|
19
|
+
*/
|
|
20
|
+
export declare function predictFutureSpeed(positions: CarPosition[], timeIntervalMs: number): number;
|
|
21
|
+
/**
|
|
22
|
+
* 转换速度从 m/ms 转成 km/h
|
|
23
|
+
* @param speed
|
|
24
|
+
* @returns
|
|
25
|
+
*/
|
|
26
|
+
export declare function transformSpeed(speed: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* 预估未来的位置
|
|
29
|
+
* @param lastPosition
|
|
30
|
+
* @param speed
|
|
31
|
+
* @param angle
|
|
32
|
+
* @param time
|
|
33
|
+
* @returns
|
|
34
|
+
*/
|
|
35
|
+
export declare function predictFuturePosition(lastPosition: [number, number], speed: number, angle: number, time: number): [number, number];
|
|
36
|
+
/**
|
|
37
|
+
* 过滤偏差大的值
|
|
38
|
+
* @param data
|
|
39
|
+
* @returns
|
|
40
|
+
*/
|
|
41
|
+
export declare function removeOutliers(data: number[]): number[];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Vector2 } from "three";
|
|
2
|
+
/**
|
|
3
|
+
* 计算一组点位的斜率和方向
|
|
4
|
+
* @param points
|
|
5
|
+
* @returns
|
|
6
|
+
*/ export function calculateLineDirection(points) {
|
|
7
|
+
const n = points.length;
|
|
8
|
+
if (n < 2) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const angles = [];
|
|
12
|
+
for(let i = 1; i < points.length; i++){
|
|
13
|
+
const point1 = new Vector2(points[i][0], points[i][1]);
|
|
14
|
+
const point0 = new Vector2(points[i - 1][0], points[i - 1][1]);
|
|
15
|
+
if (point1.distanceTo(point0) === 0) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const radians = new Vector2().subVectors(point1, point0).angle() // 于X轴的正方向的夹角 这个是顺时针的角度
|
|
19
|
+
;
|
|
20
|
+
const angle = 360 - (radians / Math.PI * 180 - 90 + 360) % 360;
|
|
21
|
+
angles.push(angle);
|
|
22
|
+
}
|
|
23
|
+
const filteredAngles = removeOutliers(angles);
|
|
24
|
+
if (!filteredAngles.length) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
let weight = 0;
|
|
28
|
+
// 最近的方向权重更大
|
|
29
|
+
const averageAngle = filteredAngles.reduce((sum, angle, i)=>{
|
|
30
|
+
weight += i + 1;
|
|
31
|
+
return sum + angle * (i + 1);
|
|
32
|
+
}, 0) / weight;
|
|
33
|
+
return averageAngle;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 计算速度,单位是m/ms km/s
|
|
37
|
+
* @param positions
|
|
38
|
+
* @returns
|
|
39
|
+
*/ export function calculateInstantaneousSpeed(positions) {
|
|
40
|
+
const speeds = [];
|
|
41
|
+
for(let i = 1; i < positions.length; i++){
|
|
42
|
+
const prev = positions[i - 1];
|
|
43
|
+
const current = positions[i];
|
|
44
|
+
// 计算位置变化
|
|
45
|
+
const deltaX = current.position[0] - prev.position[0];
|
|
46
|
+
const deltaY = current.position[1] - prev.position[1];
|
|
47
|
+
const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
|
|
48
|
+
// 计算时间变化
|
|
49
|
+
const deltaTime = current.time - prev.time; // 时间单位为毫秒
|
|
50
|
+
// 计算瞬时速度
|
|
51
|
+
if (deltaTime > 0) {
|
|
52
|
+
const speed = distance / deltaTime;
|
|
53
|
+
speeds.push(speed);
|
|
54
|
+
} else {
|
|
55
|
+
speeds.push(0); // 如果时间变化为0,速度为0
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return speeds;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 预测一段时间后的速度
|
|
62
|
+
* @param speeds
|
|
63
|
+
* @param timeIntervalMs
|
|
64
|
+
* @returns
|
|
65
|
+
*/ export function predictFutureSpeed(positions, timeIntervalMs) {
|
|
66
|
+
const speeds = calculateInstantaneousSpeed(positions);
|
|
67
|
+
const n = speeds.length;
|
|
68
|
+
if (n === 0) {
|
|
69
|
+
return 0; // 如果没有速度数据,返回0
|
|
70
|
+
}
|
|
71
|
+
// 获取当前速度
|
|
72
|
+
const currentSpeed = speeds[n - 1];
|
|
73
|
+
// 计算加速度(简单的平均加速度)
|
|
74
|
+
let acceleration = 0;
|
|
75
|
+
if (n > 1) {
|
|
76
|
+
const speedDifferences = speeds.slice(1).map((speed, index)=>speed - speeds[index]);
|
|
77
|
+
const time = positions.slice(1).reduce((sum, cur, index)=>sum + (cur.time - positions[index].time), 0);
|
|
78
|
+
acceleration = speedDifferences.reduce((sum, value)=>sum + value, 0) / time;
|
|
79
|
+
}
|
|
80
|
+
// 预测未来速度 m/ms
|
|
81
|
+
const futureSpeed = Math.max(currentSpeed + acceleration * timeIntervalMs, 0);
|
|
82
|
+
return futureSpeed;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 转换速度从 m/ms 转成 km/h
|
|
86
|
+
* @param speed
|
|
87
|
+
* @returns
|
|
88
|
+
*/ export function transformSpeed(speed) {
|
|
89
|
+
// speed m/ms km/s
|
|
90
|
+
return speed * 60 * 60; // km/h
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 预估未来的位置
|
|
94
|
+
* @param lastPosition
|
|
95
|
+
* @param speed
|
|
96
|
+
* @param angle
|
|
97
|
+
* @param time
|
|
98
|
+
* @returns
|
|
99
|
+
*/ export function predictFuturePosition(lastPosition, speed, angle, time) {
|
|
100
|
+
// 将角度转换为弧度
|
|
101
|
+
const angleInRadians = angle * (Math.PI / 180);
|
|
102
|
+
const distance = speed * time;
|
|
103
|
+
console.log("angle speed", speed, distance);
|
|
104
|
+
const vec = new Vector2(lastPosition[0], lastPosition[1]);
|
|
105
|
+
// 计算角度在x和y方向的分量
|
|
106
|
+
const vX = Math.cos(angleInRadians);
|
|
107
|
+
const vY = Math.sin(angleInRadians);
|
|
108
|
+
const dir = new Vector2(vX, vY);
|
|
109
|
+
vec.add(dir.normalize().multiplyScalar(distance));
|
|
110
|
+
// // 计算未来位置
|
|
111
|
+
// const futureX = lastPosition[0] + vX * time;
|
|
112
|
+
// const futureY = lastPosition[1] + vY * time;
|
|
113
|
+
return [
|
|
114
|
+
vec.x,
|
|
115
|
+
vec.y
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 过滤偏差大的值
|
|
120
|
+
* @param data
|
|
121
|
+
* @returns
|
|
122
|
+
*/ export function removeOutliers(data) {
|
|
123
|
+
// 排序数据
|
|
124
|
+
const d = [
|
|
125
|
+
...data
|
|
126
|
+
].sort((a, b)=>a - b);
|
|
127
|
+
// 计算四分位数 Q1 和 Q3
|
|
128
|
+
const q1 = percentile(d, 25);
|
|
129
|
+
const q3 = percentile(d, 75);
|
|
130
|
+
// 计算四分位距 IQR
|
|
131
|
+
const iqr = q3 - q1;
|
|
132
|
+
// 计算上下界限
|
|
133
|
+
const lowerBound = q1 - 1.5 * iqr;
|
|
134
|
+
const upperBound = q3 + 1.5 * iqr;
|
|
135
|
+
// 筛选出正常范围内的数据
|
|
136
|
+
const filteredData = d.filter((value)=>value >= lowerBound && value <= upperBound);
|
|
137
|
+
const originSortData = data.filter((item)=>filteredData.includes(item));
|
|
138
|
+
return originSortData;
|
|
139
|
+
}
|
|
140
|
+
// 计算百分位数(分位数)
|
|
141
|
+
function percentile(arr, p) {
|
|
142
|
+
const index = p / 100 * (arr.length - 1);
|
|
143
|
+
const lower = Math.floor(index);
|
|
144
|
+
const upper = Math.ceil(index);
|
|
145
|
+
if (lower === upper) {
|
|
146
|
+
return arr[lower];
|
|
147
|
+
}
|
|
148
|
+
const weight = index - lower;
|
|
149
|
+
return arr[lower] * (1 - weight) + arr[upper] * weight;
|
|
150
|
+
}
|
package/lib/src/plugins/index.js
CHANGED