core/Action.js

  1. /**
  2. * Action - The super class for all Actions
  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. * A context in Varv
  30. * @typedef {object} VarvContext
  31. * @property {string} [target] - The UUID of the target this context refers to
  32. * @property {object} [variables] - The variables currently set to any value
  33. */
  34. /**
  35. * Base class for all actions
  36. */
  37. class Action {
  38. /**
  39. * Crate a new Action
  40. * @param {string} name - The name of the action
  41. * @param {object} options - The options of the action
  42. * @param {Concept} concept - The owning concept
  43. */
  44. constructor(name, options, concept) {
  45. this.name = name;
  46. this.options = options;
  47. this.concept = concept;
  48. if(this.options == null) {
  49. this.options = {};
  50. }
  51. }
  52. /**
  53. * Applies this Action to the given contexts, returning some resulting contexts
  54. * @param {VarvContext[]} contexts
  55. * @param {object} arguments
  56. * @returns {Promise<VarvContext[]>}
  57. */
  58. async apply(contexts, actionArguments = {}) {
  59. console.warn("Always override Action.apply in subclass!");
  60. return contexts;
  61. }
  62. /**
  63. * @callback forEachCallback
  64. * @param {VarvContext} context - The context currently being looked at
  65. * @param {object} options - The current options with variables and arguments substituted
  66. * @param {number} index - The index of the current context in the context array
  67. * @returns {VarvContext|VarvContext[]}
  68. */
  69. /**
  70. * Loops through the given contexts, and calls the given callback for each, complete with substituted options and the index of the context
  71. * @param {VarvContext[]} contexts - The contexts to handle
  72. * @param {object} actionArguments - The arguments to use for this action
  73. * @param {forEachCallback} callback - The callback to call for each context
  74. * @returns {Promise<VarvContext[]>}
  75. */
  76. async forEachContext(contexts, actionArguments={}, callback) {
  77. if(arguments.length < 2) {
  78. throw new Error("forEachContext can be called either as (contexts, actionArguments, callback) or (contexts, callback)");
  79. }
  80. //If called with 2 arguments options is the callback
  81. if(arguments.length === 2) {
  82. if(typeof actionArguments !== "function") {
  83. throw new Error("forEachContext can be called either as (contexts, actionArguments, callback) or (contexts, callback)");
  84. }
  85. // noinspection JSValidateTypes
  86. callback = actionArguments;
  87. actionArguments = {};
  88. }
  89. let results = [];
  90. let index = 0;
  91. let options = await Action.lookupArguments(this.options, actionArguments);
  92. for (let context of contexts) {
  93. //Make sure to clone context, since we change it directly, thus variables might be a shared object if not.
  94. let clonedContext = Action.cloneContext(context);
  95. let optionsWithVariables = await Action.lookupVariables(options, clonedContext);
  96. let result = await callback(clonedContext, optionsWithVariables, index);
  97. if (result != null) {
  98. if (Array.isArray(result)) {
  99. result.forEach((entry) => {
  100. results.push(entry);
  101. });
  102. } else {
  103. results.push(result);
  104. }
  105. }
  106. index++;
  107. }
  108. return results;
  109. }
  110. /**
  111. * Get the default variable name, for when no variable name is supplied
  112. * @param {Action} - The action asking, the name of the action is used.
  113. * @returns {string} - The default variable name
  114. */
  115. static defaultVariableName(action) {
  116. if(action == null) {
  117. return "result";
  118. }
  119. return action.name;
  120. }
  121. /**
  122. * Sets the given variable to the given value, in the given context
  123. * @param {VarvContext} context - The context to set the variable inside
  124. * @param {string} name - The name of the variable to set
  125. * @param {any} value - The value to set
  126. */
  127. static setVariable(context, name, value) {
  128. if(context.variables == null) {
  129. context.variables = {}
  130. }
  131. if(name === "target") {
  132. console.warn("Unable to set variable target!");
  133. return;
  134. }
  135. context.variables[name] = value;
  136. }
  137. /**
  138. * Retrieve the value of the given variable from the given context
  139. * @param {VarvContext} context
  140. * @param {string} name
  141. * @returns {any}
  142. */
  143. static getVariable(context, name) {
  144. if(name === "target" && context.target != null) {
  145. //If context.target exists use that, if not, check variables as getCommonVariables might have saved a common target
  146. return context.target;
  147. }
  148. if(name === "lastTarget" && context.lastTarget != null) {
  149. //If context.lastTarget exists use that
  150. return context.lastTarget;
  151. }
  152. if(context.variables == null) {
  153. context.variables = {}
  154. }
  155. if(!context.variables.hasOwnProperty(name)) {
  156. if(name === "target") {
  157. throw new Error("Context did not contain target, and no variable target existed either!");
  158. }
  159. throw new Error("No named variable ["+name+"]");
  160. }
  161. return context.variables[name];
  162. }
  163. /**
  164. * Extract all the variables that have the same value across all contexts
  165. * @param contexts
  166. * @returns {{}}
  167. */
  168. static getCommonVariables(contexts) {
  169. let common = {};
  170. let mark = VarvPerformance.start();
  171. if(contexts.length > 0) {
  172. let testContext = contexts[0];
  173. if(testContext.variables != null) {
  174. Object.keys(testContext.variables).forEach((variableName)=>{
  175. let variableValue = testContext.variables[variableName];
  176. let keep = true;
  177. for(let otherContext of contexts) {
  178. if(otherContext.variables != null) {
  179. let otherValue = otherContext.variables[variableName];
  180. if(otherValue != null && otherValue === variableValue) {
  181. continue;
  182. }
  183. if(Array.isArray(otherValue) && Array.isArray(variableValue)) {
  184. if(otherValue.length === variableValue.length) {
  185. let arrayEqual = true;
  186. for(let i = 0; i<otherValue.length; i++) {
  187. arrayEqual = arrayEqual && otherValue[i] === variableValue[i];
  188. }
  189. if(arrayEqual) {
  190. continue;
  191. }
  192. }
  193. }
  194. }
  195. keep = false;
  196. break;
  197. }
  198. if(keep) {
  199. common[variableName] = variableValue;
  200. }
  201. });
  202. }
  203. if(testContext.target != null) {
  204. let keep = true;
  205. for(let otherContext of contexts) {
  206. if (otherContext.target != testContext.target) {
  207. keep = false;
  208. break;
  209. }
  210. }
  211. if(keep) {
  212. common["target"] = testContext.target;
  213. }
  214. }
  215. } else {
  216. if(contexts.savedVariables) {
  217. return contexts.savedVariables;
  218. }
  219. }
  220. VarvPerformance.stop("Action.getCommonVariables", mark, "#contexts "+contexts.length);
  221. return common;
  222. }
  223. static getCommonTarget(contexts) {
  224. let commonTarget = -1;
  225. contexts.forEach((context)=>{
  226. if(commonTarget === -1) {
  227. commonTarget = context.target;
  228. } else {
  229. if(commonTarget !== context.target) {
  230. commonTarget = null;
  231. }
  232. }
  233. });
  234. return commonTarget;
  235. }
  236. /**
  237. * Looks up any options that are set to an argument replacement value "@myArgumentName" and replaces it with that arguments value
  238. * @param {object} options - The options to do the replacement on
  239. * @param {object} actionArguments - The arguments to replace into the options
  240. * @returns {object} A clone of the options argument, with all replacement values replaced
  241. */
  242. static async lookupArguments(options, actionArguments) {
  243. let mark = VarvPerformance.start();
  244. const optionsClone = Action.clone(options);
  245. let regex = /@(\S+?)(?:@|\s|$)/gm;
  246. for(let parameter in optionsClone) {
  247. if(!Object.hasOwn(optionsClone, parameter)) {
  248. continue;
  249. }
  250. let value = optionsClone[parameter];
  251. optionsClone[parameter] = await Action.subParam(value, (value)=>{
  252. if(!value.includes("@")) {
  253. return value;
  254. }
  255. //Substitute any @argumentName with the value of the argument
  256. for(let match of value.matchAll(regex)) {
  257. let search = match[0].trim();
  258. let variableName = match[1];
  259. let variableValue = actionArguments[variableName];
  260. if(Action.DEBUG) {
  261. console.log("Replaced argument:", search, variableValue, actionArguments, variableName);
  262. }
  263. if(value === search) {
  264. //Single value, set it directly
  265. value = variableValue;
  266. } else {
  267. //Replace into value
  268. value = value.replace(search, variableValue);
  269. }
  270. }
  271. return value;
  272. });
  273. }
  274. VarvPerformance.stop("Action.lookupArguments", mark, {options});
  275. return optionsClone;
  276. }
  277. static async subParam(value, lookupCallback) {
  278. if (Array.isArray(value)) {
  279. for (let i = 0; i < value.length; i++) {
  280. value[i] = await Action.subParam(value[i], lookupCallback);
  281. }
  282. return value;
  283. } else if (typeof value === "object" && value != null && Object.getPrototypeOf(value) === Object.prototype) {
  284. for(let key in value) {
  285. if(Object.hasOwn(value, key)) {
  286. value[key] = await Action.subParam(value[key], lookupCallback);
  287. }
  288. }
  289. return value;
  290. } else if (typeof value === "string") {
  291. return lookupCallback(value);
  292. } else {
  293. return value;
  294. }
  295. }
  296. static clone(obj) {
  297. if(Array.isArray(obj)) {
  298. return obj.map((arrayValue)=>{
  299. return Action.clone(arrayValue);
  300. });
  301. } else if(typeof obj === "object" && obj != null && Object.getPrototypeOf(obj) === Object.prototype) {
  302. let clone = {};
  303. for (let key in obj) {
  304. if (!Object.hasOwn(obj, key)) {
  305. continue;
  306. }
  307. clone[key] = Action.clone(obj[key]);
  308. }
  309. return clone;
  310. }
  311. return obj;
  312. }
  313. /**
  314. * Look up any options that have a variable replacement value "$myVariable" and replaces it with the value of that variable.
  315. * @param {object} options
  316. * @param {VarvContext} context
  317. * @returns {object} - Returns a clone of the given options, with all replacement values replaced.
  318. */
  319. static async lookupVariables(options, context) {
  320. if(Action.DEBUG) {
  321. console.group("Looking up variables from context:", options, context);
  322. }
  323. let mark = VarvPerformance.start();
  324. const optionsClone = Action.clone(options);
  325. async function doLookup(context, variableName) {
  326. let variableValue = null;
  327. if(variableName.indexOf(".") !== -1) {
  328. let split = variableName.split(".");
  329. //concept.property lookup, not really variable
  330. let conceptName = split[0];
  331. let propertyName = split[1];
  332. let result = null;
  333. if(conceptName === "lastTarget") {
  334. result = await VarvEngine.lookupProperty(context.lastTarget, null, propertyName);
  335. } else if(conceptName === "target") {
  336. result = await VarvEngine.lookupProperty(context.target, null, propertyName);
  337. } else {
  338. result = await VarvEngine.lookupProperty(context.target, null, variableName);
  339. }
  340. if (result != null && result.target != null) {
  341. variableValue = await result.property.getValue(result.target);
  342. }
  343. } else {
  344. variableValue = Action.getVariable(context, variableName);
  345. }
  346. return variableValue;
  347. }
  348. let regex = /\$(\S+?)(?:\$|\s|$)/gm;
  349. for(let key of Object.keys(optionsClone)) {
  350. optionsClone[key] = await Action.subParam(optionsClone[key], async (value)=>{
  351. if(!value.includes("$")) {
  352. return value;
  353. }
  354. //Substitute any $VariableName with the value of the variable
  355. for(let match of value.matchAll(regex)) {
  356. let search = match[0].trim();
  357. let variableName = match[1];
  358. let variableValue = await doLookup(context, variableName);
  359. if(Action.DEBUG) {
  360. console.log("Replaced variable:", search, variableValue);
  361. }
  362. if(value === search) {
  363. //Single value, set it directly
  364. value = variableValue;
  365. } else {
  366. //Replace into value
  367. value = value.replace(search, variableValue);
  368. }
  369. }
  370. return value;
  371. });
  372. }
  373. if(Action.DEBUG) {
  374. console.groupEnd();
  375. }
  376. VarvPerformance.stop("Action.lookupVariables", mark, {context, options});
  377. //Handle special stuff here
  378. Action.substituteEvery(optionsClone, "calculate", (value)=>{
  379. return CalculateAction.evaluate(value);
  380. });
  381. return optionsClone;
  382. }
  383. static substituteEvery(input, nameToSubstitute, callback) {
  384. if(Array.isArray(input)) {
  385. for(let i = 0; i< input.length; i++) {
  386. input[i] = Action.substituteEvery(input[i], nameToSubstitute, callback);
  387. }
  388. } else if(typeof input === "object") {
  389. for(let key in input) {
  390. if(input.hasOwnProperty(key)) {
  391. if(key === nameToSubstitute) {
  392. if(Object.keys(input).length > 1) {
  393. console.warn("Substituting on something with more than 1 entry (Stuff will be lost):", input, nameToSubstitute);
  394. }
  395. let origValue = input[key];
  396. return callback(origValue);
  397. } else {
  398. input[key] = Action.substituteEvery(input[key], nameToSubstitute, callback);
  399. }
  400. }
  401. }
  402. }
  403. return input;
  404. }
  405. /**
  406. * Registers the given action as a primitive action with the given name
  407. * @param {string} name
  408. * @param {Action} action
  409. */
  410. static registerPrimitiveAction(name, action) {
  411. if(Action.DEBUG) {
  412. console.log("Registering primitive action:", name, action);
  413. }
  414. if (Action.primitiveActions.has(name)) {
  415. console.warn("Overriding primitive action: ", name);
  416. }
  417. Action.primitiveActions.set(name, action);
  418. }
  419. /**
  420. * Gets an instance of the primitive action with the given name, using the given options
  421. * @param {string} name
  422. * @param {object} options
  423. * @returns {Action}
  424. */
  425. static getPrimitiveAction(name, options, concept) {
  426. let actionClass = Action.primitiveActions.get(name);
  427. if (actionClass == null) {
  428. throw new Error("Unknown primitive action [" + name + "]");
  429. }
  430. let action = new actionClass(name, options, concept);
  431. action.isPrimitive = true;
  432. return action;
  433. }
  434. /**
  435. * Checks if a primitive action with the given name exists
  436. * @param {string} name
  437. * @returns {boolean}
  438. */
  439. static hasPrimitiveAction(name) {
  440. return Action.primitiveActions.has(name);
  441. }
  442. /**
  443. * Clones the given context. (Any non JSON serializable values in the context, will be lost)
  444. * @param {VarvContext} context
  445. * @returns {VarvContext} - The cloned context
  446. */
  447. static cloneContext(context) {
  448. let mark = VarvPerformance.start();
  449. let result = null;
  450. if(Array.isArray(context)) {
  451. result = context.map(Action.cloneContextInternal);
  452. } else {
  453. result = Action.cloneContextInternal(context);
  454. }
  455. VarvPerformance.stop("Action.cloneContext", mark);
  456. return result;
  457. }
  458. static cloneContextInternal(context) {
  459. //Move over allowed properties
  460. let preCloneObject = {};
  461. preCloneObject.variables = context.variables;
  462. preCloneObject.target = context.target;
  463. if(Action.DEBUG) {
  464. console.log("Cloning context:", context, preCloneObject);
  465. }
  466. return Action.clone(preCloneObject);
  467. }
  468. }
  469. Action.DEBUG = false;
  470. Action.primitiveActions = new Map();
  471. window.Action = Action;
  472. class ActionChain extends Action {
  473. constructor(name, options, concept) {
  474. super(name, options, concept);
  475. this.actions = [];
  476. }
  477. addAction(action) {
  478. const self = this;
  479. if(Array.isArray(action)) {
  480. action.forEach((actionElm)=>{
  481. self.actions.push(actionElm);
  482. })
  483. } else {
  484. this.actions.push(action);
  485. }
  486. }
  487. async apply(contexts, actionArguments = {}) {
  488. let currentContexts = contexts;
  489. for (let action of this.actions) {
  490. let commonVariablesBefore = Action.getCommonVariables(currentContexts);
  491. await ActionTrigger.before(action, currentContexts);
  492. let mark = VarvPerformance.start();
  493. currentContexts = await action.apply(currentContexts, actionArguments);
  494. if(action.isPrimitive) {
  495. VarvPerformance.stop("PrimitiveAction-"+action.name, mark);
  496. } else {
  497. VarvPerformance.stop("CustomAction-"+action.name, mark);
  498. }
  499. await ActionTrigger.after(action, currentContexts);
  500. if(currentContexts == null) {
  501. currentContexts = [];
  502. }
  503. if(currentContexts.length === 0) {
  504. currentContexts.savedVariables = commonVariablesBefore;
  505. }
  506. }
  507. return currentContexts;
  508. }
  509. }
  510. window.ActionChain = ActionChain;
  511. class LookupActionAction extends Action {
  512. constructor(name, options, concept) {
  513. super(name, options, concept);
  514. }
  515. async apply(contexts, actionArguments = {}) {
  516. const self = this;
  517. let optionsWithArguments = await Action.lookupArguments(this.options, actionArguments);
  518. if(optionsWithArguments.lookupActionName == null) {
  519. throw new Error("[LookupActionAction] Missing option 'lookupActionName'");
  520. }
  521. //TODO: We assume that all concepts are of the same type, when/if polymorphism is introduced this breaks
  522. let contextConcept = null;
  523. if(contexts.length > 0) {
  524. contextConcept = await VarvEngine.getConceptFromUUID(contexts[0].target);
  525. }
  526. let action = VarvEngine.lookupAction(optionsWithArguments.lookupActionName, [contextConcept, self.concept], optionsWithArguments.lookupActionArguments);
  527. if(action != null) {
  528. await ActionTrigger.before(action, contexts);
  529. let mark = VarvPerformance.start();
  530. let lookupActionResult = await action.apply(contexts, action.isPrimitive?{}:optionsWithArguments.lookupActionArguments);
  531. if(action.isPrimitive) {
  532. VarvPerformance.stop("PrimitiveAction-"+action.name, mark);
  533. } else {
  534. VarvPerformance.stop("CustomAction-"+action.name, mark);
  535. }
  536. await ActionTrigger.after(action, lookupActionResult);
  537. return lookupActionResult;
  538. }
  539. return null;
  540. }
  541. }
  542. window.LookupActionAction = LookupActionAction;
  543. class StopError extends Error {
  544. constructor(msg) {
  545. super(msg);
  546. }
  547. }
  548. window.StopError = StopError;