import { modelLogger as logger } from '../configuration/LoggerConfig'
import * as StrokeComponent from './StrokeComponent'
import { getSymbolsBounds } 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 {Array} strokeGroups=[] Group of strokes with same pen style.
* @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: [],
strokeGroups: [],
lastPositions: {
lastSentPosition: -1,
lastReceivedPosition: -1,
lastRenderedPosition: -1
},
defaultSymbols: [],
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.strokeGroups = []
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
}
/**
* Mutate the model given in parameter by adding the new strokeToAdd and the penstyle. Used for iink REST.
* @param {Model} model Current model
* @param {Stroke} stroke Stroke to be added to pending ones
* @param {PenStyle} strokePenStyle
* @return {Model} Updated model
*/
export function addStrokeToGroup (model, stroke, strokePenStyle) {
// We use a reference to the model. The purpose here is to update the pending stroke only.
const modelReference = model
logger.debug('addStroke', stroke)
const lastGroup = modelReference.strokeGroups.length - 1
if (modelReference.strokeGroups[lastGroup] && modelReference.strokeGroups[lastGroup].penStyle === strokePenStyle) {
modelReference.strokeGroups[lastGroup].strokes.push(stroke)
} else {
const newStrokeGroup = {
penStyle: strokePenStyle,
strokes: []
}
const strokeCopy = {}
Object.assign(strokeCopy, stroke)
newStrokeGroup.strokes.push(strokeCopy)
modelReference.strokeGroups.push(newStrokeGroup)
}
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
* @param {PenStyle} penStyle
* @return {Model} Updated model
*/
export function endPendingStroke (model, point, penStyle) {
const modelReference = model
if (modelReference.currentStroke) {
logger.trace('endPendingStroke', point)
const currentStroke = StrokeComponent.addPoint(modelReference.currentStroke, point)
// Mutating pending strokes
addStroke(modelReference, currentStroke)
addStrokeToGroup(modelReference, currentStroke, penStyle)
// 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.strokeGroups = JSON.parse(JSON.stringify(model.strokeGroups))
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
})
}