frame.akima
Version:
A package for Akima interpolation
403 lines (402 loc) • 18.5 kB
JavaScript
import { AkimaPoint } from './AkimaPoint';
import { assertEquidistance, getCenter, printDistances, round, transform, } from './utils';
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 AkimaPoint(xMin + (xMax - xMin) / 2, yMin + (yMax - yMin) / 2);
let angleRadiiArray = [];
if (points.length >= 2) {
angleRadiiArray = points.map((point) => {
const p = new AkimaPoint(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 = [];
akimaFunc;
static createPointsByAkima(points, nrPoints, print = false) {
let distance = Number.POSITIVE_INFINITY;
let previousDistance = 0;
const threshold = 0.05;
let iteration = 0;
let akima = undefined;
let calculatedPoints = points.map((point) => new AkimaPoint(point.X, point.Y));
let center = getCenter(calculatedPoints, print);
const addedCenter = new AkimaPoint(center.X, center.Y);
if (print) {
console.log(`------------------ start iterations (center: [${round(center.X)}, ${round(center.Y)}]) ------------------`);
}
while (distance > threshold && iteration < 100) {
// transform points to center
const transformedPoints = transform(calculatedPoints, center);
if (print) {
console.log(`transformedPoints: ${transformedPoints
.map((p) => `[${round(p.X)}, ${round(p.Y)}]`)
.join(', ')}`);
}
// get the Akima spline function for transformed points
akima = Akima.createAkimaFromPoints(transformedPoints);
if (print) {
const { maxDistance, averageDistance } = printDistances(points, akima, addedCenter);
console.log(`distance between original points and points on Akima Spline:`);
console.log(`- maxDistance: ${round(maxDistance)}`);
console.log(`- averageDistance: ${round(averageDistance)}`);
console.log();
}
// create new calculated points based on the Akima spline
calculatedPoints = akima.createPointArray(nrPoints, print);
if (print) {
// assert equidistance of calculated points
const maxDeviation = assertEquidistance(calculatedPoints, 3);
console.log(`maximum deviation with respect to equidistance: ${maxDeviation}`);
console.log(`calculated center: [${Math.round(center.X * 1000) / 1000}, ${Math.round(center.Y * 1000) / 1000}], addedCenter: [${Math.round(addedCenter.X * 1000) / 1000}, ${Math.round(addedCenter.Y * 1000) / 1000}] -> dist: ${distance} ( diff to previous: ${Math.abs(distance - previousDistance)})`);
if (print) {
console.log(`------------------ iteration ${iteration} finished ------------------`);
}
}
// get the current center and distance from origin
center = getCenter(calculatedPoints);
addedCenter.X += center.X;
addedCenter.Y += center.Y;
previousDistance = distance;
distance = Math.hypot(center.X, center.Y);
++iteration;
} // while (distance > threshold && iteration < 100) {
return { points: calculatedPoints, akima, center: addedCenter };
}
static createAkimaFromPoints(points) {
const { xValues, yValues } = preparePointsForAkima(points.map((point) => ({
x: point.X,
y: point.Y,
})));
const akima = new Akima(xValues, yValues);
return akima;
}
constructor(xValues, yValues) {
this.xVal = xValues;
this.yVal = yValues;
this.akimaFunc = this.createInterpolator();
}
get Spline() {
return this.akimaFunc;
}
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;
}
getPointFromAngle(angle) {
const radius = this.getValueFromAngle(angle);
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
return new AkimaPoint(x, y);
}
createPointArray(nrPoints, print = false) {
const points = [];
if (print) {
console.log();
console.log(`(Akima) createPointArray --- Creating ${nrPoints} points by Akima Spline ---`);
}
for (let loop = 0; loop < nrPoints; loop += 1) {
const angle = (Math.PI * 2 * loop) / nrPoints;
const point = this.getPointFromAngle(angle);
points.push(point);
const relAtan = point.relAtan(new AkimaPoint(0, 0));
const delta = Math.abs(relAtan - angle);
if (print) {
console.log(`(Akima) createPointArray.point[${loop}]: [${point.X}, ${point.Y}] -> angle: ${round(angle)}, relAtan: ${round(relAtan)}`);
}
if (delta > 1e-3) {
throw new Error(`(Akima) createPointArray.Point[${loop}] = [${round(point.X)}, ${round(point.Y)}] is not valid, angle = ${angle}, relAtan = ${relAtan}, delta: ${delta}`);
}
if (print) {
console.log(`(Akima) createPointArray.point[${loop}]: [${point.X}, ${point.Y}]`);
}
}
return points;
}
} // 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 AkimaPoint(xMin + (xMax - xMin) / 2, yMin + (yMax - yMin) / 2);
const akimaKoords = [];
let angleRadiiArray = [];
if (points.length >= 2) {
angleRadiiArray = points.map((point) => {
const p = new AkimaPoint(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 AkimaPoint(x, y, new AkimaPoint(universeCenter.x, universeCenter.y)));
}
}
return akimaKoords.map((point) => ({
x: point.X,
y: point.Y,
}));
}