/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
import { getRandomColor } from "../utils.mjs"
import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs"
import { ensureTypeSafety, serializesByPropertyType } from "../parameters.mjs"
// This file contains the default data to be imported from all other objects
/**
* Class to extend for every type of object that
* can be drawn on the canvas.
*/
export class DrawableObject {
/**
* Base name of the object. Needs to be constant over time.
* @return {string} Type of the object.
*/
static type() {
return "Unknown"
}
/**
* Translated name of the object to be shown to the user.
* @return {string}
*/
static displayType() {
return "Unknown"
}
/**
* Translated name of the object in plural form to be shown to the user.
* @return {string}
*/
static displayTypeMultiple() {
return "Unknowns"
}
/**
* True if this object can be created by the user, false if it can only
* be instantiated by other objects
* @return {boolean}
*/
static createable() {
return true
}
/**
* List of properties used in the Object Editor.
*
* Properties are set with key as property name and
* value as it's type name (e.g 'numbers', 'string'...),
* an Enum for enumerations, an ObjectType for DrawableObjects
* with a specific type, a List instance for lists, a
* Dictionary instance for dictionaries, an Expression for expressions...
*
* If the property is to be translated, the key should be passed
* through the QT_TRANSLATE_NOOP macro in that form:
* [QT_TRANSLATE_NOOP('prop','key')]
* Enums that are translated should be indexed in parameters.mjs and
* then be linked directly here.
*
* @return {Object.}
*/
static properties() {
return {}
}
/**
* True if this object can be executed, so that an y value might be computed
* for an x value. Only ExecutableObjects have that property set to true.
* @return {boolean}
*/
static executable() {
return false
}
/**
* Imports the object from its serialized form.
* @return {DrawableObject}
*/
static import(name, visible, color, labelContent, ...args) {
let importedArgs = [name.toString(), visible === true, color.toString(), labelContent]
console.log("Importing", this.type(), name, args)
for(let [name, propType] of Object.entries(this.properties()))
if(!name.startsWith("comment")) {
importedArgs.push(ensureTypeSafety(propType, args[importedArgs.length - 4]))
}
return new this(...importedArgs)
}
/**
* Base constructor for the object.
* @param {string} name - Name of the object
* @param {boolean} visible - true if the object is visible, false otherwise.
* @param {color|string} color - Color of the object (can be string or QColor)
* @param {"null"|"name"|"name + value"} labelContent - One of 'null', 'name' or 'name + value' describing the content of the label.
* @constructor
*/
constructor(name, visible = true, color = null, labelContent = "name + value") {
if(color == null) color = getRandomColor()
this.type = this.constructor.type()
this.name = name
this.visible = visible
this.color = color
this.labelContent = labelContent // "null", "name", "name + value"
this.requiredBy = []
this.requires = []
}
/**
* Serializes the object in an array that can be JSON serialized.
* These parameters will be re-entered in the constructor when restored.
* @return {array}
*/
export() {
let exportList = [this.name, this.visible, this.color.toString(), this.labelContent]
for(let [name, propType] of Object.entries(this.constructor.properties()))
if(!name.startsWith("comment"))
exportList.push(serializesByPropertyType(propType, this[name]))
return exportList
}
/**
* String representing the object that will be displayed to the user.
* It allows for 2 lines and translated text, but it shouldn't be too long.
* @return {string}
*/
getReadableString() {
return `${this.name} = Unknown`
}
/**
* Latex markuped version of the readable string.
* Every non latin character should be passed as latex symbols and formulas
* should be in latex form.
* See ../latex.mjs for helper methods.
* @return {string}
*/
getLatexString() {
return this.getReadableString()
}
/**
* Readable string content of the label depending on the value of the latexContent.
* @return {string}
*/
getLabel() {
switch(this.labelContent) {
case "name":
return this.name
case "name + value":
return this.getReadableString()
case "null":
return ""
}
}
/**
* Latex markup string content of the label depending on the value of the latexContent.
* Every non latin character should be passed as latex symbols and formulas
* should be in latex form.
* See ../latex.mjs for helper methods.
* @return {string}
*/
getLatexLabel() {
switch(this.labelContent) {
case "name":
return Latex.variable(this.name)
case "name + value":
return this.getLatexString()
case "null":
return ""
}
}
/**
* Returns the recursive list of objects this one depends on.
* @return {array}
*/
getDependenciesList() {
let dependencies = this.requires.map(obj => obj)
for(let obj of this.requires)
dependencies = dependencies.concat(obj.getDependenciesList())
return dependencies
}
/**
* Callback method when one of the properties of the object is updated.
*/
update() {
// Refreshing dependencies.
for(let obj of this.requires)
obj.requiredBy = obj.requiredBy.filter(dep => dep !== this)
// Checking objects this one depends on
this.requires = []
let currentObjectsByName = Objects.currentObjectsByName
let properties = this.constructor.properties()
for(let property in properties)
if(typeof properties[property] == "object" && "type" in properties[property])
if(properties[property].type === "Expression" && this[property] != null) {
// Expressions with dependencies
for(let objName of this[property].requiredObjects()) {
if(objName in currentObjectsByName && !this.requires.includes(currentObjectsByName[objName])) {
this.requires.push(currentObjectsByName[objName])
currentObjectsByName[objName].requiredBy.push(this)
}
}
if(this[property].canBeCached && this[property].requiredObjects().length > 0)
// Recalculate
this[property].recache()
} else if(properties[property].type === "ObjectType" && this[property] != null) {
// Object dependency
this.requires.push(this[property])
this[property].requiredBy.push(this)
}
// Updating objects dependent on this one
for(let req of this.requiredBy)
req.update()
}
/**
* Callback method when the object is about to get deleted.
*/
delete() {
for(let toRemove of this.requiredBy) { // Normally, there should be none here, but better leave nothing just in case.
Objects.deleteObject(toRemove.name)
}
for(let toRemoveFrom of this.requires) {
toRemoveFrom.requiredBy = toRemoveFrom.requiredBy.filter(o => o !== this)
}
}
/**
* Abstract method. Draw the object onto the canvas with the.
* @param {CanvasAPI} canvas
*/
draw(canvas) {
}
/**
* Applicates a drawFunction with two position arguments depending on
* both the posX and posY of where the label should be displayed,
* and the labelPosition which declares the label should be displayed
* relatively to that position.
*
* @param {string|Enum} labelPosition - Position of the label relative to the marked position
* @param {number} offset - Margin between the position and the object to be drawn
* @param {{width: number, height: number}} size - Size of the label item, containing two properties, "width" and "height"
* @param {number} posX - Component of the marked position on the x-axis
* @param {number} posY - Component of the marked position on the y-axis
* @param {function} drawFunction - Function with two arguments (x, y) that will be called to draw the label
*/
drawPositionDivergence(labelPosition, offset, size, posX, posY, drawFunction) {
switch(labelPosition) {
case "center":
drawFunction(posX - size.width / 2, posY - size.height / 2)
break
case "top":
case "above":
drawFunction(posX - size.width / 2, posY - size.height - offset)
break
case "bottom":
case "below":
drawFunction(posX - size.width / 2, posY + offset)
break
case "left":
drawFunction(posX - size.width - offset, posY - size.height / 2)
break
case "right":
drawFunction(posX + offset, posY - size.height / 2)
break
case "top-left":
case "above-left":
drawFunction(posX - size.width, posY - size.height - offset)
break
case "top-right":
case "above-right":
drawFunction(posX + offset, posY - size.height - offset)
break
case "bottom-left":
case "below-left":
drawFunction(posX - size.width - offset, posY + offset)
break
case "bottom-right":
case "below-right":
drawFunction(posX + offset, posY + offset)
break
}
}
/**
* Automatically draw text (by default the label of the object on the canvas
* depending on user settings.
* This method takes into account both the posX and posY of where the label
* should be displayed, including the labelPosition relative to it.
* The text is get both through the getLatexFunction and getTextFunction
* depending on whether to use latex.
* Then, it's displayed using the drawFunctionLatex (x,y,imageData) and
* drawFunctionText (x,y,text) depending on whether to use latex.
*
* @param {CanvasAPI} canvas
* @param {string|Enum} labelPosition - Position of the label relative to the marked position
* @param {number} posX - Component of the marked position on the x-axis
* @param {number} posY - Component of the marked position on the y-axis
* @param {boolean} forceText - Force the rendering of the label as text
* @param {function|null} getLatexFunction - Function (no argument) to get the latex markup to be displayed
* @param {function|null} getTextFunction - Function (no argument) to get the text to be displayed
* @param {function|null} drawFunctionLatex - Function (x,y,imageData) to display the latex image
* @param {function|null} drawFunctionText - Function (x,y,text,textSize) to display the text
*/
drawLabel(canvas, labelPosition, posX, posY, forceText = false,
getLatexFunction = null, getTextFunction = null, drawFunctionLatex = null, drawFunctionText = null) {
// Default functions
if(getLatexFunction == null)
getLatexFunction = this.getLatexLabel.bind(this)
if(getTextFunction == null)
getTextFunction = this.getLabel.bind(this)
if(drawFunctionLatex == null)
drawFunctionLatex = (x, y, ltxImg) => canvas.drawVisibleImage(ltxImg.source, x, y, ltxImg.width, ltxImg.height)
if(drawFunctionText == null)
drawFunctionText = (x, y, text, textSize) => canvas.drawVisibleText(text, x, y + textSize.height) // Positioned from left bottom
// Drawing the label
if(!forceText && Latex.enabled) {
// With latex
let drawLblCb = ((ltxImg) => {
this.drawPositionDivergence(labelPosition, 8 + canvas.linewidth / 2, ltxImg, posX, posY, (x, y) => drawFunctionLatex(x, y, ltxImg))
})
let ltxLabel = getLatexFunction()
if(ltxLabel !== "")
canvas.renderLatexImage(ltxLabel, this.color, drawLblCb)
} else {
// Without latex
let text = getTextFunction()
canvas.font = `${canvas.textsize}px sans-serif`
let textSize = canvas.measureText(text)
this.drawPositionDivergence(labelPosition, 8 + canvas.linewidth / 2, textSize, posX, posY, (x, y) => drawFunctionText(x, y, text, textSize))
}
}
toString() {
return this.name
}
}
/**
* Class to be extended for every object on which
* an y for a x can be computed with the execute function.
* If a value cannot be found during execute, it will
* return null. However, theses same x values will
* return false when passed to canExecute.
*/
export class ExecutableObject extends DrawableObject {
/**
* Returns the corresponding y value for an x value.
* If the object isn't defined on the given x, then
* this function will return null.
*
* @param {number} x
* @returns {number|null}
*/
execute(x = 1) {
return 0
}
/**
* Returns false if the object isn't defined on the given x, true otherwise.
*
* @param {number} x
* @returns {boolean}
*/
canExecute(x = 1) {
return true
}
/**
* Returns the simplified expression string for a given x.
*
* @param {number} x
* @returns {string|Expression}
*/
simplify(x = 1) {
return "0"
}
/**
* True if this object can be executed, so that an y value might be computed
* for an x value. Only ExecutableObjects have that property set to true.
* @return {boolean}
*/
static executable() {
return true
}
}