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.
1769 lines
60 KiB
JavaScript
1769 lines
60 KiB
JavaScript
/*
|
|
* Copyright (C) 2019-2020 Intel Corporation
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
/* global
|
|
require:false
|
|
*/
|
|
|
|
(() => {
|
|
const ObjectState = require('./object-state');
|
|
const {
|
|
checkObjectType,
|
|
} = require('./common');
|
|
const {
|
|
colors,
|
|
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),
|
|
};
|
|
|
|
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 {
|
|
throw new ArgumentError(
|
|
`Unknown value of shapeType has been recieved ${shapeType}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function checkShapeArea(shapeType, points) {
|
|
const MIN_SHAPE_LENGTH = 3;
|
|
const MIN_SHAPE_AREA = 9;
|
|
|
|
if (shapeType === ObjectShape.POINTS) {
|
|
return true;
|
|
}
|
|
|
|
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 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]
|
|
&& !((+value - +values[0]) % +values[2]);
|
|
}
|
|
|
|
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, injection) {
|
|
this.taskLabels = injection.labels;
|
|
this.history = injection.history;
|
|
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.updated = Date.now();
|
|
this.attributes = data.attributes.reduce((attributeAccumulator, attr) => {
|
|
attributeAccumulator[attr.spec_id] = attr.value;
|
|
return attributeAccumulator;
|
|
}, {});
|
|
this.appendDefaultAttributes(this.label);
|
|
|
|
injection.groups.max = Math.max(injection.groups.max, this.group);
|
|
}
|
|
|
|
_saveLock(lock) {
|
|
const undoLock = this.lock;
|
|
const redoLock = lock;
|
|
|
|
this.history.do(HistoryActions.CHANGED_LOCK, () => {
|
|
this.lock = undoLock;
|
|
}, () => {
|
|
this.lock = redoLock;
|
|
}, [this.clientID]);
|
|
|
|
this.lock = lock;
|
|
}
|
|
|
|
_saveColor(color) {
|
|
const undoColor = this.color;
|
|
const redoColor = color;
|
|
|
|
this.history.do(HistoryActions.CHANGED_COLOR, () => {
|
|
this.color = undoColor;
|
|
}, () => {
|
|
this.color = redoColor;
|
|
}, [this.clientID]);
|
|
|
|
this.color = color;
|
|
}
|
|
|
|
_saveHidden(hidden) {
|
|
const undoHidden = this.hidden;
|
|
const redoHidden = hidden;
|
|
|
|
this.history.do(HistoryActions.CHANGED_HIDDEN, () => {
|
|
this.hidden = undoHidden;
|
|
}, () => {
|
|
this.hidden = redoHidden;
|
|
}, [this.clientID]);
|
|
|
|
this.hidden = hidden;
|
|
}
|
|
|
|
_saveLabel(label) {
|
|
const undoLabel = this.label;
|
|
const redoLabel = label;
|
|
const undoAttributes = { ...this.attributes };
|
|
this.label = label;
|
|
this.attributes = {};
|
|
this.appendDefaultAttributes(label);
|
|
const redoAttributes = { ...this.attributes };
|
|
|
|
this.history.do(HistoryActions.CHANGED_LABEL, () => {
|
|
this.label = undoLabel;
|
|
this.attributes = undoAttributes;
|
|
}, () => {
|
|
this.label = redoLabel;
|
|
this.attributes = redoAttributes;
|
|
}, [this.clientID]);
|
|
}
|
|
|
|
_saveAttributes(attributes) {
|
|
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.attributes = redoAttributes;
|
|
}, [this.clientID]);
|
|
}
|
|
|
|
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 = updated.label || updated.attributes || updated.points
|
|
|| updated.outside || updated.occluded || updated.keyframe
|
|
|| updated.zOrder;
|
|
|
|
if (anyChanges) {
|
|
this.updated = Date.now();
|
|
}
|
|
}
|
|
|
|
delete(force) {
|
|
if (!this.lock || force) {
|
|
this.removed = true;
|
|
|
|
this.history.do(HistoryActions.REMOVED_OBJECT, () => {
|
|
this.removed = false;
|
|
}, () => {
|
|
this.removed = true;
|
|
}, [this.clientID]);
|
|
}
|
|
|
|
return this.removed;
|
|
}
|
|
}
|
|
|
|
class Drawn extends Annotation {
|
|
constructor(data, clientID, color, injection) {
|
|
super(data, clientID, injection);
|
|
|
|
this.frameMeta = injection.frameMeta;
|
|
this.hidden = false;
|
|
|
|
this.color = color;
|
|
this.shapeType = null;
|
|
}
|
|
|
|
_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.points) {
|
|
checkObjectType('points', data.points, null, Array);
|
|
checkNumberOfPoints(this.shapeType, data.points);
|
|
// cut points
|
|
const { width, height } = this.frameMeta[frame];
|
|
for (let i = 0; i < data.points.length - 1; i += 2) {
|
|
const x = data.points[i];
|
|
const y = data.points[i + 1];
|
|
|
|
checkObjectType('coordinate', x, 'number', null);
|
|
checkObjectType('coordinate', y, 'number', null);
|
|
|
|
fittedPoints.push(
|
|
Math.clamp(x, 0, width),
|
|
Math.clamp(y, 0, height),
|
|
);
|
|
}
|
|
|
|
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.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);
|
|
}
|
|
|
|
return fittedPoints;
|
|
}
|
|
|
|
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.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],
|
|
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,
|
|
};
|
|
}
|
|
|
|
// 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],
|
|
attributes: { ...this.attributes },
|
|
label: this.label,
|
|
group: {
|
|
color: this.group ? colors[this.group % colors.length] : defaultGroupColor,
|
|
id: this.group,
|
|
},
|
|
color: this.color,
|
|
hidden: this.hidden,
|
|
updated: this.updated,
|
|
frame,
|
|
};
|
|
}
|
|
|
|
_savePoints(points) {
|
|
const undoPoints = this.points;
|
|
const redoPoints = points;
|
|
|
|
this.history.do(HistoryActions.CHANGED_POINTS, () => {
|
|
this.points = undoPoints;
|
|
}, () => {
|
|
this.points = redoPoints;
|
|
}, [this.clientID]);
|
|
|
|
this.points = points;
|
|
}
|
|
|
|
_saveOccluded(occluded) {
|
|
const undoOccluded = this.occluded;
|
|
const redoOccluded = occluded;
|
|
|
|
this.history.do(HistoryActions.CHANGED_OCCLUDED, () => {
|
|
this.occluded = undoOccluded;
|
|
}, () => {
|
|
this.occluded = redoOccluded;
|
|
}, [this.clientID]);
|
|
|
|
this.occluded = occluded;
|
|
}
|
|
|
|
_saveZOrder(zOrder) {
|
|
const undoZOrder = this.zOrder;
|
|
const redoZOrder = zOrder;
|
|
|
|
this.history.do(HistoryActions.CHANGED_ZORDER, () => {
|
|
this.zOrder = undoZOrder;
|
|
}, () => {
|
|
this.zOrder = redoZOrder;
|
|
}, [this.clientID]);
|
|
|
|
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);
|
|
|
|
// Now when all fields are validated, we can apply them
|
|
if (updated.label) {
|
|
this._saveLabel(data.label);
|
|
}
|
|
|
|
if (updated.attributes) {
|
|
this._saveAttributes(data.attributes);
|
|
}
|
|
|
|
if (updated.points && fittedPoints.length) {
|
|
this._savePoints(fittedPoints);
|
|
}
|
|
|
|
if (updated.occluded) {
|
|
this._saveOccluded(data.occluded);
|
|
}
|
|
|
|
if (updated.zOrder) {
|
|
this._saveZOrder(data.zOrder);
|
|
}
|
|
|
|
if (updated.lock) {
|
|
this._saveLock(data.lock);
|
|
}
|
|
|
|
if (updated.color) {
|
|
this._saveColor(data.color);
|
|
}
|
|
|
|
if (updated.hidden) {
|
|
this._saveHidden(data.hidden);
|
|
}
|
|
|
|
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,
|
|
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,
|
|
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],
|
|
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),
|
|
group: {
|
|
color: this.group ? colors[this.group % colors.length] : defaultGroupColor,
|
|
id: this.group,
|
|
},
|
|
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,
|
|
keyframes: {
|
|
prev,
|
|
next,
|
|
first,
|
|
last,
|
|
},
|
|
frame,
|
|
};
|
|
}
|
|
|
|
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 < 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) {
|
|
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.label = redoLabel;
|
|
this.attributes = redoAttributes.unmutable;
|
|
for (const mutable of redoAttributes.mutable) {
|
|
this.shapes[mutable.frame].attributes = mutable.attributes;
|
|
}
|
|
}, [this.clientID]);
|
|
}
|
|
|
|
_saveAttributes(frame, attributes) {
|
|
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.attributes = redoAttributes;
|
|
if (redoShape) {
|
|
this.shapes[frame] = redoShape;
|
|
}
|
|
}, [this.clientID]);
|
|
}
|
|
|
|
_appendShapeActionToHistory(actionType, frame, undoShape, redoShape) {
|
|
this.history.do(actionType, () => {
|
|
if (!undoShape) {
|
|
delete this.shapes[frame];
|
|
} else {
|
|
this.shapes[frame] = undoShape;
|
|
}
|
|
}, () => {
|
|
if (!redoShape) {
|
|
delete this.shapes[frame];
|
|
} else {
|
|
this.shapes[frame] = redoShape;
|
|
}
|
|
}, [this.clientID]);
|
|
}
|
|
|
|
_savePoints(frame, points) {
|
|
const current = this.get(frame);
|
|
const wasKeyframe = frame in this.shapes;
|
|
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
|
|
const redoShape = wasKeyframe ? { ...this.shapes[frame], points } : {
|
|
frame,
|
|
points,
|
|
zOrder: current.zOrder,
|
|
outside: current.outside,
|
|
occluded: current.occluded,
|
|
attributes: {},
|
|
};
|
|
|
|
this.shapes[frame] = redoShape;
|
|
this._appendShapeActionToHistory(
|
|
HistoryActions.CHANGED_POINTS,
|
|
frame,
|
|
undoShape,
|
|
redoShape,
|
|
);
|
|
}
|
|
|
|
_saveOutside(frame, outside) {
|
|
const current = this.get(frame);
|
|
const wasKeyframe = frame in this.shapes;
|
|
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
|
|
const redoShape = wasKeyframe ? { ...this.shapes[frame], outside } : {
|
|
frame,
|
|
outside,
|
|
zOrder: current.zOrder,
|
|
points: current.points,
|
|
occluded: current.occluded,
|
|
attributes: {},
|
|
};
|
|
|
|
this.shapes[frame] = redoShape;
|
|
this._appendShapeActionToHistory(
|
|
HistoryActions.CHANGED_OUTSIDE,
|
|
frame,
|
|
undoShape,
|
|
redoShape,
|
|
);
|
|
}
|
|
|
|
_saveOccluded(frame, occluded) {
|
|
const current = this.get(frame);
|
|
const wasKeyframe = frame in this.shapes;
|
|
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
|
|
const redoShape = wasKeyframe ? { ...this.shapes[frame], occluded } : {
|
|
frame,
|
|
occluded,
|
|
zOrder: current.zOrder,
|
|
points: current.points,
|
|
outside: current.outside,
|
|
attributes: {},
|
|
};
|
|
|
|
this.shapes[frame] = redoShape;
|
|
this._appendShapeActionToHistory(
|
|
HistoryActions.CHANGED_OCCLUDED,
|
|
frame,
|
|
undoShape,
|
|
redoShape,
|
|
);
|
|
}
|
|
|
|
_saveZOrder(frame, zOrder) {
|
|
const current = this.get(frame);
|
|
const wasKeyframe = frame in this.shapes;
|
|
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
|
|
const redoShape = wasKeyframe ? { ...this.shapes[frame], zOrder } : {
|
|
frame,
|
|
zOrder,
|
|
occluded: current.occluded,
|
|
points: current.points,
|
|
outside: current.outside,
|
|
attributes: {},
|
|
};
|
|
|
|
this.shapes[frame] = redoShape;
|
|
this._appendShapeActionToHistory(
|
|
HistoryActions.CHANGED_ZORDER,
|
|
frame,
|
|
undoShape,
|
|
redoShape,
|
|
);
|
|
}
|
|
|
|
_saveKeyframe(frame, keyframe) {
|
|
const current = this.get(frame);
|
|
const wasKeyframe = frame in this.shapes;
|
|
|
|
if ((keyframe && wasKeyframe) || (!keyframe && !wasKeyframe)) {
|
|
return;
|
|
}
|
|
|
|
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
|
|
const redoShape = keyframe ? {
|
|
frame,
|
|
zOrder: current.zOrder,
|
|
points: current.points,
|
|
outside: current.outside,
|
|
occluded: current.occluded,
|
|
attributes: {},
|
|
} : undefined;
|
|
|
|
if (redoShape) {
|
|
this.shapes[frame] = redoShape;
|
|
} else {
|
|
delete this.shapes[frame];
|
|
}
|
|
|
|
this._appendShapeActionToHistory(
|
|
HistoryActions.CHANGED_KEYFRAME,
|
|
frame,
|
|
undoShape,
|
|
redoShape,
|
|
);
|
|
}
|
|
|
|
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);
|
|
|
|
if (updated.label) {
|
|
this._saveLabel(data.label);
|
|
}
|
|
|
|
if (updated.lock) {
|
|
this._saveLock(data.lock);
|
|
}
|
|
|
|
if (updated.color) {
|
|
this._saveColor(data.color);
|
|
}
|
|
|
|
if (updated.hidden) {
|
|
this._saveHidden(data.hidden);
|
|
}
|
|
|
|
if (updated.points && fittedPoints.length) {
|
|
this._savePoints(frame, fittedPoints);
|
|
}
|
|
|
|
if (updated.outside) {
|
|
this._saveOutside(frame, data.outside);
|
|
}
|
|
|
|
if (updated.occluded) {
|
|
this._saveOccluded(frame, data.occluded);
|
|
}
|
|
|
|
if (updated.zOrder) {
|
|
this._saveZOrder(frame, data.zOrder);
|
|
}
|
|
|
|
if (updated.attributes) {
|
|
this._saveAttributes(frame, data.attributes);
|
|
}
|
|
|
|
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],
|
|
occluded: leftPosition.occluded,
|
|
outside: leftPosition.outside,
|
|
zOrder: leftPosition.zOrder,
|
|
keyframe: targetFrame in this.shapes,
|
|
};
|
|
}
|
|
|
|
if (rightPosition) {
|
|
return {
|
|
points: [...rightPosition.points],
|
|
occluded: rightPosition.occluded,
|
|
outside: true,
|
|
zOrder: rightPosition.zOrder,
|
|
keyframe: targetFrame in this.shapes,
|
|
};
|
|
}
|
|
|
|
throw new DataError(
|
|
'No one left position or right position was found. '
|
|
+ `Interpolation impossible. Client ID: ${this.id}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
class Tag extends Annotation {
|
|
constructor(data, clientID, injection) {
|
|
super(data, clientID, injection);
|
|
}
|
|
|
|
// 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,
|
|
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.group,
|
|
updated: this.updated,
|
|
frame,
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
if (updated.attributes) {
|
|
this._saveAttributes(data.attributes);
|
|
}
|
|
|
|
if (updated.lock) {
|
|
this._saveLock(data.lock);
|
|
}
|
|
|
|
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;
|
|
checkNumberOfPoints(this.shapeType, this.points);
|
|
}
|
|
|
|
static distance(points, x, y) {
|
|
const [xtl, ytl, xbr, ybr] = points;
|
|
|
|
if (!(x >= xtl && x <= xbr && y >= ytl && y <= ybr)) {
|
|
// Cursor is outside of a box
|
|
return null;
|
|
}
|
|
|
|
// The shortest distance from point to an edge
|
|
return Math.min.apply(null, [x - xtl, y - ytl, xbr - x, ybr - y]);
|
|
}
|
|
}
|
|
|
|
class PolyShape extends Shape {
|
|
constructor(data, clientID, color, injection) {
|
|
super(data, clientID, color, injection);
|
|
}
|
|
}
|
|
|
|
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(
|
|
Math.pow(x - xCross, 2)
|
|
+ Math.pow(y - yCross, 2),
|
|
));
|
|
} else {
|
|
distances.push(
|
|
Math.min(
|
|
Math.sqrt(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2)),
|
|
Math.sqrt(Math.pow(x2 - x, 2) + Math.pow(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(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)),
|
|
);
|
|
} else {
|
|
// The link below works for lines (which have infinit 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(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2)),
|
|
Math.sqrt(Math.pow(x2 - x, 2) + Math.pow(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(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2)),
|
|
);
|
|
}
|
|
|
|
return Math.min.apply(null, distances);
|
|
}
|
|
}
|
|
|
|
class RectangleTrack extends Track {
|
|
constructor(data, clientID, color, injection) {
|
|
super(data, clientID, color, injection);
|
|
this.shapeType = ObjectShape.RECTANGLE;
|
|
for (const shape of Object.values(this.shapes)) {
|
|
checkNumberOfPoints(this.shapeType, shape.points);
|
|
}
|
|
}
|
|
|
|
interpolatePosition(leftPosition, rightPosition, offset) {
|
|
const positionOffset = [
|
|
rightPosition.points[0] - leftPosition.points[0],
|
|
rightPosition.points[1] - leftPosition.points[1],
|
|
rightPosition.points[2] - leftPosition.points[2],
|
|
rightPosition.points[3] - leftPosition.points[3],
|
|
];
|
|
|
|
return { // xtl, ytl, xbr, ybr
|
|
points: [
|
|
leftPosition.points[0] + positionOffset[0] * offset,
|
|
leftPosition.points[1] + positionOffset[1] * offset,
|
|
leftPosition.points[2] + positionOffset[2] * offset,
|
|
leftPosition.points[3] + positionOffset[3] * offset,
|
|
],
|
|
occluded: leftPosition.occluded,
|
|
outside: leftPosition.outside,
|
|
zOrder: leftPosition.zOrder,
|
|
};
|
|
}
|
|
}
|
|
|
|
class PolyTrack extends Track {
|
|
constructor(data, clientID, color, injection) {
|
|
super(data, clientID, color, injection);
|
|
}
|
|
|
|
interpolatePosition(leftPosition, rightPosition, offset) {
|
|
function findBox(points) {
|
|
let xmin = Number.MAX_SAFE_INTEGER;
|
|
let ymin = Number.MAX_SAFE_INTEGER;
|
|
let xmax = Number.MIN_SAFE_INTEGER;
|
|
let ymax = Number.MIN_SAFE_INTEGER;
|
|
|
|
for (let i = 0; i < points.length; i += 2) {
|
|
if (points[i] < xmin) xmin = points[i];
|
|
if (points[i + 1] < ymin) ymin = points[i + 1];
|
|
if (points[i] > xmax) xmax = points[i];
|
|
if (points[i + 1] > ymax) ymax = points[i + 1];
|
|
}
|
|
|
|
return {
|
|
xmin,
|
|
ymin,
|
|
xmax,
|
|
ymax,
|
|
};
|
|
}
|
|
|
|
function normalize(points, box) {
|
|
const normalized = [];
|
|
const width = box.xmax - box.xmin;
|
|
const height = box.ymax - box.ymin;
|
|
|
|
for (let i = 0; i < points.length; i += 2) {
|
|
normalized.push(
|
|
(points[i] - box.xmin) / width,
|
|
(points[i + 1] - box.ymin) / height,
|
|
);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function denormalize(points, box) {
|
|
const denormalized = [];
|
|
const width = box.xmax - box.xmin;
|
|
const height = box.ymax - box.ymin;
|
|
|
|
for (let i = 0; i < points.length; i += 2) {
|
|
denormalized.push(
|
|
points[i] * width + box.xmin,
|
|
points[i + 1] * height + box.ymin,
|
|
);
|
|
}
|
|
|
|
return denormalized;
|
|
}
|
|
|
|
function toPoints(array) {
|
|
const points = [];
|
|
for (let i = 0; i < array.length; i += 2) {
|
|
points.push({
|
|
x: array[i],
|
|
y: array[i + 1],
|
|
});
|
|
}
|
|
|
|
return points;
|
|
}
|
|
|
|
function toArray(points) {
|
|
const array = [];
|
|
for (const point of points) {
|
|
array.push(point.x, point.y);
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
function computeDistances(source, target) {
|
|
const distances = {};
|
|
for (let i = 0; i < source.length; i++) {
|
|
distances[i] = distances[i] || {};
|
|
for (let j = 0; j < target.length; j++) {
|
|
const dx = source[i].x - target[j].x;
|
|
const dy = source[i].y - target[j].y;
|
|
|
|
distances[i][j] = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
|
|
}
|
|
}
|
|
|
|
return distances;
|
|
}
|
|
|
|
function truncateByThreshold(mapping, threshold) {
|
|
for (const key of Object.keys(mapping)) {
|
|
if (mapping[key].distance > threshold) {
|
|
delete mapping[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/Stable_marriage_problem
|
|
// TODO: One of important part of the algorithm is to correctly match
|
|
// "corner" points. Thus it is possible for each of such point calculate
|
|
// a descriptor (d) and use (x, y, d) to calculate the distance. One more
|
|
// idea is to be sure that order or matched points is preserved. For example,
|
|
// if p1 matches q1 and p2 matches q2 and between p1 and p2 we don't have any
|
|
// points thus we should not have points between q1 and q2 as well.
|
|
function stableMarriageProblem(men, women, distances) {
|
|
const menPreferences = {};
|
|
for (const man of men) {
|
|
menPreferences[man] = women.concat()
|
|
.sort((w1, w2) => distances[man][w1] - distances[man][w2]);
|
|
}
|
|
|
|
// Start alghoritm with max N^2 complexity
|
|
const womenMaybe = {}; // id woman:id man,distance
|
|
const menBusy = {}; // id man:boolean
|
|
let prefIndex = 0;
|
|
|
|
// While there is at least one free man
|
|
while (Object.values(menBusy).length !== men.length) {
|
|
// Every man makes offer to the best woman
|
|
for (const man of men) {
|
|
// The man have already found a woman
|
|
if (menBusy[man]) {
|
|
continue;
|
|
}
|
|
|
|
const woman = menPreferences[man][prefIndex];
|
|
const distance = distances[man][woman];
|
|
|
|
// A women chooses the best offer and says "maybe"
|
|
if (woman in womenMaybe && womenMaybe[woman].distance > distance) {
|
|
// A woman got better offer
|
|
const prevChoice = womenMaybe[woman].value;
|
|
delete womenMaybe[woman];
|
|
delete menBusy[prevChoice];
|
|
}
|
|
|
|
if (!(woman in womenMaybe)) {
|
|
womenMaybe[woman] = {
|
|
value: man,
|
|
distance,
|
|
};
|
|
|
|
menBusy[man] = true;
|
|
}
|
|
}
|
|
|
|
prefIndex++;
|
|
}
|
|
|
|
const result = {};
|
|
for (const woman of Object.keys(womenMaybe)) {
|
|
result[womenMaybe[woman].value] = {
|
|
value: woman,
|
|
distance: womenMaybe[woman].distance,
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getMapping(source, target) {
|
|
function sumEdges(points) {
|
|
let result = 0;
|
|
for (let i = 1; i < points.length; i += 2) {
|
|
const distance = Math.sqrt(Math.pow(points[i].x - points[i - 1].x, 2)
|
|
+ Math.pow(points[i].y - points[i - 1].y, 2));
|
|
result += distance;
|
|
}
|
|
|
|
// Corner case when work with one point
|
|
// Mapping in this case can't be wrong
|
|
if (!result) {
|
|
return Number.MAX_SAFE_INTEGER;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function computeDeviation(points, average) {
|
|
let result = 0;
|
|
for (let i = 1; i < points.length; i += 2) {
|
|
const distance = Math.sqrt(Math.pow(points[i].x - points[i - 1].x, 2)
|
|
+ Math.pow(points[i].y - points[i - 1].y, 2));
|
|
result += Math.pow(distance - average, 2);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
const processedSource = [];
|
|
const processedTarget = [];
|
|
|
|
const distances = computeDistances(source, target);
|
|
const mapping = stableMarriageProblem(Array.from(source.keys()),
|
|
Array.from(target.keys()), distances);
|
|
|
|
const average = (sumEdges(target)
|
|
+ sumEdges(source)) / (target.length + source.length);
|
|
const meanSquareDeviation = Math.sqrt((computeDeviation(source, average)
|
|
+ computeDeviation(target, average)) / (source.length + target.length));
|
|
const threshold = average + 3 * meanSquareDeviation; // 3 sigma rule
|
|
truncateByThreshold(mapping, threshold);
|
|
for (const key of Object.keys(mapping)) {
|
|
mapping[key] = mapping[key].value;
|
|
}
|
|
|
|
// const receivingOrder = Object.keys(mapping).map(x => +x).sort((a,b) => a - b);
|
|
const receivingOrder = this.appendMapping(mapping, source, target);
|
|
|
|
for (const pointIdx of receivingOrder) {
|
|
processedSource.push(source[pointIdx]);
|
|
processedTarget.push(target[mapping[pointIdx]]);
|
|
}
|
|
|
|
return [processedSource, processedTarget];
|
|
}
|
|
|
|
if (offset === 0) {
|
|
return {
|
|
points: [...leftPosition.points],
|
|
occluded: leftPosition.occluded,
|
|
outside: leftPosition.outside,
|
|
zOrder: leftPosition.zOrder,
|
|
};
|
|
}
|
|
|
|
let leftBox = findBox(leftPosition.points);
|
|
let rightBox = findBox(rightPosition.points);
|
|
|
|
// Sometimes (if shape has one point or shape is line),
|
|
// We can get box with zero area
|
|
// Next computation will be with NaN in this case
|
|
// We have to prevent it
|
|
const delta = 1;
|
|
if (leftBox.xmax - leftBox.xmin < delta || rightBox.ymax - rightBox.ymin < delta) {
|
|
leftBox = {
|
|
xmin: 0,
|
|
xmax: 1024, // TODO: Get actual image size
|
|
ymin: 0,
|
|
ymax: 768,
|
|
};
|
|
|
|
rightBox = leftBox;
|
|
}
|
|
|
|
const leftPoints = toPoints(normalize(leftPosition.points, leftBox));
|
|
const rightPoints = toPoints(normalize(rightPosition.points, rightBox));
|
|
|
|
let newLeftPoints = [];
|
|
let newRightPoints = [];
|
|
if (leftPoints.length > rightPoints.length) {
|
|
const [
|
|
processedRight,
|
|
processedLeft,
|
|
] = getMapping.call(this, rightPoints, leftPoints);
|
|
newLeftPoints = processedLeft;
|
|
newRightPoints = processedRight;
|
|
} else {
|
|
const [
|
|
processedLeft,
|
|
processedRight,
|
|
] = getMapping.call(this, leftPoints, rightPoints);
|
|
newLeftPoints = processedLeft;
|
|
newRightPoints = processedRight;
|
|
}
|
|
|
|
const absoluteLeftPoints = denormalize(toArray(newLeftPoints), leftBox);
|
|
const absoluteRightPoints = denormalize(toArray(newRightPoints), rightBox);
|
|
|
|
const interpolation = [];
|
|
for (let i = 0; i < absoluteLeftPoints.length; i++) {
|
|
interpolation.push(absoluteLeftPoints[i] + (
|
|
absoluteRightPoints[i] - absoluteLeftPoints[i]) * offset);
|
|
}
|
|
|
|
return {
|
|
points: interpolation,
|
|
occluded: leftPosition.occluded,
|
|
outside: leftPosition.outside,
|
|
zOrder: leftPosition.zOrder,
|
|
};
|
|
}
|
|
|
|
// mapping is predicted order of points sourse_idx:target_idx
|
|
// some points from source and target can absent in mapping
|
|
// source, target - arrays of points. Target array size >= sourse array size
|
|
appendMapping(mapping, source, target) {
|
|
const targetMatched = Object.values(mapping).map((x) => +x);
|
|
const sourceMatched = Object.keys(mapping).map((x) => +x);
|
|
const orderForReceive = [];
|
|
|
|
function findNeighbors(point) {
|
|
let prev = point;
|
|
let next = point;
|
|
|
|
if (!targetMatched.length) {
|
|
// Prevent infinity loop
|
|
throw new ScriptingError('Interpolation mapping is empty');
|
|
}
|
|
|
|
while (!targetMatched.includes(prev)) {
|
|
prev--;
|
|
if (prev < 0) {
|
|
prev = target.length - 1;
|
|
}
|
|
}
|
|
|
|
while (!targetMatched.includes(next)) {
|
|
next++;
|
|
if (next >= target.length) {
|
|
next = 0;
|
|
}
|
|
}
|
|
|
|
return [prev, next];
|
|
}
|
|
|
|
function computeOffset(point, prev, next) {
|
|
const pathPoints = [];
|
|
|
|
while (prev !== next) {
|
|
pathPoints.push(target[prev]);
|
|
prev++;
|
|
if (prev >= target.length) {
|
|
prev = 0;
|
|
}
|
|
}
|
|
pathPoints.push(target[next]);
|
|
|
|
let curveLength = 0;
|
|
let offset = 0;
|
|
let iCrossed = false;
|
|
for (let k = 1; k < pathPoints.length; k++) {
|
|
const p1 = pathPoints[k];
|
|
const p2 = pathPoints[k - 1];
|
|
const distance = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
|
|
|
if (!iCrossed) {
|
|
offset += distance;
|
|
}
|
|
curveLength += distance;
|
|
if (target[point] === pathPoints[k]) {
|
|
iCrossed = true;
|
|
}
|
|
}
|
|
|
|
if (!curveLength) {
|
|
return 0;
|
|
}
|
|
|
|
return offset / curveLength;
|
|
}
|
|
|
|
for (let i = 0; i < target.length; i++) {
|
|
const index = targetMatched.indexOf(i);
|
|
if (index === -1) {
|
|
// We have to find a neighbours which have been mapped
|
|
const [prev, next] = findNeighbors(i);
|
|
|
|
// Now compute edge offset
|
|
const offset = computeOffset(i, prev, next);
|
|
|
|
// Get point between two neighbors points
|
|
const prevPoint = target[prev];
|
|
const nextPoint = target[next];
|
|
const autoPoint = {
|
|
x: prevPoint.x + (nextPoint.x - prevPoint.x) * offset,
|
|
y: prevPoint.y + (nextPoint.y - prevPoint.y) * offset,
|
|
};
|
|
|
|
// Put it into matched
|
|
source.push(autoPoint);
|
|
mapping[source.length - 1] = i;
|
|
orderForReceive.push(source.length - 1);
|
|
} else {
|
|
orderForReceive.push(sourceMatched[index]);
|
|
}
|
|
}
|
|
|
|
return orderForReceive;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
RectangleTrack.distance = RectangleShape.distance;
|
|
PolygonTrack.distance = PolygonShape.distance;
|
|
PolylineTrack.distance = PolylineShape.distance;
|
|
PointsTrack.distance = PointsShape.distance;
|
|
|
|
module.exports = {
|
|
RectangleShape,
|
|
PolygonShape,
|
|
PolylineShape,
|
|
PointsShape,
|
|
RectangleTrack,
|
|
PolygonTrack,
|
|
PolylineTrack,
|
|
PointsTrack,
|
|
Track,
|
|
Shape,
|
|
Tag,
|
|
objectStateFactory,
|
|
};
|
|
})();
|