UNPKG

frame.akima

Version:

A package for Akima interpolation

403 lines (402 loc) 18.5 kB
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, })); }