frame.akima
Version:
A package for Akima interpolation
309 lines (308 loc) • 13.7 kB
JavaScript
import { Point } from './Point';
export function preparePointsForAkima(points) {
let xMin = Infinity;
let xMax = -Infinity;
let yMin = Infinity;
let yMax = -Infinity;
if (points.length >= 2) {
xMin = points.reduce((min, p) => Math.min(min, p.x), Infinity);
xMax = points.reduce((max, p) => Math.max(max, p.x), -Infinity);
yMin = points.reduce((min, p) => Math.min(min, p.y), Infinity);
yMax = points.reduce((max, p) => Math.max(max, p.y), -Infinity);
}
const center = new Point(xMin + (xMax - xMin) / 2, yMin + (yMax - yMin) / 2);
let angleRadiiArray = [];
if (points.length >= 2) {
angleRadiiArray = points.map((point) => {
const p = new Point(point.x, point.y);
return {
angle: p.relAtan(center),
radius: p.Radius,
};
});
angleRadiiArray.sort((a, b) => a.angle - b.angle);
}
return {
xValues: angleRadiiArray.map((angleRadii) => angleRadii.angle),
yValues: angleRadiiArray.map((angleRadii) => angleRadii.radius),
};
}
export class Akima {
xVal = [];
yVal = [];
koefAkima = [];
constructor(xValues, yValues) {
this.xVal = xValues;
this.yVal = yValues;
}
berechnenDerSteigungen(size, step_x, y) {
const m = new Array(size).fill(0);
const t = new Array(size).fill(0);
for (let i = 0; i < size - 1; i++) {
m[i] = (y[i + 1] - y[i]) / step_x[i]; //Geradensteigungen
}
for (let i = 2; i < size - 2; i++) {
t[i] = Math.abs(m[i + 1] - m[i]) + Math.abs(m[i - 1] - m[i - 2]); // Summe: abs(diff Steigungen links) + abs(diff Steigungen rechts)
if (t[i] < 1e-7) {
this.koefAkima[i - 2][0] = (m[i - 1] + m[i]) * 0.5; // Steigung: Mittelwert r/l
}
else {
this.koefAkima[i - 2][0] =
(m[i - 1] * Math.abs(m[i + 1] - m[i]) +
m[i] * Math.abs(m[i - 1] - m[i - 2])) /
t[i];
}
}
}
berechnenDerRestlichenKoeffizienten(size, step_x, y) {
for (let i = 2; i < size - 2; i++ //i < size - 2: alle Stützstellen
) {
if (i < size - 3) {
this.koefAkima[i - 2][1] =
(3 * (y[i + 1] - y[i])) / (step_x[i] * step_x[i]) -
(2 * this.koefAkima[i - 2][0] + this.koefAkima[i - 1][0]) / step_x[i];
this.koefAkima[i - 2][2] =
(this.koefAkima[i - 2][0] + this.koefAkima[i - 1][0]) /
(step_x[i] * step_x[i]) -
(2 * (y[i + 1] - y[i])) / (step_x[i] * step_x[i] * step_x[i]);
} //letzte Stützstelle
else {
this.koefAkima[i - 2][1] =
(3 * (y[i + 1] - y[i])) / (step_x[i] * step_x[i]) -
(2 * this.koefAkima[i - 2][0] + this.koefAkima[i - 2 /*!*/][0]) /
step_x[i];
this.koefAkima[i - 2][2] =
(this.koefAkima[i - 2][0] + this.koefAkima[i - 2 /*!*/][0]) /
(step_x[i] * step_x[i]) -
(2 * (y[i + 1] - y[i])) / (step_x[i] * step_x[i] * step_x[i]);
}
}
}
createInterpolator() {
//Polynomkoeffizienten, Index=0,1,2 entsprechende Potenz=1,2,3
this.koefAkima = Array.from({ length: this.xVal.length }, () => Array(3).fill(0));
const size = this.xVal.length + 4; //erweitern der x-Werte an Anfang und Ende
const x = new Array(size).fill(0);
const y = new Array(size).fill(0);
const step_x = new Array(size).fill(0);
if (this.xVal.length < 3) {
throw new Error('ascendingValues must have at least 3 elements');
}
for (let i = 0; i < this.xVal.length; i++) {
// Übertragen der Stützstellenwerte auf interne x,y-Werte
x[2 + i] = this.xVal[i];
y[2 + i] = this.yVal[i];
}
// Berechnen der Stützstellenabstände step_x
for (let i = 0; i < this.xVal.length - 1; i++) {
step_x[i + 2] = this.xVal[i + 1] - this.xVal[i];
if (step_x[i + 2] < 1e-7) {
let warning = `this.xVal.length = ${this.xVal.length};\n`;
for (let j = 0; j < this.xVal.length; j++) {
warning += `this.xVal[${j}] = ${this.xVal[j]};\n`;
}
// Abstand darf nicht Null oder negativ sein!
throw new Error(`step_x[${i + 2}] is <= 0, got ${step_x[i + 2]} between this.xVal[${i}] and this.xVal[${i + 1}] = [${this.xVal[i]}, ${this.xVal[i + 1]}]` +
'\n' +
`Obviously there are some duplicate points` +
'\n' +
warning);
}
}
//Randstützstellen
x[1] = x[2] - step_x[2];
step_x[1] = step_x[2];
x[0] = x[1] - step_x[1];
step_x[0] = step_x[1];
x[size - 2] = x[size - 3] + step_x[size - 4];
step_x[size - 3] = step_x[size - 4];
x[size - 1] = x[size - 2] + step_x[size - 4];
step_x[size - 2] = step_x[size - 3];
//(nicht ganz korrekt bei nicht-äquidistanten Knoten)
y[0] = this.yVal[this.yVal.length - 2];
y[1] = this.yVal[this.yVal.length - 1];
y[size - 2] = this.yVal[0];
y[size - 1] = this.yVal[1];
//Berechnen der Steigungen -> Empirische Formel nach Akima...
this.berechnenDerSteigungen(size, step_x, y);
//Berechnen der restlichen Koeffizienten
this.berechnenDerRestlichenKoeffizienten(size, step_x, y);
return this.getValueFromAngle.bind(this);
} // createInterpolator()
getValueFromAngle(angle) {
let y0 = 0;
let index = this.xVal.length - 1; //für Extrapolation rechts (für Notfall)
if (angle < this.xVal[0]) {
index = 0; //für Extrapolation links (für Notfall)
}
else {
for (let i = 0; i < this.xVal.length - 1; i++) {
if (angle >= this.xVal[i] && angle < this.xVal[i + 1]) {
index = i;
break;
}
}
}
y0 = this.koefAkima[index][2];
y0 = y0 * (angle - this.xVal[index]) + this.koefAkima[index][1];
y0 = y0 * (angle - this.xVal[index]) + this.koefAkima[index][0];
y0 = y0 * (angle - this.xVal[index]) + this.yVal[index];
return y0;
}
} // class Akima
export function calcSplineKoef_Akima(xw_ascending, yw
// koef_akima: number[][]
) {
//WHauk: Konvertierung C#, Erweiterungen, Vereinfachungen (Extrapolation, Ringschluss), 2010
let fehler = 0;
//Polynomkoeffizienten, Index=0,1,2 entsprechende Potenz=1,2,3
const koef_akima = Array.from({ length: xw_ascending.length }, () => Array(3).fill(0));
const size = xw_ascending.length + 4; //erweitern der x-Werte an Anfang und Ende
const x = new Array(size).fill(0);
const y = new Array(size).fill(0);
const step_x = new Array(size).fill(0);
if (xw_ascending.length < 3) {
fehler = 1;
throw new Error('ascendingValues must have at least 3 elements');
}
if (fehler === 0) {
for (let i = 0; i < xw_ascending.length; i++) {
// Übertragen der Stützstellenwerte auf interne x,y-Werte
x[2 + i] = xw_ascending[i];
y[2 + i] = yw[i];
}
// Berechnen der Stützstellenabstände step_x
for (let i = 0; i < xw_ascending.length - 1; i++) {
step_x[i + 2] = xw_ascending[i + 1] - xw_ascending[i];
if (step_x[i + 2] < 1e-7) {
let warning = `ascendingValues.length = ${xw_ascending.length};\n`;
for (let j = 0; j < xw_ascending.length; j++) {
warning += `ascendingValues[${j}] = ${xw_ascending[j]};\n`;
}
// Abstand darf nicht Null oder negativ sein!
throw new Error(`step_x[${i + 2}] is < 0, got ${step_x[i + 2]} between ascendingValues[${i}] and ascendingValues[${i + 1}] = [${xw_ascending[i]}, ${xw_ascending[i + 1]}]` +
'\n' +
`Obviously the coords are not sorted in an ascending order or there are points with the same ascending value` +
'\n' +
warning);
}
}
//Randstützstellen
x[1] = x[2] - step_x[2];
step_x[1] = step_x[2];
x[0] = x[1] - step_x[1];
step_x[0] = step_x[1];
x[size - 2] = x[size - 3] + step_x[size - 4];
step_x[size - 3] = step_x[size - 4];
x[size - 1] = x[size - 2] + step_x[size - 4];
step_x[size - 2] = step_x[size - 3];
//(nicht ganz korrekt bei nicht-äquidistanten Knoten)
y[0] = yw[yw.length - 2];
y[1] = yw[yw.length - 1];
y[size - 2] = yw[0];
y[size - 1] = yw[1];
//Berechnen der Steigungen -> Empirische Formel nach Akima...
const m = new Array(size).fill(0);
const t = new Array(size).fill(0);
for (let i = 0; i < size - 1; i++) {
m[i] = (y[i + 1] - y[i]) / step_x[i]; //Geradensteigungen
}
for (let i = 2; i < size - 2; i++) {
t[i] = Math.abs(m[i + 1] - m[i]) + Math.abs(m[i - 1] - m[i - 2]); //Summe: abs(diff Steigungen links) + abs(diff Steigungen rechts)
if (t[i] < 1e-7) {
koef_akima[i - 2][0] = (m[i - 1] + m[i]) * 0.5; //Steigung: Mittelwert r/l
}
else {
koef_akima[i - 2][0] =
(m[i - 1] * Math.abs(m[i + 1] - m[i]) +
m[i] * Math.abs(m[i - 1] - m[i - 2])) /
t[i];
}
}
//Berechnen der restlichen Koeffizienten
for (let i = 2; i < size - 2; i++ //i < size - 2: alle Stützstellen
) {
if (i < size - 3) {
koef_akima[i - 2][1] =
(3 * (y[i + 1] - y[i])) / (step_x[i] * step_x[i]) -
(2 * koef_akima[i - 2][0] + koef_akima[i - 1][0]) / step_x[i];
koef_akima[i - 2][2] =
(koef_akima[i - 2][0] + koef_akima[i - 1][0]) /
(step_x[i] * step_x[i]) -
(2 * (y[i + 1] - y[i])) / (step_x[i] * step_x[i] * step_x[i]);
} //letzte Stützstelle
else {
koef_akima[i - 2][1] =
(3 * (y[i + 1] - y[i])) / (step_x[i] * step_x[i]) -
(2 * koef_akima[i - 2][0] + koef_akima[i - 2 /*!*/][0]) / step_x[i];
koef_akima[i - 2][2] =
(koef_akima[i - 2][0] + koef_akima[i - 2 /*!*/][0]) /
(step_x[i] * step_x[i]) -
(2 * (y[i + 1] - y[i])) / (step_x[i] * step_x[i] * step_x[i]);
}
}
} //if (fehler == 0)
return koef_akima;
}
export function getSplineWert(x0, xwerte, ywerte, koef) {
let y0 = 0;
let index = xwerte.length - 1; //für Extrapolation rechts (für Notfall)
if (x0 < xwerte[0]) {
index = 0; //für Extrapolation links (für Notfall)
}
else {
for (let i = 0; i < xwerte.length - 1; i++) {
if (x0 >= xwerte[i] && x0 < xwerte[i + 1]) {
index = i;
break;
}
}
}
y0 = koef[index][2];
y0 = y0 * (x0 - xwerte[index]) + koef[index][1];
y0 = y0 * (x0 - xwerte[index]) + koef[index][0];
y0 = y0 * (x0 - xwerte[index]) + ywerte[index];
return y0;
}
export function createAkimaRadii(points, universeCenter, nrRadii) {
let xMin = Infinity;
let xMax = -Infinity;
let yMin = Infinity;
let yMax = -Infinity;
if (points.length >= 2) {
xMin = points.reduce((min, p) => Math.min(min, p.x), Infinity);
xMax = points.reduce((max, p) => Math.max(max, p.x), -Infinity);
yMin = points.reduce((min, p) => Math.min(min, p.y), Infinity);
yMax = points.reduce((max, p) => Math.max(max, p.y), -Infinity);
}
const center = new Point(xMin + (xMax - xMin) / 2, yMin + (yMax - yMin) / 2);
const akimaKoords = [];
let angleRadiiArray = [];
if (points.length >= 2) {
angleRadiiArray = points.map((point) => {
const p = new Point(point.x, point.y);
return {
angle: p.relAtan(center),
radius: p.Radius,
};
});
angleRadiiArray.sort((a, b) => a.angle - b.angle);
const koef = calcSplineKoef_Akima(angleRadiiArray.map((angleRadii) => angleRadii.angle), angleRadiiArray.map((angleRadii) => angleRadii.radius));
const positions = [];
for (let i = 0; i < nrRadii; i++) {
const pos = ((Math.PI * 2) / nrRadii) * i;
positions.push(pos);
}
for (let i = 0; i < positions.length; i++) {
const currentAngle = positions[i];
const currentRadius = getSplineWert(currentAngle, angleRadiiArray.map((angleRadii) => angleRadii.angle), angleRadiiArray.map((angleRadii) => angleRadii.radius), koef);
const y = Math.round(Math.sin(currentAngle) * currentRadius * 1000) / 1000;
const x = Math.round(Math.cos(currentAngle) * currentRadius * 1000) / 1000;
akimaKoords.push(new Point(x, y, new Point(universeCenter.x, universeCenter.y)));
}
}
return akimaKoords.map((point) => ({
x: point.X,
y: point.Y,
}));
}