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.

730 line
19KB

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