actions/MathActions.js

  1. /**
  2. * MathActions - Actions that calculate math
  3. *
  4. * This code is licensed under the MIT License (MIT).
  5. *
  6. * Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI,
  7. * Center for Advanced Visualization and Interaction, Aarhus University
  8. *
  9. * Permission is hereby granted, free of charge, to any person obtaining a copy
  10. * of this software and associated documentation files (the “Software”), to deal
  11. * in the Software without restriction, including without limitation the rights
  12. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. * copies of the Software, and to permit persons to whom the Software is
  14. * furnished to do so, subject to the following conditions:
  15. *
  16. * The above copyright notice and this permission notice shall be included in
  17. * all copies or substantial portions of the Software.
  18. *
  19. * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. * THE SOFTWARE.
  26. *
  27. */
  28. /**
  29. * Actions that do math
  30. * @namespace MathActions
  31. */
  32. class IncrementDecrementAction extends Action {
  33. constructor(name, decrement, options, concept) {
  34. // Handle string shorthand
  35. if(typeof options === "string"){
  36. options = {
  37. property: options
  38. }
  39. }
  40. const defaultOptions = {
  41. by: 1
  42. }
  43. super(name, Object.assign({}, defaultOptions, options), concept);
  44. this.decrement = decrement;
  45. }
  46. /**
  47. * @param {VarvContext[]} context
  48. * @returns {Promise<VarvContext[]>}
  49. */
  50. async apply(contexts, actionArguments = {}) {
  51. const self = this;
  52. if(this.options.property == null && this.options.variable == null) {
  53. throw new Error("Either 'property' or 'variable' needs to be set for action 'increment'");
  54. }
  55. return this.forEachContext(contexts, actionArguments, async (context, options)=>{
  56. if(options.property != null) {
  57. const lookup = await VarvEngine.lookupProperty(context.target, self.concept, options.property);
  58. if(lookup == null) {
  59. throw new Error("No property ["+options.of.property+"] found");
  60. }
  61. const concept = lookup.concept;
  62. const property = lookup.property;
  63. const target = lookup.target;
  64. let currentValue = await property.getValue(target);
  65. if (this.decrement) {
  66. currentValue -= options.by;
  67. } else {
  68. currentValue += options.by;
  69. }
  70. await property.setValue(target, currentValue);
  71. } else if(options.variable != null) {
  72. let currentValue = Action.getVariable(context, options.variable);
  73. if (this.decrement) {
  74. currentValue -= options.by;
  75. } else {
  76. currentValue += options.by;
  77. }
  78. Action.setVariable(context, options.variable, currentValue);
  79. }
  80. return context;
  81. });
  82. }
  83. }
  84. /**
  85. * An action "increment" that increments a number property or variable
  86. * @memberOf MathActions
  87. * @example
  88. * // Increment the property by 1
  89. * {
  90. * "increment": {
  91. * "property": "myNumberProperty"
  92. * }
  93. * }
  94. *
  95. * @example
  96. * // Increment the property by 2
  97. * {
  98. * "increment": {
  99. * "property": "myNumberProperty",
  100. * "by": 2
  101. * }
  102. * }
  103. *
  104. * @example
  105. * // Increment the property by 1, (Shorthand version)
  106. * {
  107. * "increment": "myNumberProperty"
  108. * }
  109. *
  110. * @example
  111. * // Increment the variable by 2
  112. * {
  113. * "increment": {
  114. * "variable": "myNumberVariable",
  115. * "by": 2
  116. * }
  117. * }
  118. */
  119. class IncrementAction extends IncrementDecrementAction {
  120. static options() {
  121. return {
  122. "$inc": "enumValue[property,variable]",
  123. "by": "number%1"
  124. }
  125. }
  126. constructor(name, options, concept) {
  127. super(name, false, options, concept);
  128. }
  129. }
  130. Action.registerPrimitiveAction("increment", IncrementAction);
  131. window.IncrementAction = IncrementAction;
  132. /**
  133. * An action "decrement" that decrements a number property or variable
  134. * @memberOf MathActions
  135. * @example
  136. * // Decrement the property by 1
  137. * {
  138. * "decrement": {
  139. * "property": "myNumberProperty"
  140. * }
  141. * }
  142. *
  143. * @example
  144. * // Decrement the property by 2
  145. * {
  146. * "decrement": {
  147. * "property": "myNumberProperty",
  148. * "by": 2
  149. * }
  150. * }
  151. *
  152. * @example
  153. * // Decrement the property by 1, (Shorthand version)
  154. * {
  155. * "decrement": "myNumberProperty"
  156. * }
  157. *
  158. * @example
  159. * // Decrement the variable by 2
  160. * {
  161. * "decrement": {
  162. * "variable": "myNumberVariable",
  163. * "by": 2
  164. * }
  165. * }
  166. */
  167. class DecrementAction extends IncrementDecrementAction {
  168. static options() {
  169. return {
  170. "$dec": "enumValue[property,variable]",
  171. "by": "number%1"
  172. }
  173. }
  174. constructor(name, options, concept) {
  175. super(name, true, options, concept);
  176. }
  177. }
  178. Action.registerPrimitiveAction("decrement", DecrementAction);
  179. window.DecrementAction = DecrementAction;
  180. /**
  181. * An action 'calculate' that calculates a given math expression and sets a variable with the result
  182. * @memberOf MathActions
  183. * @example
  184. * //Shorthand, calculates and sets result in variable 'calculate'
  185. * {
  186. * "calculate": "42 + 60 + $myVariableName$
  187. * }
  188. *
  189. * @example
  190. * {
  191. * "calculate": {
  192. * "expression": "sqrt(2) * sqrt(2)",
  193. * "as": "myResultVariableName"
  194. * }
  195. * }
  196. */
  197. class CalculateAction extends Action {
  198. static options() {
  199. return {
  200. "expression": "string",
  201. "as": "@string"
  202. }
  203. }
  204. constructor(name, options, concept) {
  205. //Shorthand
  206. if(typeof options === "string") {
  207. options = {
  208. expression: options
  209. }
  210. }
  211. if(Array.isArray((options))) {
  212. options = {
  213. expression: options.join(";")
  214. }
  215. }
  216. super(name, options, concept);
  217. }
  218. static evaluate(expression) {
  219. let result = math.evaluate(expression);
  220. if (typeof result === "object" && Array.isArray(result.entries) && result.entries.length>0){
  221. // Collapse to single value if parser switched to multi-expression evaluation mode
  222. result = result.entries[0];
  223. }
  224. return result;
  225. }
  226. async apply(contexts, actionArguments) {
  227. const self = this;
  228. return this.forEachContext(contexts, actionArguments, async (context, options)=>{
  229. let resultName = Action.defaultVariableName(self);
  230. if(options.as != null) {
  231. resultName = options.as;
  232. }
  233. let result = CalculateAction.evaluate(options.expression);
  234. Action.setVariable(context, resultName, result);
  235. return context;
  236. });
  237. }
  238. }
  239. Action.registerPrimitiveAction("calculate", CalculateAction);
  240. window.CalculateAction = CalculateAction;
  241. /**
  242. * An action 'random' that generates a random number from within a range. Both minimum and maximum are inclusive
  243. *
  244. * If no range is specified. 0 - Number.MAX_SAFE_INTEGER (2^53 -1) is used as range.
  245. * @memberOf MathActions
  246. * @example
  247. * //Shorthand, generates a random number between 0 and 10, saves the result in the variable named "random"
  248. * {
  249. * "random": [0, 10]
  250. * }
  251. *
  252. * @example
  253. * //Generate a random integer between 0 and 10, and save the result in the variable "myResultVariableName"
  254. * {
  255. * "random": {
  256. * "range": [0, 10],
  257. * "as": "myResultVariableName"
  258. * }
  259. * }
  260. *
  261. * @example
  262. * //Generate a random float number between 0 and 10, and save the result in the variable "myResultVariableName"
  263. * {
  264. * "random": {
  265. * "range": [0, 10],
  266. * "float": true,
  267. * "as": "myResultVariableName"
  268. * }
  269. * }
  270. */
  271. class RandomAction extends Action {
  272. static options() {
  273. return {
  274. "range": "range",
  275. "float": "boolean%false",
  276. "as": "@string"
  277. }
  278. }
  279. constructor(name, options, concept) {
  280. //Shorthand
  281. if(Array.isArray(options)) {
  282. options = {
  283. range: options
  284. }
  285. }
  286. super(name, options, concept);
  287. }
  288. async apply(contexts, actionArguments) {
  289. const self = this;
  290. function getRandomInt(min, max) {
  291. min = Math.ceil(min);
  292. max = Math.floor(max);
  293. return Math.floor(Math.random() * (max - min + 1) + min);
  294. }
  295. function getRandomArbitrary(min, max) {
  296. if(max < Number.MAX_VALUE) {
  297. //Upper bound is off by the smallest possible value
  298. //make it inclusive by incrementing by the smallest value
  299. max += Number.MIN_VALUE;
  300. }
  301. return Math.random() * (max - min) + min;
  302. }
  303. return this.forEachContext(contexts, actionArguments, async (context, options)=>{
  304. let range = options.range;
  305. if(range == null) {
  306. range = [0, Number.MAX_SAFE_INTEGER]
  307. }
  308. let randomNumber = Number.NaN;
  309. if(options.float) {
  310. randomNumber = getRandomArbitrary(range[0], range[1]);
  311. } else {
  312. randomNumber = getRandomInt(range[0], range[1]);
  313. }
  314. let variableName = Action.defaultVariableName(this);
  315. if(options.as != null) {
  316. variableName = options.as;
  317. }
  318. Action.setVariable(context, variableName, randomNumber);
  319. return context;
  320. });
  321. }
  322. }
  323. Action.registerPrimitiveAction("random", RandomAction);
  324. window.RandomAction = RandomAction;