You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

nodes.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. /** Modified from original Node-Red source, for audio system visualization
  2. * vim: set ts=4:
  3. * Copyright 2013 IBM Corp.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. **/
  17. RED.nodes = (function() {
  18. var node_defs = {};
  19. var nodes = [];
  20. var configNodes = {};
  21. var links = [];
  22. //var defaultWorkspace;
  23. var workspaces = {};
  24. function registerType(nt,def) {
  25. node_defs[nt] = def;
  26. // TODO: too tightly coupled into palette UI
  27. RED.palette.add(nt,def);
  28. }
  29. function getID() {
  30. var str = (1+Math.random()*4294967295).toString(16);
  31. console.log("getID = " + str);
  32. return str;
  33. }
  34. function checkID(name) {
  35. var i;
  36. for (i=0;i<nodes.length;i++) {
  37. console.log("checkID, nodes[i].id = " + nodes[i].id);
  38. if (nodes[i].id == name) return true;
  39. }
  40. /*
  41. for (i in workspaces) {
  42. if (workspaces.hasOwnProperty(i)) { }
  43. }
  44. for (i in configNodes) {
  45. if (configNodes.hasOwnProperty(i)) { }
  46. }
  47. */
  48. return false;
  49. }
  50. function createUniqueCppName(n) {
  51. console.log("getUniqueCppName, n.type=" + n.type + ", n._def.shortName=" + n._def.shortName);
  52. var basename = (n._def.shortName) ? n._def.shortName : n.type.replace(/^Analog/, "");
  53. console.log("getUniqueCppName, using basename=" + basename);
  54. var count = 1;
  55. var sep = /[0-9]$/.test(basename) ? "_" : "";
  56. var name;
  57. while (1) {
  58. name = basename + sep + count;
  59. if (!checkID(name)) break;
  60. count++;
  61. }
  62. console.log("getUniqueCppName, unique name=" + name);
  63. return name;
  64. }
  65. function getType(type) {
  66. return node_defs[type];
  67. }
  68. function selectNode(name) {
  69. // window.history.pushState(null, null, window.location.protocol + "//"
  70. // + window.location.host + window.location.pathname + '?info=' + name);
  71. }
  72. function addNode(n) {
  73. if (n._def.category == "config") {
  74. configNodes[n.id] = n;
  75. RED.sidebar.config.refresh();
  76. } else {
  77. n.dirty = true;
  78. nodes.push(n);
  79. var updatedConfigNode = false;
  80. for (var d in n._def.defaults) {
  81. if (n._def.defaults.hasOwnProperty(d)) {
  82. var property = n._def.defaults[d];
  83. if (property.type) {
  84. var type = getType(property.type);
  85. if (type && type.category == "config") {
  86. var configNode = configNodes[n[d]];
  87. if (configNode) {
  88. updatedConfigNode = true;
  89. configNode.users.push(n);
  90. }
  91. }
  92. }
  93. }
  94. }
  95. if (updatedConfigNode) {
  96. RED.sidebar.config.refresh();
  97. }
  98. }
  99. }
  100. function addLink(l) {
  101. links.push(l);
  102. }
  103. /*
  104. function addConfig(c) {
  105. configNodes[c.id] = c;
  106. }
  107. */
  108. function getNode(id) {
  109. if (id in configNodes) {
  110. return configNodes[id];
  111. } else {
  112. for (var n in nodes) {
  113. if (nodes[n].id == id) {
  114. return nodes[n];
  115. }
  116. }
  117. }
  118. return null;
  119. }
  120. function removeNode(id) {
  121. var removedLinks = [];
  122. if (id in configNodes) {
  123. delete configNodes[id];
  124. RED.sidebar.config.refresh();
  125. } else {
  126. var node = getNode(id);
  127. if (node) {
  128. nodes.splice(nodes.indexOf(node),1);
  129. removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
  130. removedLinks.map(function(l) {links.splice(links.indexOf(l), 1); });
  131. }
  132. var updatedConfigNode = false;
  133. for (var d in node._def.defaults) {
  134. if (node._def.defaults.hasOwnProperty(d)) {
  135. var property = node._def.defaults[d];
  136. if (property.type) {
  137. var type = getType(property.type);
  138. if (type && type.category == "config") {
  139. var configNode = configNodes[node[d]];
  140. if (configNode) {
  141. updatedConfigNode = true;
  142. var users = configNode.users;
  143. users.splice(users.indexOf(node),1);
  144. }
  145. }
  146. }
  147. }
  148. }
  149. if (updatedConfigNode) {
  150. RED.sidebar.config.refresh();
  151. }
  152. }
  153. return removedLinks;
  154. }
  155. function removeLink(l) {
  156. var index = links.indexOf(l);
  157. if (index != -1) {
  158. links.splice(index,1);
  159. }
  160. }
  161. function refreshValidation() {
  162. for (var n=0;n<nodes.length;n++) {
  163. RED.editor.validateNode(nodes[n]);
  164. }
  165. }
  166. function addWorkspace(ws) {
  167. workspaces[ws.id] = ws;
  168. }
  169. function getWorkspace(id) {
  170. return workspaces[id];
  171. }
  172. function removeWorkspace(id) {
  173. delete workspaces[id];
  174. var removedNodes = [];
  175. var removedLinks = [];
  176. var n;
  177. for (n=0;n<nodes.length;n++) {
  178. var node = nodes[n];
  179. if (node.z == id) {
  180. removedNodes.push(node);
  181. }
  182. }
  183. for (n=0;n<removedNodes.length;n++) {
  184. var rmlinks = removeNode(removedNodes[n].id);
  185. removedLinks = removedLinks.concat(rmlinks);
  186. }
  187. return {nodes:removedNodes,links:removedLinks};
  188. }
  189. function getAllFlowNodes(node) {
  190. var visited = {};
  191. visited[node.id] = true;
  192. var nns = [node];
  193. var stack = [node];
  194. while(stack.length !== 0) {
  195. var n = stack.shift();
  196. var childLinks = links.filter(function(d) { return (d.source === n) || (d.target === n);});
  197. for (var i=0;i<childLinks.length;i++) {
  198. var child = (childLinks[i].source === n)?childLinks[i].target:childLinks[i].source;
  199. if (!visited[child.id]) {
  200. visited[child.id] = true;
  201. nns.push(child);
  202. stack.push(child);
  203. }
  204. }
  205. }
  206. return nns;
  207. }
  208. /**
  209. * Converts a node to an exportable JSON Object
  210. **/
  211. function convertNode(n, exportCreds) {
  212. exportCreds = exportCreds || false;
  213. var node = {};
  214. node.id = n.id;
  215. node.type = n.type;
  216. for (var d in n._def.defaults) {
  217. if (n._def.defaults.hasOwnProperty(d)) {
  218. node[d] = n[d];
  219. }
  220. }
  221. if(exportCreds && n.credentials) {
  222. node.credentials = {};
  223. for (var cred in n._def.credentials) {
  224. if (n._def.credentials.hasOwnProperty(cred)) {
  225. if (n.credentials[cred] != null) {
  226. node.credentials[cred] = n.credentials[cred];
  227. }
  228. }
  229. }
  230. }
  231. if (n._def.category != "config") {
  232. node.x = n.x;
  233. node.y = n.y;
  234. node.z = n.z;
  235. node.wires = [];
  236. for(var i=0;i<n.outputs;i++) {
  237. node.wires.push([]);
  238. }
  239. var wires = links.filter(function(d){return d.source === n;});
  240. for (var j=0;j<wires.length;j++) {
  241. var w = wires[j];
  242. node.wires[w.sourcePort].push(w.target.id + ":" + w.targetPort);
  243. }
  244. }
  245. return node;
  246. }
  247. /**
  248. * Converts the current node selection to an exportable JSON Object
  249. **/
  250. function createExportableNodeSet(set) {
  251. var nns = [];
  252. var exportedConfigNodes = {};
  253. for (var n=0;n<set.length;n++) {
  254. var node = set[n].n;
  255. var convertedNode = RED.nodes.convertNode(node);
  256. for (var d in node._def.defaults) {
  257. if (node._def.defaults[d].type && node[d] in configNodes) {
  258. var confNode = configNodes[node[d]];
  259. var exportable = getType(node._def.defaults[d].type).exportable;
  260. if ((exportable == null || exportable)) {
  261. if (!(node[d] in exportedConfigNodes)) {
  262. exportedConfigNodes[node[d]] = true;
  263. nns.unshift(RED.nodes.convertNode(confNode));
  264. }
  265. } else {
  266. convertedNode[d] = "";
  267. }
  268. }
  269. }
  270. nns.push(convertedNode);
  271. }
  272. return nns;
  273. }
  274. //TODO: rename this (createCompleteNodeSet)
  275. function createCompleteNodeSet() {
  276. var nns = [];
  277. var i;
  278. for (i in workspaces) {
  279. if (workspaces.hasOwnProperty(i)) {
  280. nns.push(workspaces[i]);
  281. }
  282. }
  283. for (i in configNodes) {
  284. if (configNodes.hasOwnProperty(i)) {
  285. nns.push(convertNode(configNodes[i], true));
  286. }
  287. }
  288. for (i=0;i<nodes.length;i++) {
  289. var node = nodes[i];
  290. nns.push(convertNode(node, true));
  291. }
  292. return nns;
  293. }
  294. /**
  295. * Parses the input string which contains copied code from the Arduino IDE, scans the
  296. * nodes and connections and forms them into a JSON representation which will be
  297. * returned as string.
  298. *
  299. * So the result may directly imported in the localStorage or the import dialog.
  300. */
  301. function cppToJSON(newNodesStr) {
  302. var newNodes = [];
  303. var skipped = [];
  304. var cables = [];
  305. var words = [];
  306. var lastY = 0;
  307. const NODE_COMMENT = "//";
  308. const NODE_AC = "AudioConnection";
  309. var parseLine = function(line) {
  310. var parts = line.match(/^(\S+)\s(.*)/);
  311. if (parts == null) {
  312. return
  313. }
  314. parts = parts.slice(1);
  315. if (parts == null || parts.length <= 1) {
  316. return
  317. }
  318. var type = $.trim(parts[0]);
  319. line = $.trim(parts[1]) + " ";
  320. var name = "";
  321. var coords = [0, 0];
  322. var conn = [];
  323. parts = line.match(/^([^;]{0,});(.*)/);
  324. if (parts && parts.length >= 2) {
  325. parts = parts.slice(1);
  326. if (parts && parts.length >= 1) {
  327. name = $.trim(parts[0]);
  328. coords = $.trim(parts[1]);
  329. parts = coords.match(/^([^\/]{0,})\/\/xy=(.*)/);
  330. if (parts) {
  331. parts = parts.slice(1);
  332. coords = $.trim(parts[1]).split(",");
  333. }
  334. }
  335. }
  336. if (type == NODE_AC) {
  337. parts = name.match(/^([^\(]*\()([^\)]*)(.*)/);
  338. if (parts && parts.length > 1) {
  339. conn = $.trim(parts[2]).split(",");
  340. cables.push(conn);
  341. }
  342. } else if (type == NODE_COMMENT) {
  343. // do nothing
  344. } else {
  345. var names = [];
  346. var yPos = [];
  347. if (name.indexOf(",") >= 0) {
  348. names = name.split(",");
  349. } else {
  350. names.push(name);
  351. }
  352. for (var n = 0; n < names.length; n++) {
  353. name = names[n].trim();
  354. var gap = 10;
  355. var def = node_defs[type];
  356. var dW = Math.max(RED.view.defaults.width, RED.view.calculateTextWidth(name) + (def.inputs > 0 ? 7 : 0));
  357. var dH = Math.max(RED.view.defaults.height,(Math.max(def.outputs, def.inputs)||0) * 15);
  358. var newX = parseInt(coords ? coords[0] : 0);
  359. var newY = parseInt(coords ? coords[1] : 0);
  360. //newY = newY == 0 ? lastY + (dH * n) + gap : newY;
  361. //lastY = Math.max(lastY, newY);
  362. var node = new Object({"order": n, "id": name, "type": type, "x": newX, "y": newY, "z": 0, "wires": []});
  363. // first solution: skip existing id
  364. if (RED.nodes.node(node.id) !== null) {
  365. skipped.push(node.id);
  366. } else {
  367. newNodes.push(node);
  368. }
  369. }
  370. }
  371. };
  372. var findNode = function(name) {
  373. var len = newNodes.length;
  374. for (var key = 0; key < len; key++) {
  375. if (newNodes[key].id == name) {
  376. return newNodes[key];
  377. }
  378. }
  379. };
  380. var linkCables = function(cables) {
  381. $.each(cables, function(i, item) {
  382. var conn = item;
  383. // when there are only two entries in the array, there
  384. // is only one output to connect to one input, so we have
  385. // to extend the array with the appropriate index "0" for
  386. // both parst (in and out)
  387. if (conn.length == 2) {
  388. conn[2] = conn[1];
  389. conn[1] = conn[3] = 0;
  390. }
  391. // now we assign the outputs (marked by the "idx" of the array)
  392. // to the inputs describend by text
  393. var currNode = findNode($.trim(conn[0]));
  394. var idx = parseInt($.trim(conn[1]));
  395. if (currNode) {
  396. if ($.trim(conn[2]) != "" && $.trim(conn[3]) != "") {
  397. var wire = $.trim(conn[2]) + ":" + $.trim(conn[3]);
  398. var tmp = currNode.wires[idx] ? currNode.wires[idx] : [];
  399. tmp.push(wire);
  400. currNode.wires[idx] = tmp;
  401. }
  402. }
  403. });
  404. };
  405. var traverseLines = function(raw) {
  406. var lines = raw.split("\n");
  407. for (var i = 0; i < lines.length; i++) {
  408. var line = lines[i].trim();
  409. // we reached the setup or loop part ...
  410. var pattSu = new RegExp(/\s*void\s*setup\s*\(\s*\).*/);
  411. var pattLo = new RegExp(/\s*void\s*loop\s*\(\s*\).*/);
  412. if (pattSu.test(line) || pattLo.test(line)) {
  413. break;
  414. }
  415. // we need at least two parts to examine
  416. var parts = line.match(/^(\S+)\s(.*)/);
  417. if (parts == null || parts.length == 1) {
  418. continue;
  419. }
  420. // ... and it has to end with an semikolon ...
  421. var pattSe = new RegExp(/.*;$/);
  422. if (pattSe.test(line)) {
  423. var word = parts[1].trim();
  424. if (words.indexOf(word) >= 0) {
  425. parseLine(line);
  426. }
  427. }
  428. }
  429. };
  430. /*
  431. var readCode = function() {
  432. var fileImport = $("#importInput")[0];
  433. var regex = /^([a-zA-Z0-9\s_\\.\-:])+(.ino|.txt)$/;
  434. if (regex.test(fileImport.value.toLowerCase())) {
  435. if (typeof (FileReader) != "undefined") {
  436. var reader = new FileReader();
  437. $(reader).on("load", function (e) {
  438. });
  439. reader.readAsText(fileImport.files[0]);
  440. } else {
  441. alert("This browser does not support HTML5.");
  442. }
  443. } else {
  444. alert("Please upload a valid INO or text file.");
  445. }
  446. };
  447. */
  448. function startImport() {
  449. words = Array(NODE_AC);
  450. $.each(node_defs, function (key, obj) {
  451. words.push(key);
  452. });
  453. traverseLines(newNodesStr);
  454. linkCables(cables);
  455. }
  456. startImport();
  457. return {
  458. count: newNodes.length,
  459. skipped: skipped.length,
  460. data: newNodes.length > 0 ? JSON.stringify(newNodes) : ""
  461. };
  462. }
  463. function importNodes(newNodesObj,createNewIds) {
  464. try {
  465. var i;
  466. var n;
  467. var newNodes;
  468. if (typeof newNodesObj === "string") {
  469. if (newNodesObj === "") {
  470. return;
  471. }
  472. newNodes = JSON.parse(newNodesObj);
  473. } else {
  474. newNodes = newNodesObj;
  475. }
  476. if (!$.isArray(newNodes)) {
  477. newNodes = [newNodes];
  478. }
  479. var unknownTypes = [];
  480. for (i=0;i<newNodes.length;i++) {
  481. n = newNodes[i];
  482. // TODO: remove workspace in next release+1
  483. if (n.type != "workspace" && n.type != "tab" && !getType(n.type)) {
  484. // TODO: get this UI thing out of here! (see below as well)
  485. n.name = n.type;
  486. n.type = "unknown";
  487. if (unknownTypes.indexOf(n.name)==-1) {
  488. unknownTypes.push(n.name);
  489. }
  490. if (n.x == null && n.y == null) {
  491. // config node - remove it
  492. newNodes.splice(i,1);
  493. i--;
  494. }
  495. }
  496. }
  497. /*
  498. if (unknownTypes.length > 0) {
  499. var typeList = "<ul><li>"+unknownTypes.join("</li><li>")+"</li></ul>";
  500. var type = "type"+(unknownTypes.length > 1?"s":"");
  501. RED.notify("<strong>Imported unrecognised "+type+":</strong>"+typeList,"error",false,10000);
  502. //"DO NOT DEPLOY while in this state.<br/>Either, add missing types to Node-RED, restart and then reload page,<br/>or delete unknown "+n.name+", rewire as required, and then deploy.","error");
  503. }
  504. for (i=0;i<newNodes.length;i++) {
  505. n = newNodes[i];
  506. // TODO: remove workspace in next release+1
  507. if (n.type === "workspace" || n.type === "tab") {
  508. if (n.type === "workspace") {
  509. n.type = "tab";
  510. }
  511. if (defaultWorkspace == null) {
  512. defaultWorkspace = n;
  513. }
  514. addWorkspace(n);
  515. RED.view.addWorkspace(n);
  516. }
  517. }
  518. if (defaultWorkspace == null) {
  519. defaultWorkspace = { type:"tab", id:getID(), label:"Sheet 1" };
  520. addWorkspace(defaultWorkspace);
  521. RED.view.addWorkspace(defaultWorkspace);
  522. }
  523. */
  524. var node_map = {};
  525. var new_nodes = [];
  526. var new_links = [];
  527. for (i=0;i<newNodes.length;i++) {
  528. n = newNodes[i];
  529. // TODO: remove workspace in next release+1
  530. if (n.type !== "workspace" && n.type !== "tab") {
  531. var def = getType(n.type);
  532. if (def && def.category == "config") {
  533. if (!RED.nodes.node(n.id)) {
  534. var configNode = {id:n.id,type:n.type,users:[]};
  535. for (var d in def.defaults) {
  536. if (def.defaults.hasOwnProperty(d)) {
  537. configNode[d] = n[d];
  538. }
  539. }
  540. configNode.label = def.label;
  541. configNode._def = def;
  542. RED.nodes.add(configNode);
  543. }
  544. } else {
  545. var node = {x:n.x,y:n.y,z:n.z,type:0,wires:n.wires,changed:false};
  546. if (createNewIds) {
  547. node.z = RED.view.getWorkspace();
  548. node.id = getID();
  549. } else {
  550. node.id = n.id;
  551. if (node.z == null || !workspaces[node.z]) {
  552. node.z = RED.view.getWorkspace();
  553. }
  554. }
  555. node.type = n.type;
  556. node._def = def;
  557. if (!node._def) {
  558. node._def = {
  559. color:"#fee",
  560. defaults: {},
  561. label: "unknown: "+n.type,
  562. labelStyle: "node_label_italic",
  563. outputs: n.outputs||n.wires.length
  564. }
  565. }
  566. node.outputs = n.outputs||node._def.outputs;
  567. for (var d2 in node._def.defaults) {
  568. if (node._def.defaults.hasOwnProperty(d2)) {
  569. node[d2] = n[d2];
  570. }
  571. }
  572. addNode(node);
  573. RED.editor.validateNode(node);
  574. node_map[n.id] = node;
  575. new_nodes.push(node);
  576. }
  577. }
  578. }
  579. for (i=0;i<new_nodes.length;i++) {
  580. n = new_nodes[i];
  581. for (var w1=0;w1<n.wires.length;w1++) {
  582. var wires = (n.wires[w1] instanceof Array)?n.wires[w1]:[n.wires[w1]];
  583. for (var w2=0;w2<wires.length;w2++) {
  584. var parts = wires[w2].split(":");
  585. if (parts.length == 2 && parts[0] in node_map) {
  586. var dst = node_map[parts[0]];
  587. var link = {source:n,sourcePort:w1,target:dst,targetPort:parts[1]};
  588. addLink(link);
  589. new_links.push(link);
  590. }
  591. }
  592. }
  593. delete n.wires;
  594. }
  595. return [new_nodes,new_links];
  596. } catch(error) {
  597. //TODO: get this UI thing out of here! (see above as well)
  598. RED.notify("<strong>Error</strong>: "+error,"error");
  599. return null;
  600. }
  601. }
  602. return {
  603. registerType: registerType,
  604. getType: getType,
  605. convertNode: convertNode,
  606. selectNode: selectNode,
  607. add: addNode,
  608. addLink: addLink,
  609. remove: removeNode,
  610. removeLink: removeLink,
  611. addWorkspace: addWorkspace,
  612. removeWorkspace: removeWorkspace,
  613. workspace: getWorkspace,
  614. eachNode: function(cb) {
  615. for (var n=0;n<nodes.length;n++) {
  616. cb(nodes[n]);
  617. }
  618. },
  619. eachLink: function(cb) {
  620. for (var l=0;l<links.length;l++) {
  621. cb(links[l]);
  622. }
  623. },
  624. eachConfig: function(cb) {
  625. for (var id in configNodes) {
  626. if (configNodes.hasOwnProperty(id)) {
  627. cb(configNodes[id]);
  628. }
  629. }
  630. },
  631. node: getNode,
  632. cppToJSON: cppToJSON,
  633. import: importNodes,
  634. refreshValidation: refreshValidation,
  635. getAllFlowNodes: getAllFlowNodes,
  636. createExportableNodeSet: createExportableNodeSet,
  637. createCompleteNodeSet: createCompleteNodeSet,
  638. id: getID,
  639. cppName: createUniqueCppName,
  640. nodes: nodes, // TODO: exposed for d3 vis
  641. links: links // TODO: exposed for d3 vis
  642. };
  643. })();