You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2181 lines
78 KiB
TypeScript

// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const ObjectState = require('./object-state');
const { checkObjectType } = require('./common');
const {
colors, Source, ObjectShape, ObjectType, AttributeType, HistoryActions,
} = require('./enums');
const { DataError, ArgumentError, ScriptingError } = require('./exceptions');
const { Label } = require('./labels');
const defaultGroupColor = '#E0E0E0';
// Called with the Annotation context
function objectStateFactory(frame, data) {
const objectState = new ObjectState(data);
// eslint-disable-next-line no-underscore-dangle
objectState.__internal = {
save: this.save.bind(this, frame, objectState),
delete: this.delete.bind(this),
context: this,
};
return objectState;
}
function checkNumberOfPoints(shapeType, points) {
if (shapeType === ObjectShape.RECTANGLE) {
if (points.length / 2 !== 2) {
throw new DataError(`Rectangle must have 2 points, but got ${points.length / 2}`);
}
} else if (shapeType === ObjectShape.POLYGON) {
if (points.length / 2 < 3) {
throw new DataError(`Polygon must have at least 3 points, but got ${points.length / 2}`);
}
} else if (shapeType === ObjectShape.POLYLINE) {
if (points.length / 2 < 2) {
throw new DataError(`Polyline must have at least 2 points, but got ${points.length / 2}`);
}
} else if (shapeType === ObjectShape.POINTS) {
if (points.length / 2 < 1) {
throw new DataError(`Points must have at least 1 points, but got ${points.length / 2}`);
}
} else if (shapeType === ObjectShape.CUBOID) {
if (points.length / 2 !== 8) {
throw new DataError(`Cuboid must have 8 points, but got ${points.length / 2}`);
}
} else if (shapeType === ObjectShape.ELLIPSE) {
if (points.length / 2 !== 2) {
throw new DataError(`Ellipse must have 1 point, rx and ry but got ${points.toString()}`);
}
} else {
throw new ArgumentError(`Unknown value of shapeType has been received ${shapeType}`);
}
}
function findAngleDiff(rightAngle, leftAngle) {
let angleDiff = rightAngle - leftAngle;
angleDiff = ((angleDiff + 180) % 360) - 180;
if (Math.abs(angleDiff) >= 180) {
// if the main arc is bigger than 180, go another arc
// to find it, just substract absolute value from 360 and inverse sign
angleDiff = 360 - Math.abs(angleDiff) * Math.sign(angleDiff) * -1;
}
return angleDiff;
}
function checkShapeArea(shapeType, points) {
const MIN_SHAPE_LENGTH = 3;
const MIN_SHAPE_AREA = 9;
if (shapeType === ObjectShape.POINTS) {
return true;
}
if (shapeType === ObjectShape.ELLIPSE) {
const [cx, cy, rightX, topY] = points;
const [rx, ry] = [rightX - cx, cy - topY];
return rx * ry * Math.PI > MIN_SHAPE_AREA;
}
let xmin = Number.MAX_SAFE_INTEGER;
let xmax = Number.MIN_SAFE_INTEGER;
let ymin = Number.MAX_SAFE_INTEGER;
let ymax = Number.MIN_SAFE_INTEGER;
for (let i = 0; i < points.length - 1; i += 2) {
xmin = Math.min(xmin, points[i]);
xmax = Math.max(xmax, points[i]);
ymin = Math.min(ymin, points[i + 1]);
ymax = Math.max(ymax, points[i + 1]);
}
if (shapeType === ObjectShape.POLYLINE) {
const length = Math.max(xmax - xmin, ymax - ymin);
return length >= MIN_SHAPE_LENGTH;
}
const area = (xmax - xmin) * (ymax - ymin);
return area >= MIN_SHAPE_AREA;
}
function rotatePoint(x, y, angle, cx = 0, cy = 0) {
const sin = Math.sin((angle * Math.PI) / 180);
const cos = Math.cos((angle * Math.PI) / 180);
const rotX = (x - cx) * cos - (y - cy) * sin + cx;
const rotY = (y - cy) * cos + (x - cx) * sin + cy;
return [rotX, rotY];
}
function fitPoints(shapeType, points, rotation, maxX, maxY) {
checkObjectType('rotation', rotation, 'number', null);
points.forEach((coordinate) => checkObjectType('coordinate', coordinate, 'number', null));
if (shapeType === ObjectShape.CUBOID || shapeType === ObjectShape.ELLIPSE || !!rotation) {
// cuboids and rotated bounding boxes cannot be fitted
return points;
}
const fittedPoints = [];
for (let i = 0; i < points.length - 1; i += 2) {
const x = points[i];
const y = points[i + 1];
const clampedX = Math.clamp(x, 0, maxX);
const clampedY = Math.clamp(y, 0, maxY);
fittedPoints.push(clampedX, clampedY);
}
return fittedPoints;
}
function validateAttributeValue(value, attr) {
const { values } = attr;
const type = attr.inputType;
if (typeof value !== 'string') {
throw new ArgumentError(`Attribute value is expected to be string, but got ${typeof value}`);
}
if (type === AttributeType.NUMBER) {
return +value >= +values[0] && +value <= +values[1];
}
if (type === AttributeType.CHECKBOX) {
return ['true', 'false'].includes(value.toLowerCase());
}
if (type === AttributeType.TEXT) {
return true;
}
return values.includes(value);
}
class Annotation {
constructor(data, clientID, color, injection) {
this.taskLabels = injection.labels;
this.history = injection.history;
this.groupColors = injection.groupColors;
this.clientID = clientID;
this.serverID = data.id;
this.group = data.group;
this.label = this.taskLabels[data.label_id];
this.frame = data.frame;
this.removed = false;
this.lock = false;
this.color = color;
this.source = data.source;
this.updated = Date.now();
this.attributes = data.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value;
return attributeAccumulator;
}, {});
this.groupObject = Object.defineProperties(
{},
{
color: {
get: () => {
if (this.group) {
return this.groupColors[this.group] || colors[this.group % colors.length];
}
return defaultGroupColor;
},
set: (newColor) => {
if (this.group && typeof newColor === 'string' && /^#[0-9A-F]{6}$/i.test(newColor)) {
this.groupColors[this.group] = newColor;
}
},
},
id: {
get: () => this.group,
},
},
);
this.appendDefaultAttributes(this.label);
injection.groups.max = Math.max(injection.groups.max, this.group);
}
_saveLock(lock, frame) {
const undoLock = this.lock;
const redoLock = lock;
this.history.do(
HistoryActions.CHANGED_LOCK,
() => {
this.lock = undoLock;
this.updated = Date.now();
},
() => {
this.lock = redoLock;
this.updated = Date.now();
},
[this.clientID],
frame,
);
this.lock = lock;
}
_saveColor(color, frame) {
const undoColor = this.color;
const redoColor = color;
this.history.do(
HistoryActions.CHANGED_COLOR,
() => {
this.color = undoColor;
this.updated = Date.now();
},
() => {
this.color = redoColor;
this.updated = Date.now();
},
[this.clientID],
frame,
);
this.color = color;
}
_saveHidden(hidden, frame) {
const undoHidden = this.hidden;
const redoHidden = hidden;
this.history.do(
HistoryActions.CHANGED_HIDDEN,
() => {
this.hidden = undoHidden;
this.updated = Date.now();
},
() => {
this.hidden = redoHidden;
this.updated = Date.now();
},
[this.clientID],
frame,
);
this.hidden = hidden;
}
_saveLabel(label, frame) {
const undoLabel = this.label;
const redoLabel = label;
const undoAttributes = { ...this.attributes };
this.label = label;
this.attributes = {};
this.appendDefaultAttributes(label);
// Try to keep old attributes if name matches and old value is still valid
for (const attribute of redoLabel.attributes) {
for (const oldAttribute of undoLabel.attributes) {
if (
attribute.name === oldAttribute.name &&
validateAttributeValue(undoAttributes[oldAttribute.id], attribute)
) {
this.attributes[attribute.id] = undoAttributes[oldAttribute.id];
}
}
}
const redoAttributes = { ...this.attributes };
this.history.do(
HistoryActions.CHANGED_LABEL,
() => {
this.label = undoLabel;
this.attributes = undoAttributes;
this.updated = Date.now();
},
() => {
this.label = redoLabel;
this.attributes = redoAttributes;
this.updated = Date.now();
},
[this.clientID],
frame,
);
}
_saveAttributes(attributes, frame) {
const undoAttributes = { ...this.attributes };
for (const attrID of Object.keys(attributes)) {
this.attributes[attrID] = attributes[attrID];
}
const redoAttributes = { ...this.attributes };
this.history.do(
HistoryActions.CHANGED_ATTRIBUTES,
() => {
this.attributes = undoAttributes;
this.updated = Date.now();
},
() => {
this.attributes = redoAttributes;
this.updated = Date.now();
},
[this.clientID],
frame,
);
}
_validateStateBeforeSave(frame, data, updated) {
let fittedPoints = [];
if (updated.label) {
checkObjectType('label', data.label, null, Label);
}
const labelAttributes = data.label.attributes.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) {
const value = data.attributes[attrID];
if (attrID in labelAttributes) {
if (!validateAttributeValue(value, labelAttributes[attrID])) {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`The label of the shape doesn't have the attribute with id ${attrID} and value ${value}`,
);
}
}
}
if (updated.descriptions) {
if (!Array.isArray(data.descriptions) || data.descriptions.some((desc) => typeof desc !== 'string')) {
throw new ArgumentError(
`Descriptions are expected to be an array of strings but got ${data.descriptions}`,
);
}
}
if (updated.points) {
checkObjectType('points', data.points, null, Array);
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height, filename } = this.frameMeta[frame];
fittedPoints = fitPoints(this.shapeType, data.points, data.rotation, width, height);
let check = true;
if (filename && filename.slice(filename.length - 3) === 'pcd') {
check = false;
}
if (check) {
if (!checkShapeArea(this.shapeType, fittedPoints)) {
fittedPoints = [];
}
}
}
if (updated.occluded) {
checkObjectType('occluded', data.occluded, 'boolean', null);
}
if (updated.outside) {
checkObjectType('outside', data.outside, 'boolean', null);
}
if (updated.zOrder) {
checkObjectType('zOrder', data.zOrder, 'integer', null);
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
}
if (updated.pinned) {
checkObjectType('pinned', data.pinned, 'boolean', null);
}
if (updated.color) {
checkObjectType('color', data.color, 'string', null);
if (!/^#[0-9A-F]{6}$/i.test(data.color)) {
throw new ArgumentError(`Got invalid color value: "${data.color}"`);
}
}
if (updated.hidden) {
checkObjectType('hidden', data.hidden, 'boolean', null);
}
if (updated.keyframe) {
checkObjectType('keyframe', data.keyframe, 'boolean', null);
if (!this.shapes || (Object.keys(this.shapes).length === 1 && !data.keyframe)) {
throw new ArgumentError(
'Can not remove the latest keyframe of an object. Consider removing the object instead',
);
}
}
return fittedPoints;
}
appendDefaultAttributes(label) {
const labelAttributes = label.attributes;
for (const attribute of labelAttributes) {
if (!(attribute.id in this.attributes)) {
this.attributes[attribute.id] = attribute.defaultValue;
}
}
}
updateTimestamp(updated) {
const anyChanges = Object.keys(updated).some((key) => !!updated[key]);
if (anyChanges) {
this.updated = Date.now();
}
}
delete(frame, force) {
if (!this.lock || force) {
this.removed = true;
this.history.do(
HistoryActions.REMOVED_OBJECT,
() => {
this.serverID = undefined;
this.removed = false;
this.updated = Date.now();
},
() => {
this.removed = true;
this.updated = Date.now();
},
[this.clientID],
frame,
);
}
return this.removed;
}
}
class Drawn extends Annotation {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.frameMeta = injection.frameMeta;
this.descriptions = data.descriptions || [];
this.hidden = false;
this.pinned = true;
this.shapeType = null;
}
_saveDescriptions(descriptions) {
this.descriptions = [...descriptions];
}
_savePinned(pinned, frame) {
const undoPinned = this.pinned;
const redoPinned = pinned;
this.history.do(
HistoryActions.CHANGED_PINNED,
() => {
this.pinned = undoPinned;
this.updated = Date.now();
},
() => {
this.pinned = redoPinned;
this.updated = Date.now();
},
[this.clientID],
frame,
);
this.pinned = pinned;
}
save() {
throw new ScriptingError('Is not implemented');
}
get() {
throw new ScriptingError('Is not implemented');
}
toJSON() {
throw new ScriptingError('Is not implemented');
}
}
class Shape extends Drawn {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.points = data.points;
this.rotation = data.rotation || 0;
this.occluded = data.occluded;
this.zOrder = data.z_order;
}
// Method is used to export data to the server
toJSON() {
return {
type: this.shapeType,
clientID: this.clientID,
occluded: this.occluded,
z_order: this.zOrder,
points: [...this.points],
rotation: this.rotation,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({
spec_id: attrId,
value: this.attributes[attrId],
});
return attributeAccumulator;
}, []),
id: this.serverID,
frame: this.frame,
label_id: this.label.id,
group: this.group,
source: this.source,
};
}
// Method is used to construct ObjectState objects
get(frame) {
if (frame !== this.frame) {
throw new ScriptingError('Got frame is not equal to the frame of the shape');
}
return {
objectType: ObjectType.SHAPE,
shapeType: this.shapeType,
clientID: this.clientID,
serverID: this.serverID,
occluded: this.occluded,
lock: this.lock,
zOrder: this.zOrder,
points: [...this.points],
rotation: this.rotation,
attributes: { ...this.attributes },
descriptions: [...this.descriptions],
label: this.label,
group: this.groupObject,
color: this.color,
hidden: this.hidden,
updated: this.updated,
pinned: this.pinned,
frame,
source: this.source,
};
}
_savePoints(points, rotation, frame) {
const undoPoints = this.points;
const undoRotation = this.rotation;
const redoPoints = points;
const redoRotation = rotation;
const undoSource = this.source;
const redoSource = Source.MANUAL;
this.history.do(
HistoryActions.CHANGED_POINTS,
() => {
this.points = undoPoints;
this.source = undoSource;
this.rotation = undoRotation;
this.updated = Date.now();
},
() => {
this.points = redoPoints;
this.source = redoSource;
this.rotation = redoRotation;
this.updated = Date.now();
},
[this.clientID],
frame,
);
this.source = Source.MANUAL;
this.points = points;
this.rotation = rotation;
}
_saveOccluded(occluded, frame) {
const undoOccluded = this.occluded;
const redoOccluded = occluded;
const undoSource = this.source;
const redoSource = Source.MANUAL;
this.history.do(
HistoryActions.CHANGED_OCCLUDED,
() => {
this.occluded = undoOccluded;
this.source = undoSource;
this.updated = Date.now();
},
() => {
this.occluded = redoOccluded;
this.source = redoSource;
this.updated = Date.now();
},
[this.clientID],
frame,
);
this.source = Source.MANUAL;
this.occluded = occluded;
}
_saveZOrder(zOrder, frame) {
const undoZOrder = this.zOrder;
const redoZOrder = zOrder;
const undoSource = this.source;
const redoSource = Source.MANUAL;
this.history.do(
HistoryActions.CHANGED_ZORDER,
() => {
this.zOrder = undoZOrder;
this.source = undoSource;
this.updated = Date.now();
},
() => {
this.zOrder = redoZOrder;
this.source = redoSource;
this.updated = Date.now();
},
[this.clientID],
frame,
);
this.source = Source.MANUAL;
this.zOrder = zOrder;
}
save(frame, data) {
if (frame !== this.frame) {
throw new ScriptingError('Got frame is not equal to the frame of the shape');
}
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
}
const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
const { rotation } = data;
// Now when all fields are validated, we can apply them
if (updated.label) {
this._saveLabel(data.label, frame);
}
if (updated.attributes) {
this._saveAttributes(data.attributes, frame);
}
if (updated.descriptions) {
this._saveDescriptions(data.descriptions);
}
if (updated.points && fittedPoints.length) {
this._savePoints(fittedPoints, rotation, frame);
}
if (updated.occluded) {
this._saveOccluded(data.occluded, frame);
}
if (updated.zOrder) {
this._saveZOrder(data.zOrder, frame);
}
if (updated.lock) {
this._saveLock(data.lock, frame);
}
if (updated.pinned) {
this._savePinned(data.pinned, frame);
}
if (updated.color) {
this._saveColor(data.color, frame);
}
if (updated.hidden) {
this._saveHidden(data.hidden, frame);
}
this.updateTimestamp(updated);
updated.reset();
return objectStateFactory.call(this, frame, this.get(frame));
}
}
class Track extends Drawn {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapes = data.shapes.reduce((shapeAccumulator, value) => {
shapeAccumulator[value.frame] = {
serverID: value.id,
occluded: value.occluded,
zOrder: value.z_order,
points: value.points,
outside: value.outside,
rotation: value.rotation || 0,
attributes: value.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value;
return attributeAccumulator;
}, {}),
};
return shapeAccumulator;
}, {});
}
// Method is used to export data to the server
toJSON() {
const labelAttributes = this.label.attributes.reduce((accumulator, attribute) => {
accumulator[attribute.id] = attribute;
return accumulator;
}, {});
return {
clientID: this.clientID,
id: this.serverID,
frame: this.frame,
label_id: this.label.id,
group: this.group,
source: this.source,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
if (!labelAttributes[attrId].mutable) {
attributeAccumulator.push({
spec_id: attrId,
value: this.attributes[attrId],
});
}
return attributeAccumulator;
}, []),
shapes: Object.keys(this.shapes).reduce((shapesAccumulator, frame) => {
shapesAccumulator.push({
type: this.shapeType,
occluded: this.shapes[frame].occluded,
z_order: this.shapes[frame].zOrder,
points: [...this.shapes[frame].points],
rotation: this.shapes[frame].rotation,
outside: this.shapes[frame].outside,
attributes: Object.keys(this.shapes[frame].attributes).reduce(
(attributeAccumulator, attrId) => {
if (labelAttributes[attrId].mutable) {
attributeAccumulator.push({
spec_id: attrId,
value: this.shapes[frame].attributes[attrId],
});
}
return attributeAccumulator;
},
[],
),
id: this.shapes[frame].serverID,
frame: +frame,
});
return shapesAccumulator;
}, []),
};
}
// Method is used to construct ObjectState objects
get(frame) {
const {
prev, next, first, last,
} = this.boundedKeyframes(frame);
return {
...this.getPosition(frame, prev, next),
attributes: this.getAttributes(frame),
descriptions: [...this.descriptions],
group: this.groupObject,
objectType: ObjectType.TRACK,
shapeType: this.shapeType,
clientID: this.clientID,
serverID: this.serverID,
lock: this.lock,
color: this.color,
hidden: this.hidden,
updated: this.updated,
label: this.label,
pinned: this.pinned,
keyframes: {
prev,
next,
first,
last,
},
frame,
source: this.source,
};
}
boundedKeyframes(targetFrame) {
const frames = Object.keys(this.shapes).map((frame) => +frame);
let lDiff = Number.MAX_SAFE_INTEGER;
let rDiff = Number.MAX_SAFE_INTEGER;
let first = Number.MAX_SAFE_INTEGER;
let last = Number.MIN_SAFE_INTEGER;
for (const frame of frames) {
if (frame in this.frameMeta.deleted_frames) {
continue;
}
if (frame < first) {
first = frame;
}
if (frame > last) {
last = frame;
}
const diff = Math.abs(targetFrame - frame);
if (frame < targetFrame && diff < lDiff) {
lDiff = diff;
} else if (frame > targetFrame && diff < rDiff) {
rDiff = diff;
}
}
const prev = lDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame - lDiff;
const next = rDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame + rDiff;
return {
prev,
next,
first,
last,
};
}
getAttributes(targetFrame) {
const result = {};
// First of all copy all unmutable attributes
for (const attrID in this.attributes) {
if (Object.prototype.hasOwnProperty.call(this.attributes, attrID)) {
result[attrID] = this.attributes[attrID];
}
}
// Secondly get latest mutable attributes up to target frame
const frames = Object.keys(this.shapes).sort((a, b) => +a - +b);
for (const frame of frames) {
if (frame <= targetFrame) {
const { attributes } = this.shapes[frame];
for (const attrID in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, attrID)) {
result[attrID] = attributes[attrID];
}
}
}
}
return result;
}
_saveLabel(label, frame) {
const undoLabel = this.label;
const redoLabel = label;
const undoAttributes = {
unmutable: { ...this.attributes },
mutable: Object.keys(this.shapes).map((key) => ({
frame: +key,
attributes: { ...this.shapes[key].attributes },
})),
};
this.label = label;
this.attributes = {};
for (const shape of Object.values(this.shapes)) {
shape.attributes = {};
}
this.appendDefaultAttributes(label);
const redoAttributes = {
unmutable: { ...this.attributes },
mutable: Object.keys(this.shapes).map((key) => ({
frame: +key,
attributes: { ...this.shapes[key].attributes },
})),
};
this.history.do(
HistoryActions.CHANGED_LABEL,
() => {
this.label = undoLabel;
this.attributes = undoAttributes.unmutable;
for (const mutable of undoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes;
}
this.updated = Date.now();
},
() => {
this.label = redoLabel;
this.attributes = redoAttributes.unmutable;
for (const mutable of redoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes;
}
this.updated = Date.now();
},
[this.clientID],
frame,
);
}
_saveAttributes(attributes, frame) {
const current = this.get(frame);
const labelAttributes = this.label.attributes.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
const wasKeyframe = frame in this.shapes;
const undoAttributes = this.attributes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
let mutableAttributesUpdated = false;
const redoAttributes = { ...this.attributes };
for (const attrID of Object.keys(attributes)) {
if (!labelAttributes[attrID].mutable) {
redoAttributes[attrID] = attributes[attrID];
} else if (attributes[attrID] !== current.attributes[attrID]) {
mutableAttributesUpdated = mutableAttributesUpdated ||
// not keyframe yet
!(frame in this.shapes) ||
// keyframe, but without this attrID
!(attrID in this.shapes[frame].attributes) ||
// keyframe with attrID, but with another value
this.shapes[frame].attributes[attrID] !== attributes[attrID];
}
}
let redoShape;
if (mutableAttributesUpdated) {
if (wasKeyframe) {
redoShape = {
...this.shapes[frame],
attributes: {
...this.shapes[frame].attributes,
},
};
} else {
redoShape = {
frame,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
occluded: current.occluded,
attributes: {},
};
}
}
for (const attrID of Object.keys(attributes)) {
if (labelAttributes[attrID].mutable && attributes[attrID] !== current.attributes[attrID]) {
redoShape.attributes[attrID] = attributes[attrID];
}
}
this.attributes = redoAttributes;
if (redoShape) {
this.shapes[frame] = redoShape;
}
this.history.do(
HistoryActions.CHANGED_ATTRIBUTES,
() => {
this.attributes = undoAttributes;
if (undoShape) {
this.shapes[frame] = undoShape;
} else if (redoShape) {
delete this.shapes[frame];
}
this.updated = Date.now();
},
() => {
this.attributes = redoAttributes;
if (redoShape) {
this.shapes[frame] = redoShape;
}
this.updated = Date.now();
},
[this.clientID],
frame,
);
}
_appendShapeActionToHistory(actionType, frame, undoShape, redoShape, undoSource, redoSource) {
this.history.do(
actionType,
() => {
if (!undoShape) {
delete this.shapes[frame];
} else {
this.shapes[frame] = undoShape;
}
this.source = undoSource;
this.updated = Date.now();
},
() => {
if (!redoShape) {
delete this.shapes[frame];
} else {
this.shapes[frame] = redoShape;
}
this.source = redoSource;
this.updated = Date.now();
},
[this.clientID],
frame,
);
}
_savePoints(points, rotation, frame) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], points, rotation } : {
frame,
points,
rotation,
zOrder: current.zOrder,
outside: current.outside,
occluded: current.occluded,
attributes: {},
};
this.shapes[frame] = redoShape;
this.source = Source.MANUAL;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_POINTS,
frame,
undoShape,
redoShape,
undoSource,
redoSource,
);
}
_saveOutside(frame, outside) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ?
{ ...this.shapes[frame], outside } :
{
frame,
outside,
rotation: current.rotation,
zOrder: current.zOrder,
points: current.points,
occluded: current.occluded,
attributes: {},
};
this.shapes[frame] = redoShape;
this.source = Source.MANUAL;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_OUTSIDE,
frame,
undoShape,
redoShape,
undoSource,
redoSource,
);
}
_saveOccluded(occluded, frame) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ?
{ ...this.shapes[frame], occluded } :
{
frame,
occluded,
rotation: current.rotation,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
attributes: {},
};
this.shapes[frame] = redoShape;
this.source = Source.MANUAL;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_OCCLUDED,
frame,
undoShape,
redoShape,
undoSource,
redoSource,
);
}
_saveZOrder(zOrder, frame) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ?
{ ...this.shapes[frame], zOrder } :
{
frame,
zOrder,
rotation: current.rotation,
occluded: current.occluded,
points: current.points,
outside: current.outside,
attributes: {},
};
this.shapes[frame] = redoShape;
this.source = Source.MANUAL;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_ZORDER,
frame,
undoShape,
redoShape,
undoSource,
redoSource,
);
}
_saveKeyframe(frame, keyframe) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
if ((keyframe && wasKeyframe) || (!keyframe && !wasKeyframe)) {
return;
}
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = keyframe ?
{
frame,
rotation: current.rotation,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
occluded: current.occluded,
attributes: {},
source: current.source,
} :
undefined;
this.source = Source.MANUAL;
if (redoShape) {
this.shapes[frame] = redoShape;
} else {
delete this.shapes[frame];
}
this._appendShapeActionToHistory(
HistoryActions.CHANGED_KEYFRAME,
frame,
undoShape,
redoShape,
undoSource,
redoSource,
);
}
save(frame, data) {
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
}
const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
const { rotation } = data;
if (updated.label) {
this._saveLabel(data.label, frame);
}
if (updated.lock) {
this._saveLock(data.lock, frame);
}
if (updated.pinned) {
this._savePinned(data.pinned, frame);
}
if (updated.color) {
this._saveColor(data.color, frame);
}
if (updated.hidden) {
this._saveHidden(data.hidden, frame);
}
if (updated.points && fittedPoints.length) {
this._savePoints(fittedPoints, rotation, frame);
}
if (updated.outside) {
this._saveOutside(frame, data.outside);
}
if (updated.occluded) {
this._saveOccluded(data.occluded, frame);
}
if (updated.zOrder) {
this._saveZOrder(data.zOrder, frame);
}
if (updated.attributes) {
this._saveAttributes(data.attributes, frame);
}
if (updated.descriptions) {
this._saveDescriptions(data.descriptions);
}
if (updated.keyframe) {
this._saveKeyframe(frame, data.keyframe);
}
this.updateTimestamp(updated);
updated.reset();
return objectStateFactory.call(this, frame, this.get(frame));
}
getPosition(targetFrame, leftKeyframe, rightFrame) {
const leftFrame = targetFrame in this.shapes ? targetFrame : leftKeyframe;
const rightPosition = Number.isInteger(rightFrame) ? this.shapes[rightFrame] : null;
const leftPosition = Number.isInteger(leftFrame) ? this.shapes[leftFrame] : null;
if (leftPosition && rightPosition) {
return {
...this.interpolatePosition(
leftPosition,
rightPosition,
(targetFrame - leftFrame) / (rightFrame - leftFrame),
),
keyframe: targetFrame in this.shapes,
};
}
if (leftPosition) {
return {
points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
keyframe: targetFrame in this.shapes,
};
}
if (rightPosition) {
return {
points: [...rightPosition.points],
rotation: rightPosition.rotation,
occluded: rightPosition.occluded,
zOrder: rightPosition.zOrder,
keyframe: targetFrame in this.shapes,
outside: true,
};
}
throw new DataError(
'No one left position or right position was found. ' +
`Interpolation impossible. Client ID: ${this.clientID}`,
);
}
}
class Tag extends Annotation {
// Method is used to export data to the server
toJSON() {
return {
clientID: this.clientID,
id: this.serverID,
frame: this.frame,
label_id: this.label.id,
group: this.group,
source: this.source,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({
spec_id: attrId,
value: this.attributes[attrId],
});
return attributeAccumulator;
}, []),
};
}
// Method is used to construct ObjectState objects
get(frame) {
if (frame !== this.frame) {
throw new ScriptingError('Got frame is not equal to the frame of the shape');
}
return {
objectType: ObjectType.TAG,
clientID: this.clientID,
serverID: this.serverID,
lock: this.lock,
attributes: { ...this.attributes },
label: this.label,
group: this.groupObject,
color: this.color,
updated: this.updated,
frame,
source: this.source,
};
}
save(frame, data) {
if (frame !== this.frame) {
throw new ScriptingError('Got frame is not equal to the frame of the tag');
}
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
}
const updated = data.updateFlags;
this._validateStateBeforeSave(frame, data, updated);
// Now when all fields are validated, we can apply them
if (updated.label) {
this._saveLabel(data.label, frame);
}
if (updated.attributes) {
this._saveAttributes(data.attributes, frame);
}
if (updated.lock) {
this._saveLock(data.lock, frame);
}
if (updated.color) {
this._saveColor(data.color, frame);
}
this.updateTimestamp(updated);
updated.reset();
return objectStateFactory.call(this, frame, this.get(frame));
}
}
class RectangleShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.RECTANGLE;
this.pinned = false;
checkNumberOfPoints(this.shapeType, this.points);
}
static distance(points, x, y, angle) {
const [xtl, ytl, xbr, ybr] = points;
const cx = xtl + (xbr - xtl) / 2;
const cy = ytl + (ybr - ytl) / 2;
const [rotX, rotY] = rotatePoint(x, y, -angle, cx, cy);
if (!(rotX >= xtl && rotX <= xbr && rotY >= ytl && rotY <= ybr)) {
// Cursor is outside of a box
return null;
}
// The shortest distance from point to an edge
return Math.min.apply(null, [rotX - xtl, rotY - ytl, xbr - rotX, ybr - rotY]);
}
}
class EllipseShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.ELLIPSE;
this.pinned = false;
checkNumberOfPoints(this.shapeType, this.points);
}
static distance(points, x, y, angle) {
const [cx, cy, rightX, topY] = points;
const [rx, ry] = [rightX - cx, cy - topY];
const [rotX, rotY] = rotatePoint(x, y, -angle, cx, cy);
// https://math.stackexchange.com/questions/76457/check-if-a-point-is-within-an-ellipse
const pointWithinEllipse = (_x, _y) => (
((_x - cx) ** 2) / rx ** 2) + (((_y - cy) ** 2) / ry ** 2
) <= 1;
if (!pointWithinEllipse(rotX, rotY)) {
// Cursor is outside of an ellipse
return null;
}
if (Math.abs(x - cx) < Number.EPSILON && Math.abs(y - cy) < Number.EPSILON) {
// cursor is near to the center, just return minimum of height, width
return Math.min(rx, ry);
}
// ellipse equation is x^2/rx^2 + y^2/ry^2 = 1
// from this equation:
// x^2 = ((rx * ry)^2 - (y * rx)^2) / ry^2
// y^2 = ((rx * ry)^2 - (x * ry)^2) / rx^2
// we have one point inside the ellipse, let's build two lines (horizontal and vertical) through the point
// and find their interception with ellipse
const x2Equation = (_y) => (((rx * ry) ** 2) - ((_y * rx) ** 2)) / (ry ** 2);
const y2Equation = (_x) => (((rx * ry) ** 2) - ((_x * ry) ** 2)) / (rx ** 2);
// shift x,y to the ellipse coordinate system to compute equation correctly
// y axis is inverted
const [shiftedX, shiftedY] = [x - cx, cy - y];
const [x1, x2] = [Math.sqrt(x2Equation(shiftedY)), -Math.sqrt(x2Equation(shiftedY))];
const [y1, y2] = [Math.sqrt(y2Equation(shiftedX)), -Math.sqrt(y2Equation(shiftedX))];
// found two points on ellipse edge
const ellipseP1X = shiftedX >= 0 ? x1 : x2; // ellipseP1Y is shiftedY
const ellipseP2Y = shiftedY >= 0 ? y1 : y2; // ellipseP1X is shiftedX
// found diffs between two points on edges and target point
const diff1X = ellipseP1X - shiftedX;
const diff2Y = ellipseP2Y - shiftedY;
// return minimum, get absolute value because we need distance, not diff
return Math.min(Math.abs(diff1X), Math.abs(diff2Y));
}
}
class PolyShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.rotation = 0; // is not supported
}
}
class PolygonShape extends PolyShape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.POLYGON;
checkNumberOfPoints(this.shapeType, this.points);
}
static distance(points, x, y) {
function position(x1, y1, x2, y2) {
return (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1);
}
let wn = 0;
const distances = [];
for (let i = 0, j = points.length - 2; i < points.length - 1; j = i, i += 2) {
// Current point
const x1 = points[j];
const y1 = points[j + 1];
// Next point
const x2 = points[i];
const y2 = points[i + 1];
// Check if a point is inside a polygon
// with a winding numbers algorithm
// https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm
if (y1 <= y) {
if (y2 > y) {
if (position(x1, y1, x2, y2) > 0) {
wn++;
}
}
} else if (y2 <= y) {
if (position(x1, y1, x2, y2) < 0) {
wn--;
}
}
// Find the shortest distance from point to an edge
// Get an equation of a line in general
const aCoef = y1 - y2;
const bCoef = x2 - x1;
// Vector (aCoef, bCoef) is a perpendicular to line
// Now find the point where two lines
// (edge and its perpendicular through the point (x,y)) are cross
const xCross = x - aCoef;
const yCross = y - bCoef;
if ((xCross - x1) * (x2 - xCross) >= 0 && (yCross - y1) * (y2 - yCross) >= 0) {
// Cross point is on segment between p1(x1,y1) and p2(x2,y2)
distances.push(Math.sqrt((x - xCross) ** 2 + (y - yCross) ** 2));
} else {
distances.push(
Math.min(
Math.sqrt((x1 - x) ** 2 + (y1 - y) ** 2),
Math.sqrt((x2 - x) ** 2 + (y2 - y) ** 2),
),
);
}
}
if (wn !== 0) {
return Math.min.apply(null, distances);
}
return null;
}
}
class PolylineShape extends PolyShape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.POLYLINE;
checkNumberOfPoints(this.shapeType, this.points);
}
static distance(points, x, y) {
const distances = [];
for (let i = 0; i < points.length - 2; i += 2) {
// Current point
const x1 = points[i];
const y1 = points[i + 1];
// Next point
const x2 = points[i + 2];
const y2 = points[i + 3];
// Find the shortest distance from point to an edge
if ((x - x1) * (x2 - x) >= 0 && (y - y1) * (y2 - y) >= 0) {
// Find the length of a perpendicular
// https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
distances.push(
Math.abs((y2 - y1) * x - (x2 - x1) * y + x2 * y1 - y2 * x1) /
Math.sqrt((y2 - y1) ** 2 + (x2 - x1) ** 2),
);
} else {
// The link below works for lines (which have infinite length)
// There is a case when perpendicular doesn't cross the edge
// In this case we don't use the computed distance
// Instead we use just distance to the nearest point
distances.push(
Math.min(
Math.sqrt((x1 - x) ** 2 + (y1 - y) ** 2),
Math.sqrt((x2 - x) ** 2 + (y2 - y) ** 2),
),
);
}
}
return Math.min.apply(null, distances);
}
}
class PointsShape extends PolyShape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.POINTS;
checkNumberOfPoints(this.shapeType, this.points);
}
static distance(points, x, y) {
const distances = [];
for (let i = 0; i < points.length; i += 2) {
const x1 = points[i];
const y1 = points[i + 1];
distances.push(Math.sqrt((x1 - x) ** 2 + (y1 - y) ** 2));
}
return Math.min.apply(null, distances);
}
}
class CuboidShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.rotation = 0;
this.shapeType = ObjectShape.CUBOID;
this.pinned = false;
checkNumberOfPoints(this.shapeType, this.points);
}
static makeHull(geoPoints) {
// Returns the convex hull, assuming that each points[i] <= points[i + 1].
function makeHullPresorted(points) {
if (points.length <= 1) return points.slice();
// Andrew's monotone chain algorithm. Positive y coordinates correspond to 'up'
// as per the mathematical convention, instead of 'down' as per the computer
// graphics convention. This doesn't affect the correctness of the result.
const upperHull = [];
for (let i = 0; i < points.length; i += 1) {
const p = points[`${i}`];
while (upperHull.length >= 2) {
const q = upperHull[upperHull.length - 1];
const r = upperHull[upperHull.length - 2];
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop();
else break;
}
upperHull.push(p);
}
upperHull.pop();
const lowerHull = [];
for (let i = points.length - 1; i >= 0; i -= 1) {
const p = points[`${i}`];
while (lowerHull.length >= 2) {
const q = lowerHull[lowerHull.length - 1];
const r = lowerHull[lowerHull.length - 2];
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop();
else break;
}
lowerHull.push(p);
}
lowerHull.pop();
if (
upperHull.length === 1 &&
lowerHull.length === 1 &&
upperHull[0].x === lowerHull[0].x &&
upperHull[0].y === lowerHull[0].y
) return upperHull;
return upperHull.concat(lowerHull);
}
function POINT_COMPARATOR(a, b) {
if (a.x < b.x) return -1;
if (a.x > b.x) return +1;
if (a.y < b.y) return -1;
if (a.y > b.y) return +1;
return 0;
}
const newPoints = geoPoints.slice();
newPoints.sort(POINT_COMPARATOR);
return makeHullPresorted(newPoints);
}
static contain(shapePoints, x, y) {
function isLeft(P0, P1, P2) {
return (P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y);
}
const points = CuboidShape.makeHull(shapePoints);
let wn = 0;
for (let i = 0; i < points.length; i += 1) {
const p1 = points[`${i}`];
const p2 = points[i + 1] || points[0];
if (p1.y <= y) {
if (p2.y > y) {
if (isLeft(p1, p2, { x, y }) > 0) {
wn += 1;
}
}
} else if (p2.y < y) {
if (isLeft(p1, p2, { x, y }) < 0) {
wn -= 1;
}
}
}
return wn !== 0;
}
static distance(actualPoints, x, y) {
const points = [];
for (let i = 0; i < 16; i += 2) {
points.push({ x: actualPoints[i], y: actualPoints[i + 1] });
}
if (!CuboidShape.contain(points, x, y)) return null;
let minDistance = Number.MAX_SAFE_INTEGER;
for (let i = 0; i < points.length; i += 1) {
const p1 = points[`${i}`];
const p2 = points[i + 1] || points[0];
// perpendicular from point to straight length
const distance = Math.abs((p2.y - p1.y) * x - (p2.x - p1.x) * y + p2.x * p1.y - p2.y * p1.x) /
Math.sqrt((p2.y - p1.y) ** 2 + (p2.x - p1.x) ** 2);
// check if perpendicular belongs to the straight segment
const a = (p1.x - x) ** 2 + (p1.y - y) ** 2;
const b = (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2;
const c = (p2.x - x) ** 2 + (p2.y - y) ** 2;
if (distance < minDistance && a + b - c >= 0 && c + b - a >= 0) {
minDistance = distance;
}
}
return minDistance;
}
}
class RectangleTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.RECTANGLE;
this.pinned = false;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
return {
points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
rotation:
(leftPosition.rotation + findAngleDiff(
rightPosition.rotation, leftPosition.rotation,
) * offset + 360) % 360,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
}
class EllipseTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.ELLIPSE;
this.pinned = false;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
return {
points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
rotation:
(leftPosition.rotation + findAngleDiff(
rightPosition.rotation, leftPosition.rotation,
) * offset + 360) % 360,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
}
class PolyTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
for (const shape of Object.values(this.shapes)) {
shape.rotation = 0; // is not supported
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
if (offset === 0) {
return {
points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
function toArray(points) {
return points.reduce((acc, val) => {
acc.push(val.x, val.y);
return acc;
}, []);
}
function toPoints(array) {
return array.reduce((acc, _, index) => {
if (index % 2) {
acc.push({
x: array[index - 1],
y: array[index],
});
}
return acc;
}, []);
}
function curveLength(points) {
return points.slice(1).reduce((acc, _, index) => {
const dx = points[index + 1].x - points[index].x;
const dy = points[index + 1].y - points[index].y;
return acc + Math.sqrt(dx ** 2 + dy ** 2);
}, 0);
}
function curveToOffsetVec(points, length) {
const offsetVector = [0]; // with initial value
let accumulatedLength = 0;
points.slice(1).forEach((_, index) => {
const dx = points[index + 1].x - points[index].x;
const dy = points[index + 1].y - points[index].y;
accumulatedLength += Math.sqrt(dx ** 2 + dy ** 2);
offsetVector.push(accumulatedLength / length);
});
return offsetVector;
}
function findNearestPair(value, curve) {
let minimum = [0, Math.abs(value - curve[0])];
for (let i = 1; i < curve.length; i++) {
const distance = Math.abs(value - curve[i]);
if (distance < minimum[1]) {
minimum = [i, distance];
}
}
return minimum[0];
}
function matchLeftRight(leftCurve, rightCurve) {
const matching = {};
for (let i = 0; i < leftCurve.length; i++) {
matching[i] = [findNearestPair(leftCurve[i], rightCurve)];
}
return matching;
}
function matchRightLeft(leftCurve, rightCurve, leftRightMatching) {
const matchedRightPoints = Object.values(leftRightMatching).flat();
const unmatchedRightPoints = rightCurve
.map((_, index) => index)
.filter((index) => !matchedRightPoints.includes(index));
const updatedMatching = { ...leftRightMatching };
for (const rightPoint of unmatchedRightPoints) {
const leftPoint = findNearestPair(rightCurve[rightPoint], leftCurve);
updatedMatching[leftPoint].push(rightPoint);
}
for (const key of Object.keys(updatedMatching)) {
const sortedRightIndexes = updatedMatching[key].sort((a, b) => a - b);
updatedMatching[key] = sortedRightIndexes;
}
return updatedMatching;
}
function reduceInterpolation(interpolatedPoints, matching, leftPoints, rightPoints) {
function averagePoint(points) {
let sumX = 0;
let sumY = 0;
for (const point of points) {
sumX += point.x;
sumY += point.y;
}
return {
x: sumX / points.length,
y: sumY / points.length,
};
}
function computeDistance(point1, point2) {
return Math.sqrt((point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2);
}
function minimizeSegment(baseLength, N, startInterpolated, stopInterpolated) {
const threshold = baseLength / (2 * N);
const minimized = [interpolatedPoints[startInterpolated]];
let latestPushed = startInterpolated;
for (let i = startInterpolated + 1; i < stopInterpolated; i++) {
const distance = computeDistance(interpolatedPoints[latestPushed], interpolatedPoints[i]);
if (distance >= threshold) {
minimized.push(interpolatedPoints[i]);
latestPushed = i;
}
}
minimized.push(interpolatedPoints[stopInterpolated]);
if (minimized.length === 2) {
const distance = computeDistance(
interpolatedPoints[startInterpolated],
interpolatedPoints[stopInterpolated],
);
if (distance < threshold) {
return [averagePoint(minimized)];
}
}
return minimized;
}
const reduced = [];
const interpolatedIndexes = {};
let accumulated = 0;
for (let i = 0; i < leftPoints.length; i++) {
// eslint-disable-next-line
interpolatedIndexes[i] = matching[i].map(() => accumulated++);
}
function leftSegment(start, stop) {
const startInterpolated = interpolatedIndexes[start][0];
const stopInterpolated = interpolatedIndexes[stop][0];
if (startInterpolated === stopInterpolated) {
reduced.push(interpolatedPoints[startInterpolated]);
return;
}
const baseLength = curveLength(leftPoints.slice(start, stop + 1));
const N = stop - start + 1;
reduced.push(...minimizeSegment(baseLength, N, startInterpolated, stopInterpolated));
}
function rightSegment(leftPoint) {
const start = matching[leftPoint][0];
const [stop] = matching[leftPoint].slice(-1);
const startInterpolated = interpolatedIndexes[leftPoint][0];
const [stopInterpolated] = interpolatedIndexes[leftPoint].slice(-1);
const baseLength = curveLength(rightPoints.slice(start, stop + 1));
const N = stop - start + 1;
reduced.push(...minimizeSegment(baseLength, N, startInterpolated, stopInterpolated));
}
let previousOpened = null;
for (let i = 0; i < leftPoints.length; i++) {
if (matching[i].length === 1) {
// check if left segment is opened
if (previousOpened !== null) {
// check if we should continue the left segment
if (matching[i][0] === matching[previousOpened][0]) {
continue;
} else {
// left segment found
const start = previousOpened;
const stop = i - 1;
leftSegment(start, stop);
// start next left segment
previousOpened = i;
}
} else {
// start next left segment
previousOpened = i;
}
} else {
// check if left segment is opened
if (previousOpened !== null) {
// left segment found
const start = previousOpened;
const stop = i - 1;
leftSegment(start, stop);
previousOpened = null;
}
// right segment found
rightSegment(i);
}
}
// check if there is an opened segment
if (previousOpened !== null) {
leftSegment(previousOpened, leftPoints.length - 1);
}
return reduced;
}
// the algorithm below is based on fact that both left and right
// polyshapes have the same start point and the same draw direction
const leftPoints = toPoints(leftPosition.points);
const rightPoints = toPoints(rightPosition.points);
const leftOffsetVec = curveToOffsetVec(leftPoints, curveLength(leftPoints));
const rightOffsetVec = curveToOffsetVec(rightPoints, curveLength(rightPoints));
const matching = matchLeftRight(leftOffsetVec, rightOffsetVec);
const completedMatching = matchRightLeft(leftOffsetVec, rightOffsetVec, matching);
const interpolatedPoints = Object.keys(completedMatching)
.map((leftPointIdx) => +leftPointIdx)
.sort((a, b) => a - b)
.reduce((acc, leftPointIdx) => {
const leftPoint = leftPoints[leftPointIdx];
for (const rightPointIdx of completedMatching[leftPointIdx]) {
const rightPoint = rightPoints[rightPointIdx];
acc.push({
x: leftPoint.x + (rightPoint.x - leftPoint.x) * offset,
y: leftPoint.y + (rightPoint.y - leftPoint.y) * offset,
});
}
return acc;
}, []);
const reducedPoints = reduceInterpolation(interpolatedPoints, completedMatching, leftPoints, rightPoints);
return {
points: toArray(reducedPoints),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
}
class PolygonTrack extends PolyTrack {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.POLYGON;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
const copyLeft = {
...leftPosition,
points: [...leftPosition.points, leftPosition.points[0], leftPosition.points[1]],
};
const copyRight = {
...rightPosition,
points: [...rightPosition.points, rightPosition.points[0], rightPosition.points[1]],
};
const result = PolyTrack.prototype.interpolatePosition.call(this, copyLeft, copyRight, offset);
return {
...result,
points: result.points.slice(0, -2),
};
}
}
class PolylineTrack extends PolyTrack {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.POLYLINE;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
}
}
}
class PointsTrack extends PolyTrack {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.POINTS;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
// interpolate only when one point in both left and right positions
if (leftPosition.points.length === 2 && rightPosition.points.length === 2) {
return {
points: leftPosition.points.map(
(value, index) => value + (rightPosition.points[index] - value) * offset,
),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
return {
points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
}
class CuboidTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.CUBOID;
this.pinned = false;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
shape.rotation = 0; // is not supported
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
return {
points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
}
RectangleTrack.distance = RectangleShape.distance;
PolygonTrack.distance = PolygonShape.distance;
PolylineTrack.distance = PolylineShape.distance;
PointsTrack.distance = PointsShape.distance;
EllipseTrack.distance = EllipseShape.distance;
CuboidTrack.distance = CuboidShape.distance;
module.exports = {
RectangleShape,
PolygonShape,
PolylineShape,
PointsShape,
EllipseShape,
CuboidShape,
RectangleTrack,
PolygonTrack,
PolylineTrack,
PointsTrack,
EllipseTrack,
CuboidTrack,
Track,
Shape,
Tag,
objectStateFactory,
};
})();