123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711 |
- import { Matrix, Vector3 } from "../Maths/math.vector.js";
- // This implementation was based on the original MIT-licensed TRACE repository
- // from https://github.com/septagon/TRACE.
- /**
- * Generic implementation of Levenshtein distance.
- */
- var Levenshtein;
- (function (Levenshtein) {
- /**
- * Alphabet from which to construct sequences to be compared using Levenshtein
- * distance.
- */
- class Alphabet {
- /**
- * Serialize the Alphabet to JSON string.
- * @returns JSON serialization
- */
- serialize() {
- const jsonObject = {};
- const characters = new Array(this._characterToIdx.size);
- this._characterToIdx.forEach((v, k) => {
- characters[v] = k;
- });
- jsonObject["characters"] = characters;
- jsonObject["insertionCosts"] = this._insertionCosts;
- jsonObject["deletionCosts"] = this._deletionCosts;
- jsonObject["substitutionCosts"] = this._substitutionCosts;
- return JSON.stringify(jsonObject);
- }
- /**
- * Parse an Alphabet from a JSON serialization.
- * @param json JSON string to deserialize
- * @returns deserialized Alphabet
- */
- static Deserialize(json) {
- const jsonObject = JSON.parse(json);
- const alphabet = new Alphabet(jsonObject["characters"]);
- alphabet._insertionCosts = jsonObject["insertionCosts"];
- alphabet._deletionCosts = jsonObject["deletionCosts"];
- alphabet._substitutionCosts = jsonObject["substitutionCosts"];
- return alphabet;
- }
- /**
- * Create a new Alphabet.
- * @param characters characters of the alphabet
- * @param charToInsertionCost function mapping characters to insertion costs
- * @param charToDeletionCost function mapping characters to deletion costs
- * @param charsToSubstitutionCost function mapping character pairs to substitution costs
- */
- constructor(characters, charToInsertionCost = null, charToDeletionCost = null, charsToSubstitutionCost = null) {
- charToInsertionCost = charToInsertionCost ?? (() => 1);
- charToDeletionCost = charToDeletionCost ?? (() => 1);
- charsToSubstitutionCost = charsToSubstitutionCost ?? ((a, b) => (a === b ? 0 : 1));
- this._characterToIdx = new Map();
- this._insertionCosts = new Array(characters.length);
- this._deletionCosts = new Array(characters.length);
- this._substitutionCosts = new Array(characters.length);
- let c;
- for (let outerIdx = 0; outerIdx < characters.length; ++outerIdx) {
- c = characters[outerIdx];
- this._characterToIdx.set(c, outerIdx);
- this._insertionCosts[outerIdx] = charToInsertionCost(c);
- this._deletionCosts[outerIdx] = charToDeletionCost(c);
- this._substitutionCosts[outerIdx] = new Array(characters.length);
- for (let innerIdx = outerIdx; innerIdx < characters.length; ++innerIdx) {
- this._substitutionCosts[outerIdx][innerIdx] = charsToSubstitutionCost(c, characters[innerIdx]);
- }
- }
- }
- /**
- * Get the index (internally-assigned number) for a character.
- * @param char character
- * @returns index
- */
- getCharacterIdx(char) {
- return this._characterToIdx.get(char);
- }
- /**
- * Get the insertion cost of a character from its index.
- * @param idx character index
- * @returns insertion cost
- */
- getInsertionCost(idx) {
- return this._insertionCosts[idx];
- }
- /**
- * Get the deletion cost of a character from its index.
- * @param idx character index
- * @returns deletion cost
- */
- getDeletionCost(idx) {
- return this._deletionCosts[idx];
- }
- /**
- * Gets the cost to substitute two characters. NOTE: this cost is
- * required to be bi-directional, meaning it cannot matter which of
- * the provided characters is being removed and which is being inserted.
- * @param idx1 the first character index
- * @param idx2 the second character index
- * @returns substitution cost
- */
- getSubstitutionCost(idx1, idx2) {
- const min = Math.min(idx1, idx2);
- const max = Math.max(idx1, idx2);
- return this._substitutionCosts[min][max];
- }
- }
- Levenshtein.Alphabet = Alphabet;
- /**
- * Character sequence intended to be compared against other Sequences created
- * with the same Alphabet in order to compute Levenshtein distance.
- */
- class Sequence {
- /**
- * Serialize to JSON string. JSON representation does NOT include the Alphabet
- * from which this Sequence was created; Alphabet must be independently
- * serialized.
- * @returns JSON string
- */
- serialize() {
- return JSON.stringify(this._characters);
- }
- /**
- * Deserialize from JSON string and Alphabet. This should be the same Alphabet
- * from which the Sequence was originally created, which must be serialized and
- * deserialized independently so that it can be passed in here.
- * @param json JSON string representation of Sequence
- * @param alphabet Alphabet from which Sequence was originally created
- * @returns Sequence
- */
- static Deserialize(json, alphabet) {
- const sequence = new Sequence([], alphabet);
- sequence._characters = JSON.parse(json);
- return sequence;
- }
- /**
- * Create a new Sequence.
- * @param characters characters in the new Sequence
- * @param alphabet Alphabet, which must include all used characters
- */
- constructor(characters, alphabet) {
- if (characters.length > Sequence._MAX_SEQUENCE_LENGTH) {
- throw new Error("Sequences longer than " + Sequence._MAX_SEQUENCE_LENGTH + " not supported.");
- }
- this._alphabet = alphabet;
- this._characters = characters.map((c) => this._alphabet.getCharacterIdx(c));
- }
- /**
- * Get the distance between this Sequence and another.
- * @param other sequence to compare to
- * @returns Levenshtein distance
- */
- distance(other) {
- return Sequence._Distance(this, other);
- }
- /**
- * Compute the Levenshtein distance between two Sequences.
- * @param a first Sequence
- * @param b second Sequence
- * @returns Levenshtein distance
- */
- static _Distance(a, b) {
- const alphabet = a._alphabet;
- if (alphabet !== b._alphabet) {
- throw new Error("Cannot Levenshtein compare Sequences built from different alphabets.");
- }
- const aChars = a._characters;
- const bChars = b._characters;
- const aLength = aChars.length;
- const bLength = bChars.length;
- const costMatrix = Sequence._CostMatrix;
- costMatrix[0][0] = 0;
- for (let idx = 0; idx < aLength; ++idx) {
- costMatrix[idx + 1][0] = costMatrix[idx][0] + alphabet.getInsertionCost(aChars[idx]);
- }
- for (let idx = 0; idx < bLength; ++idx) {
- costMatrix[0][idx + 1] = costMatrix[0][idx] + alphabet.getInsertionCost(bChars[idx]);
- }
- for (let aIdx = 0; aIdx < aLength; ++aIdx) {
- for (let bIdx = 0; bIdx < bLength; ++bIdx) {
- Sequence._InsertionCost = costMatrix[aIdx + 1][bIdx] + alphabet.getInsertionCost(bChars[bIdx]);
- Sequence._DeletionCost = costMatrix[aIdx][bIdx + 1] + alphabet.getDeletionCost(aChars[aIdx]);
- Sequence._SubstitutionCost = costMatrix[aIdx][bIdx] + alphabet.getSubstitutionCost(aChars[aIdx], bChars[bIdx]);
- costMatrix[aIdx + 1][bIdx + 1] = Math.min(Sequence._InsertionCost, Sequence._DeletionCost, Sequence._SubstitutionCost);
- }
- }
- return costMatrix[aLength][bLength];
- }
- }
- // Scratch values
- Sequence._MAX_SEQUENCE_LENGTH = 256;
- Sequence._CostMatrix = [...Array(Sequence._MAX_SEQUENCE_LENGTH + 1)].map(() => new Array(Sequence._MAX_SEQUENCE_LENGTH + 1));
- Levenshtein.Sequence = Sequence;
- })(Levenshtein || (Levenshtein = {}));
- /**
- * A 3D trajectory consisting of an order list of vectors describing a
- * path of motion through 3D space.
- */
- export class Trajectory {
- /**
- * Serialize to JSON.
- * @returns serialized JSON string
- */
- serialize() {
- return JSON.stringify(this);
- }
- /**
- * Deserialize from JSON.
- * @param json serialized JSON string
- * @returns deserialized Trajectory
- */
- static Deserialize(json) {
- const jsonObject = JSON.parse(json);
- const trajectory = new Trajectory(jsonObject["_segmentLength"]);
- trajectory._points = jsonObject["_points"].map((pt) => {
- return new Vector3(pt["_x"], pt["_y"], pt["_z"]);
- });
- return trajectory;
- }
- /**
- * Create a new empty Trajectory.
- * @param segmentLength radius of discretization for Trajectory points
- */
- constructor(segmentLength = 0.01) {
- this._points = [];
- this._segmentLength = segmentLength;
- }
- /**
- * Get the length of the Trajectory.
- * @returns length of the Trajectory
- */
- getLength() {
- return this._points.length * this._segmentLength;
- }
- /**
- * Append a new point to the Trajectory.
- * NOTE: This implementation has many allocations.
- * @param point point to append to the Trajectory
- */
- add(point) {
- let numPoints = this._points.length;
- if (numPoints === 0) {
- this._points.push(point.clone());
- }
- else {
- const getT = () => this._segmentLength / Vector3.Distance(this._points[numPoints - 1], point);
- for (let t = getT(); t <= 1.0; t = getT()) {
- const newPoint = this._points[numPoints - 1].scale(1.0 - t);
- point.scaleAndAddToRef(t, newPoint);
- this._points.push(newPoint);
- ++numPoints;
- }
- }
- }
- /**
- * Create a new Trajectory with a segment length chosen to make it
- * probable that the new Trajectory will have a specified number of
- * segments. This operation is imprecise.
- * @param targetResolution number of segments desired
- * @returns new Trajectory with approximately the requested number of segments
- */
- resampleAtTargetResolution(targetResolution) {
- const resampled = new Trajectory(this.getLength() / targetResolution);
- this._points.forEach((pt) => {
- resampled.add(pt);
- });
- return resampled;
- }
- /**
- * Convert Trajectory segments into tokenized representation. This
- * representation is an array of numbers where each nth number is the
- * index of the token which is most similar to the nth segment of the
- * Trajectory.
- * @param tokens list of vectors which serve as discrete tokens
- * @returns list of indices of most similar token per segment
- */
- tokenize(tokens) {
- const tokenization = [];
- const segmentDir = new Vector3();
- for (let idx = 2; idx < this._points.length; ++idx) {
- if (Trajectory._TransformSegmentDirToRef(this._points[idx - 2], this._points[idx - 1], this._points[idx], segmentDir)) {
- tokenization.push(Trajectory._TokenizeSegment(segmentDir, tokens));
- }
- }
- return tokenization;
- }
- /**
- * Transform the rotation (i.e., direction) of a segment to isolate
- * the relative transformation represented by the segment. This operation
- * may or may not succeed due to singularities in the equations that define
- * motion relativity in this context.
- * @param priorVec the origin of the prior segment
- * @param fromVec the origin of the current segment
- * @param toVec the destination of the current segment
- * @param result reference to output variable
- * @returns whether or not transformation was successful
- */
- static _TransformSegmentDirToRef(priorVec, fromVec, toVec, result) {
- const DOT_PRODUCT_SAMPLE_REJECTION_THRESHOLD = 0.98;
- fromVec.subtractToRef(priorVec, Trajectory._ForwardDir);
- Trajectory._ForwardDir.normalize();
- fromVec.scaleToRef(-1, Trajectory._InverseFromVec);
- Trajectory._InverseFromVec.normalize();
- if (Math.abs(Vector3.Dot(Trajectory._ForwardDir, Trajectory._InverseFromVec)) > DOT_PRODUCT_SAMPLE_REJECTION_THRESHOLD) {
- return false;
- }
- Vector3.CrossToRef(Trajectory._ForwardDir, Trajectory._InverseFromVec, Trajectory._UpDir);
- Trajectory._UpDir.normalize();
- Matrix.LookAtLHToRef(priorVec, fromVec, Trajectory._UpDir, Trajectory._LookMatrix);
- toVec.subtractToRef(fromVec, Trajectory._FromToVec);
- Trajectory._FromToVec.normalize();
- Vector3.TransformNormalToRef(Trajectory._FromToVec, Trajectory._LookMatrix, result);
- return true;
- }
- /**
- * Determine which token vector is most similar to the
- * segment vector.
- * @param segment segment vector
- * @param tokens token vector list
- * @returns index of the most similar token to the segment
- */
- static _TokenizeSegment(segment, tokens) {
- Trajectory._BestMatch = 0;
- Trajectory._Score = Vector3.Dot(segment, tokens[0]);
- Trajectory._BestScore = Trajectory._Score;
- for (let idx = 1; idx < tokens.length; ++idx) {
- Trajectory._Score = Vector3.Dot(segment, tokens[idx]);
- if (Trajectory._Score > Trajectory._BestScore) {
- Trajectory._BestMatch = idx;
- Trajectory._BestScore = Trajectory._Score;
- }
- }
- return Trajectory._BestMatch;
- }
- }
- Trajectory._ForwardDir = new Vector3();
- Trajectory._InverseFromVec = new Vector3();
- Trajectory._UpDir = new Vector3();
- Trajectory._FromToVec = new Vector3();
- Trajectory._LookMatrix = new Matrix();
- /**
- * Collection of vectors intended to be used as the basis of Trajectory
- * tokenization for Levenshtein distance comparison. Canonically, a
- * Vector3Alphabet will resemble a "spikeball" of vectors distributed
- * roughly evenly over the surface of the unit sphere.
- */
- class Vector3Alphabet {
- /**
- * Helper method to create new "spikeball" Vector3Alphabets. Uses a naive
- * optimize-from-random strategy to space points around the unit sphere
- * surface as a simple alternative to really doing the math to tile the
- * sphere.
- * @param alphabetSize size of the desired alphabet
- * @param iterations number of iterations over which to optimize the "spikeball"
- * @param startingStepSize distance factor to move points in early optimization iterations
- * @param endingStepSize distance factor to move points in late optimization iterations
- * @param fixedValues alphabet "characters" that are required and cannot be moved by optimization
- * @returns a new randomly generated and optimized Vector3Alphabet of the specified size
- */
- static Generate(alphabetSize = 64, iterations = 256, startingStepSize = 0.1, endingStepSize = 0.001, fixedValues = []) {
- const EPSILON = 0.001;
- const EPSILON_SQUARED = EPSILON * EPSILON;
- const alphabet = new Vector3Alphabet(alphabetSize);
- for (let idx = 0; idx < alphabetSize; ++idx) {
- alphabet.chars[idx] = new Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5);
- alphabet.chars[idx].normalize();
- }
- for (let idx = 0; idx < fixedValues.length; ++idx) {
- alphabet.chars[idx].copyFrom(fixedValues[idx]);
- }
- let stepSize;
- let distSq;
- const force = new Vector3();
- const scratch = new Vector3();
- const lerp = (l, r, t) => (1.0 - t) * l + t * r;
- for (let iteration = 0; iteration < iterations; ++iteration) {
- stepSize = lerp(startingStepSize, endingStepSize, iteration / (iterations - 1));
- for (let idx = fixedValues.length; idx < alphabet.chars.length; ++idx) {
- force.copyFromFloats(0, 0, 0);
- alphabet.chars.forEach((pt) => {
- alphabet.chars[idx].subtractToRef(pt, scratch);
- distSq = scratch.lengthSquared();
- if (distSq > EPSILON_SQUARED) {
- scratch.scaleAndAddToRef(1 / (scratch.lengthSquared() * distSq), force);
- }
- });
- force.scaleInPlace(stepSize);
- alphabet.chars[idx].addInPlace(force);
- alphabet.chars[idx].normalize();
- }
- }
- return alphabet;
- }
- /**
- * Serialize to JSON.
- * @returns JSON serialization
- */
- serialize() {
- return JSON.stringify(this.chars);
- }
- /**
- * Deserialize from JSON.
- * @param json JSON serialization
- * @returns deserialized Vector3Alphabet
- */
- static Deserialize(json) {
- const jsonObject = JSON.parse(json);
- const alphabet = new Vector3Alphabet(jsonObject.length);
- for (let idx = 0; idx < jsonObject.length; ++idx) {
- alphabet.chars[idx] = new Vector3(jsonObject[idx]["_x"], jsonObject[idx]["_y"], jsonObject[idx]["_z"]);
- }
- return alphabet;
- }
- constructor(size) {
- this.chars = new Array(size);
- }
- }
- /**
- * Class which formalizes the manner in which a Vector3Alphabet is used to tokenize and
- * describe a Trajectory. This class houses the functionality which determines what
- * attributes of Trajectories are and are not considered important, such as scale.
- */
- class TrajectoryDescriptor {
- /**
- * Serialize to JSON.
- * @returns JSON serialization
- */
- serialize() {
- return JSON.stringify(this._sequences.map((sequence) => sequence.serialize()));
- }
- /**
- * Deserialize from JSON string and Alphabet. This should be the same Alphabet
- * from which the descriptor was originally created, which must be serialized and
- * deserialized independently so that it can be passed in here.
- * @param json JSON serialization
- * @param alphabet Alphabet from which descriptor was originally created
- * @returns deserialized TrajectoryDescriptor
- */
- static Deserialize(json, alphabet) {
- const descriptor = new TrajectoryDescriptor();
- descriptor._sequences = JSON.parse(json).map((s) => Levenshtein.Sequence.Deserialize(s, alphabet));
- return descriptor;
- }
- /**
- * Create a new TrajectoryDescriptor to describe a provided Trajectory according
- * to the provided alphabets.
- * @param trajectory Trajectory to be described
- * @param vector3Alphabet Vector3Alphabet to be used to tokenize the Trajectory
- * @param levenshteinAlphabet Levenshtein.Alphabet to be used as basis for comparison with other descriptors
- * @returns TrajectoryDescriptor describing provided Trajectory
- */
- static CreateFromTrajectory(trajectory, vector3Alphabet, levenshteinAlphabet) {
- return TrajectoryDescriptor.CreateFromTokenizationPyramid(TrajectoryDescriptor._GetTokenizationPyramid(trajectory, vector3Alphabet), levenshteinAlphabet);
- }
- /**
- * Create a new TrajectoryDescriptor from a pre-existing pyramid of tokens.
- * NOTE: This function exists to support an outdated serialization mechanism and should
- * be deleted if it is no longer useful.
- * @param pyramid tokenization pyramid
- * @param levenshteinAlphabet Levenshtein.Alphabet to be uses as basis for comparison with other descriptors
- * @returns TrajectoryDescriptor describing the Trajectory from which the pyramid was built
- */
- static CreateFromTokenizationPyramid(pyramid, levenshteinAlphabet) {
- const descriptor = new TrajectoryDescriptor();
- descriptor._sequences = pyramid.map((tokens) => new Levenshtein.Sequence(tokens, levenshteinAlphabet));
- return descriptor;
- }
- constructor() {
- this._sequences = [];
- }
- /**
- * Create the tokenization pyramid for the provided Trajectory according to the given
- * Vector3Alphabet.
- * @param trajectory Trajectory to be tokenized
- * @param alphabet Vector3Alphabet containing tokens
- * @param targetResolution finest resolution of descriptor
- * @returns tokenization pyramid for Trajectory
- */
- static _GetTokenizationPyramid(trajectory, alphabet, targetResolution = TrajectoryDescriptor._FINEST_DESCRIPTOR_RESOLUTION) {
- const pyramid = [];
- for (let res = targetResolution; res > 4; res = Math.floor(res / 2)) {
- pyramid.push(trajectory.resampleAtTargetResolution(res).tokenize(alphabet.chars));
- }
- return pyramid;
- }
- /**
- * Calculate a distance metric between this TrajectoryDescriptor and another. This is
- * essentially a similarity score and does not directly represent Euclidean distance,
- * edit distance, or any other formal distance metric.
- * @param other TrajectoryDescriptor from which to determine distance
- * @returns distance, a nonnegative similarity score where larger values indicate dissimilarity
- */
- distance(other) {
- let totalDistance = 0;
- let weight;
- for (let idx = 0; idx < this._sequences.length; ++idx) {
- weight = Math.pow(2, idx);
- totalDistance += weight * this._sequences[idx].distance(other._sequences[idx]);
- }
- return totalDistance;
- }
- }
- TrajectoryDescriptor._FINEST_DESCRIPTOR_RESOLUTION = 32;
- /**
- * A set of TrajectoryDescriptors defined to be "the same." This is essentially a helper
- * class to facilitate methods of Trajectory clustering.
- */
- class TrajectoryClass {
- /**
- * Serialize to JSON.
- * @returns JSON serialization
- */
- serialize() {
- const jsonObject = {};
- jsonObject.descriptors = this._descriptors.map((desc) => desc.serialize());
- jsonObject.centroidIdx = this._centroidIdx;
- jsonObject.averageDistance = this._averageDistance;
- return JSON.stringify(jsonObject);
- }
- /**
- * Deserialize from JSON string and Alphabet. This should be the same Alphabet
- * from which the descriptors were originally created, which must be serialized and
- * deserialized independently so that it can be passed in here.
- * @param json JSON string representation
- * @param alphabet Alphabet from which TrajectoryDescriptors were originally created
- * @returns deserialized TrajectoryDescriptor
- */
- static Deserialize(json, alphabet) {
- const jsonObject = JSON.parse(json);
- const described = new TrajectoryClass();
- described._descriptors = jsonObject.descriptors.map((s) => TrajectoryDescriptor.Deserialize(s, alphabet));
- described._centroidIdx = jsonObject.centroidIdx;
- described._averageDistance = jsonObject.averageDistance;
- return described;
- }
- /**
- * Create a new DescribedTrajectory.
- * @param descriptors currently-known TrajectoryDescriptors, if any
- */
- constructor(descriptors = []) {
- this._descriptors = descriptors;
- this._centroidIdx = -1;
- this._averageDistance = 0;
- this._refreshDescription();
- }
- /**
- * Add a new TrajectoryDescriptor to the list of descriptors known to describe
- * this same DescribedTrajectory.
- * @param descriptor descriptor to be added
- */
- add(descriptor) {
- this._descriptors.push(descriptor);
- this._refreshDescription();
- }
- /**
- * Compute the cost, which is inversely related to the likelihood that the provided
- * TrajectoryDescriptor describes a Trajectory that is considered to be the same as
- * the class represented by this DescribedTrajectory.
- * @param descriptor the descriptor to be costed
- * @returns cost of the match, which is a nonnegative similarity metric where larger values indicate dissimilarity
- */
- getMatchCost(descriptor) {
- return descriptor.distance(this._descriptors[this._centroidIdx]) / this._averageDistance;
- }
- /**
- * Compute the minimum distance between the queried TrajectoryDescriptor and a
- * descriptor which is a member of this collection. This is an alternative way of
- * conceptualizing match cost from getMatchCost(), and it serves a different function.
- * @param descriptor the descriptor to find the minimum distance to
- * @returns minimum descriptor distance to a member descriptor of this DescribedTrajectory
- */
- getMatchMinimumDistance(descriptor) {
- return Math.min(...this._descriptors.map((desc) => desc.distance(descriptor)));
- }
- /**
- * Refreshes the internal representation of this DescribedTrajectory.
- */
- _refreshDescription() {
- this._centroidIdx = -1;
- let sum;
- const distances = this._descriptors.map((a) => {
- sum = 0;
- this._descriptors.forEach((b) => {
- sum += a.distance(b);
- });
- return sum;
- });
- for (let idx = 0; idx < distances.length; ++idx) {
- if (this._centroidIdx < 0 || distances[idx] < distances[this._centroidIdx]) {
- this._centroidIdx = idx;
- }
- }
- this._averageDistance = 0;
- this._descriptors.forEach((desc) => {
- this._averageDistance += desc.distance(this._descriptors[this._centroidIdx]);
- });
- if (this._descriptors.length > 0) {
- this._averageDistance = Math.max(this._averageDistance / this._descriptors.length, TrajectoryClass._MIN_AVERAGE_DISTANCE);
- }
- }
- }
- TrajectoryClass._MIN_AVERAGE_DISTANCE = 1;
- /**
- * Class representing a set of known, named trajectories to which Trajectories can be
- * added and using which Trajectories can be recognized.
- */
- export class TrajectoryClassifier {
- /**
- * Serialize to JSON.
- * @returns JSON serialization
- */
- serialize() {
- const jsonObject = {};
- jsonObject.maximumAllowableMatchCost = this._maximumAllowableMatchCost;
- jsonObject.vector3Alphabet = this._vector3Alphabet.serialize();
- jsonObject.levenshteinAlphabet = this._levenshteinAlphabet.serialize();
- jsonObject.nameToDescribedTrajectory = [];
- this._nameToDescribedTrajectory.forEach((described, name) => {
- jsonObject.nameToDescribedTrajectory.push(name);
- jsonObject.nameToDescribedTrajectory.push(described.serialize());
- });
- return JSON.stringify(jsonObject);
- }
- /**
- * Deserialize from JSON.
- * @param json JSON serialization
- * @returns deserialized TrajectorySet
- */
- static Deserialize(json) {
- const jsonObject = JSON.parse(json);
- const classifier = new TrajectoryClassifier();
- classifier._maximumAllowableMatchCost = jsonObject.maximumAllowableMatchCost;
- classifier._vector3Alphabet = Vector3Alphabet.Deserialize(jsonObject.vector3Alphabet);
- classifier._levenshteinAlphabet = Levenshtein.Alphabet.Deserialize(jsonObject.levenshteinAlphabet);
- for (let idx = 0; idx < jsonObject.nameToDescribedTrajectory.length; idx += 2) {
- classifier._nameToDescribedTrajectory.set(jsonObject.nameToDescribedTrajectory[idx], TrajectoryClass.Deserialize(jsonObject.nameToDescribedTrajectory[idx + 1], classifier._levenshteinAlphabet));
- }
- return classifier;
- }
- /**
- * Initialize a new empty TrajectorySet with auto-generated Alphabets.
- * VERY naive, need to be generating these things from known
- * sets. Better version later, probably eliminating this one.
- * @returns auto-generated TrajectorySet
- */
- static Generate() {
- const vecs = Vector3Alphabet.Generate(64, 256, 0.1, 0.001, [Vector3.Forward()]);
- const charIdxs = new Array(vecs.chars.length);
- for (let idx = 0; idx < charIdxs.length; ++idx) {
- charIdxs[idx] = idx;
- }
- const alphabet = new Levenshtein.Alphabet(charIdxs, (idx) => (idx === 0 ? 0 : 1), (idx) => (idx === 0 ? 0 : 1), (a, b) => Math.min(1 - Vector3.Dot(vecs.chars[a], vecs.chars[b]), 1));
- const trajectorySet = new TrajectoryClassifier();
- trajectorySet._vector3Alphabet = vecs;
- trajectorySet._levenshteinAlphabet = alphabet;
- return trajectorySet;
- }
- constructor() {
- this._maximumAllowableMatchCost = 4;
- this._nameToDescribedTrajectory = new Map();
- }
- /**
- * Add a new Trajectory to the set with a given name.
- * @param trajectory new Trajectory to be added
- * @param classification name to which to add the Trajectory
- */
- addTrajectoryToClassification(trajectory, classification) {
- if (!this._nameToDescribedTrajectory.has(classification)) {
- this._nameToDescribedTrajectory.set(classification, new TrajectoryClass());
- }
- this._nameToDescribedTrajectory.get(classification).add(TrajectoryDescriptor.CreateFromTrajectory(trajectory, this._vector3Alphabet, this._levenshteinAlphabet));
- }
- /**
- * Remove a known named trajectory and all Trajectories associated with it.
- * @param classification name to remove
- * @returns whether anything was removed
- */
- deleteClassification(classification) {
- return this._nameToDescribedTrajectory.delete(classification);
- }
- /**
- * Attempt to recognize a Trajectory from among all the classifications
- * already known to the classifier.
- * @param trajectory Trajectory to be recognized
- * @returns classification of Trajectory if recognized, null otherwise
- */
- classifyTrajectory(trajectory) {
- const descriptor = TrajectoryDescriptor.CreateFromTrajectory(trajectory, this._vector3Alphabet, this._levenshteinAlphabet);
- const allowableMatches = [];
- this._nameToDescribedTrajectory.forEach((trajectoryClass, classification) => {
- if (trajectoryClass.getMatchCost(descriptor) < this._maximumAllowableMatchCost) {
- allowableMatches.push(classification);
- }
- });
- if (allowableMatches.length === 0) {
- return null;
- }
- let bestIdx = 0;
- let bestMatch = this._nameToDescribedTrajectory.get(allowableMatches[bestIdx]).getMatchMinimumDistance(descriptor);
- let match;
- for (let idx = 0; idx < allowableMatches.length; ++idx) {
- match = this._nameToDescribedTrajectory.get(allowableMatches[idx]).getMatchMinimumDistance(descriptor);
- if (match < bestMatch) {
- bestMatch = match;
- bestIdx = idx;
- }
- }
- return allowableMatches[bestIdx];
- }
- }
- //# sourceMappingURL=trajectoryClassifier.js.map
|