src/model/InkModel.js
import { modelLogger as logger } from '../configuration/LoggerConfig';
import * as StrokeComponent from './StrokeComponent';
import { getSymbolsBounds, getDefaultSymbols } from './Symbol';
/**
* Recognition positions
* @typedef {Object} RecognitionPositions
* @property {Number} [lastSentPosition=-1] Index of the last sent stroke.
* @property {Number} [lastReceivedPosition=-1] Index of the last received stroke.
* @property {Number} [lastRenderedPosition=-1] Last rendered recognized symbol position
*/
/**
* Raw results
* @typedef {Object} RawResults
* @property {Object} convert=undefined The convert result
* @property {Object} exports=undefined The exports output as return by the recognition service.
*/
/**
* Editor model
* @typedef {Object} Model
* @property {Stroke} currentStroke=undefined Stroke in building process.
* @property {Array<Stroke>} rawStrokes=[] List of captured strokes.
* @property {RecognitionPositions} lastPositions Last recognition sent/received stroke indexes.
* @property {Array<Object>} defaultSymbols=[] Default symbols, relative to the current recognition type.
* @property {Array<Object>} recognizedSymbols=undefined Symbols to render (e.g. stroke, shape primitives, string, characters...).
* @property {Object} exports=undefined Result of the export (e.g. mathml, latex, text...).
* @property {RawResults} rawResults The recognition output as return by the recognition service.
* @property {Number} creationTime Date of creation timestamp.
* @property {Number} modificationTime=undefined Date of lastModification.
*/
/**
* Bounding box
* @typedef {Object} Bounds
* @property {Number} minX Minimal x coordinate
* @property {Number} maxX Maximal x coordinate
* @property {Number} minY Minimal y coordinate
* @property {Number} maxY Maximal y coordinate
*/
/**
* Create a new model
* @param {Configuration} [configuration] Parameters to use to populate default recognition symbols
* @return {Model} New model
*/
export function createModel(configuration) {
// see @typedef documentation on top
return {
currentStroke: undefined,
rawStrokes: [],
lastPositions: {
lastSentPosition: -1,
lastReceivedPosition: -1,
lastRenderedPosition: -1
},
defaultSymbols: configuration ? getDefaultSymbols(configuration) : [],
recognizedSymbols: undefined,
exports: undefined,
rawResults: {
convert: undefined,
exports: undefined
},
creationTime: new Date().getTime(),
modificationTime: undefined
};
}
/**
* Clear the model.
* @param {Model} model Current model
* @return {Model} Cleared model
*/
export function clearModel(model) {
const modelReference = model;
modelReference.currentStroke = undefined;
modelReference.rawStrokes = [];
modelReference.lastPositions.lastSentPosition = -1;
modelReference.lastPositions.lastReceivedPosition = -1;
modelReference.lastPositions.lastRenderedPosition = -1;
modelReference.recognizedSymbols = undefined;
modelReference.exports = undefined;
modelReference.rawResults.convert = undefined;
modelReference.rawResults.exports = undefined;
return modelReference;
}
/**
* Check if the model needs to be redrawn.
* @param {Model} model Current model
* @return {Boolean} True if the model needs to be redrawn, false otherwise
*/
export function needRedraw(model) {
return model.recognizedSymbols ? (model.rawStrokes.length !== model.recognizedSymbols.filter(symbol => symbol.type === 'stroke').length) : false;
}
/**
* Mutate the model given in parameter by adding the new strokeToAdd.
* @param {Model} model Current model
* @param {Stroke} stroke Stroke to be added to pending ones
* @return {Model} Updated model
*/
export function addStroke(model, stroke) {
// We use a reference to the model. The purpose here is to update the pending stroke only.
const modelReference = model;
logger.debug('addStroke', stroke);
modelReference.rawStrokes.push(stroke);
return modelReference;
}
/**
* Get the strokes that needs to be recognized
* @param {Model} model Current model
* @param {Number} [position=lastReceived] Index from where to extract strokes
* @return {Array<Stroke>} Pending strokes
*/
export function extractPendingStrokes(model, position = model.lastPositions.lastReceivedPosition + 1) {
return model.rawStrokes.slice(position);
}
/**
* Mutate the model by adding a point and close the current stroke.
* @param {Model} model Current model
* @param {{x: Number, y: Number, t: Number}} point Captured point to create current stroke
* @param {Object} properties Properties to be applied to the current stroke
* @param {Number} [dpi=96] The screen dpi resolution
* @return {Model} Updated model
*/
export function initPendingStroke(model, point, properties, dpi = 96) {
if (properties && properties['-myscript-pen-width']) {
const pxWidth = (properties['-myscript-pen-width'] * dpi) / 25.4;
Object.assign(properties, { width: pxWidth / 2 }); // FIXME hack to get better render
}
const modelReference = model;
logger.trace('initPendingStroke', point);
// Setting the current stroke to an empty one
modelReference.currentStroke = StrokeComponent.createStrokeComponent(properties);
modelReference.currentStroke = StrokeComponent.addPoint(modelReference.currentStroke, point);
return modelReference;
}
/**
* Mutate the model by adding a point to the current pending stroke.
* @param {Model} model Current model
* @param {{x: Number, y: Number, t: Number}} point Captured point to be append to the current stroke
* @return {Model} Updated model
*/
export function appendToPendingStroke(model, point) {
const modelReference = model;
if (modelReference.currentStroke) {
logger.trace('appendToPendingStroke', point);
modelReference.currentStroke = StrokeComponent.addPoint(modelReference.currentStroke, point);
}
return modelReference;
}
/**
* Mutate the model by adding the new point on a initPendingStroke.
* @param {Model} model Current model
* @param {{x: Number, y: Number, t: Number}} point Captured point to be append to the current stroke
* @return {Model} Updated model
*/
export function endPendingStroke(model, point) {
const modelReference = model;
if (modelReference.currentStroke) {
logger.trace('endPendingStroke', point);
const currentStroke = StrokeComponent.addPoint(modelReference.currentStroke, point);
// Mutating pending strokes
addStroke(modelReference, currentStroke);
// Resetting the current stroke to an undefined one
delete modelReference.currentStroke;
}
return modelReference;
}
/**
* Get the bounds of the current model.
* @param {Model} model Current model
* @return {Bounds} Bounding box enclosing the current drawn model
*/
export function getBorderCoordinates(model) {
let modelBounds = { minX: Number.MAX_VALUE, maxX: Number.MIN_VALUE, minY: Number.MAX_VALUE, maxY: Number.MIN_VALUE };
// Default symbols
if (model.defaultSymbols && model.defaultSymbols.length > 0) {
modelBounds = getSymbolsBounds(model.defaultSymbols, modelBounds);
}
// Recognized symbols
if (model.recognizedSymbols && model.recognizedSymbols.length > 0) {
modelBounds = getSymbolsBounds(model.recognizedSymbols, modelBounds);
// Pending strokes
modelBounds = getSymbolsBounds(extractPendingStrokes(model), modelBounds);
} else {
modelBounds = getSymbolsBounds(model.rawStrokes, modelBounds);
}
return modelBounds;
}
/**
* Extract strokes from an ink range
* @param {Model} model Current model
* @param {Number} firstStroke First stroke index to extract
* @param {Number} lastStroke Last stroke index to extract
* @param {Number} firstPoint First point index to extract
* @param {Number} lastPoint Last point index to extract
* @return {Array<Stroke>} The extracted strokes
*/
export function extractStrokesFromInkRange(model, firstStroke, lastStroke, firstPoint, lastPoint) {
return model.rawStrokes.slice(firstStroke, lastStroke + 1).map((stroke, index, slicedStrokes) => {
if (slicedStrokes.length < 2) {
return StrokeComponent.slice(stroke, firstPoint, lastPoint + 1);
}
if (index === 0) {
return StrokeComponent.slice(stroke, firstPoint);
}
if (index === (slicedStrokes.length - 1)) {
return StrokeComponent.slice(stroke, 0, lastPoint + 1);
}
return stroke;
});
}
/**
* Update model lastSentPosition
* @param {Model} model
* @param {Number} [position]
* @return {Model}
*/
export function updateModelSentPosition(model, position = model.rawStrokes.length - 1) {
const modelReference = model;
modelReference.lastPositions.lastSentPosition = position;
return modelReference;
}
/**
* Update model lastReceivedPosition regarding to lastSentPosition
* @param {Model} model
* @return {Model}
*/
export function updateModelReceivedPosition(model) {
const modelReference = model;
modelReference.lastPositions.lastReceivedPosition = modelReference.lastPositions.lastSentPosition;
return modelReference;
}
/**
* Reset model lastReceivedPosition and lastSentPosition
* @param {Model} model
* @return {Model}
*/
export function resetModelPositions(model) {
const modelReference = model;
modelReference.lastPositions.lastSentPosition = -1;
modelReference.lastPositions.lastReceivedPosition = -1;
return modelReference;
}
/**
* Reset model lastRenderedPosition
* @param {Model} model
* @return {Model}
*/
export function resetModelRendererPosition(model) {
const modelReference = model;
modelReference.lastPositions.lastRenderedPosition = -1;
return modelReference;
}
/**
* Update model lastRenderedPosition
* @param {Model} model
* @param {Number} [position]
* @return {Model}
*/
export function updateModelRenderedPosition(model, position = model.recognizedSymbols ? model.recognizedSymbols.length - 1 : -1) {
const modelReference = model;
modelReference.lastPositions.lastRenderedPosition = position;
return modelReference;
}
/**
* Get the symbols that needs to be rendered
* @param {Model} model Current model
* @param {Number} [position=lastRendered] Index from where to extract symbols
* @return {Array<Object>}
*/
export function extractPendingRecognizedSymbols(model, position = model.lastPositions.lastRenderedPosition + 1) {
return model.recognizedSymbols ? model.recognizedSymbols.slice(position) : [];
}
/**
* Clone model
* @param {Model} model Current model
* @return {Model} Clone of the current model
*/
export function cloneModel(model) {
const clonedModel = Object.assign({}, model);
// We clone the properties that need to be. Take care of arrays.
clonedModel.defaultSymbols = [...model.defaultSymbols];
clonedModel.currentStroke = model.currentStroke ? Object.assign({}, model.currentStroke) : undefined;
clonedModel.rawStrokes = [...model.rawStrokes];
clonedModel.lastPositions = Object.assign({}, model.lastPositions);
clonedModel.exports = model.exports ? Object.assign({}, model.exports) : undefined;
clonedModel.rawResults = Object.assign({}, model.rawResults);
clonedModel.recognizedSymbols = model.recognizedSymbols ? [...model.recognizedSymbols] : undefined;
return clonedModel;
}
/**
* Merge models
* @param {...Model} models Models to merge (ordered)
* @return {Model} Updated model
*/
export function mergeModels(...models) {
return models.reduce((a, b) => {
const modelRef = a;
modelRef.recognizedSymbols = b.recognizedSymbols;
modelRef.lastPositions.lastSentPosition = b.lastPositions.lastSentPosition;
modelRef.lastPositions.lastReceivedPosition = b.lastPositions.lastReceivedPosition;
modelRef.lastPositions.lastRenderedPosition = b.lastPositions.lastRenderedPosition;
modelRef.rawResults = b.rawResults;
modelRef.exports = b.exports;
return modelRef;
});
}