Simple editor to create custom node network graphs aka ontologies
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.

ontology.java 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. import processing.core.*;
  2. import processing.data.*;
  3. import processing.event.*;
  4. import processing.opengl.*;
  5. import java.util.HashMap;
  6. import java.util.ArrayList;
  7. import java.io.File;
  8. import java.io.BufferedReader;
  9. import java.io.PrintWriter;
  10. import java.io.InputStream;
  11. import java.io.OutputStream;
  12. import java.io.IOException;
  13. public class ontology extends PApplet {
  14. Node nodes[] = new Node[1000];
  15. Link links[] = new Link[6000];
  16. int textSize = 14;
  17. boolean darkMode = true;
  18. public void setup() {
  19. surface.setResizable(true);
  20. //frame.setResizable(true);
  21. textSize(textSize);
  22. initButtons();
  23. /**
  24. * When saving, a copy of the save-file is stored locally which is opened when starting the program
  25. */
  26. try {
  27. loadCSV(dataPath("save.csv"));
  28. }
  29. catch(Exception e) {
  30. println("No File found, starting blank");
  31. }
  32. }
  33. public void draw() {
  34. background(darkMode? 0 : 255);
  35. /**
  36. * Update and display links
  37. */
  38. for (int i = 0; i < linkCount; i++) {
  39. if (links[i].active) {
  40. links[i].update();
  41. links[i].display();
  42. }
  43. }
  44. /**
  45. * Update and display nodes
  46. */
  47. for (int i = 0; i < nodeCount; i++) {
  48. if (!nodes[i].deleted) {
  49. nodes[i].update();
  50. nodes[i].display();
  51. }
  52. }
  53. /**
  54. * Update and display ui, hide via exporting-flag for easy image-saving
  55. * Not all buttons always show, that's why this isn't in a loop
  56. */
  57. if (!exporting) {
  58. textSize(14);
  59. rectMode(CORNER);
  60. textAlign(CORNER);
  61. buttons[0].display();
  62. if (i_selectedNode != -1) buttons[1].display();
  63. if (menuOpen) {
  64. buttons[2].display();
  65. buttons[3].display();
  66. buttons[4].display();
  67. buttons[5].display();
  68. buttons[6].display();
  69. }
  70. buttons[7].display();
  71. rectMode(CENTER);
  72. textAlign(CENTER);
  73. textSize(textSize);
  74. } else {
  75. save(savePNGpath);
  76. exporting = false;
  77. }
  78. /**
  79. * Pull "wire" out of in/outlet before creating a link (see mouseReleased in ui)
  80. */
  81. if (dragLink) {
  82. stroke(darkMode? 255:0);
  83. strokeWeight(2);
  84. if (dragOriginLet == -1) {
  85. line(nodes[dragOriginId].inletCenter.x, nodes[dragOriginId].inletCenter.y, mouseX, mouseY);
  86. } else if (dragOriginLet == 1) {
  87. line(nodes[dragOriginId].outletCenter.x, nodes[dragOriginId].outletCenter.y, mouseX, mouseY);
  88. }
  89. }
  90. }
  91. public void deleteAll() {
  92. for (int i = 0; i < nodeCount; i++) {
  93. //nodes[i].deleted = true;
  94. i_selectedNode = i;
  95. deleteSelectedNode();
  96. }
  97. nodeCount = 0;
  98. for (int i = 0; i < linkCount; i++) {
  99. links[i].active = false;
  100. }
  101. linkCount = 0;
  102. }
  103. int buttonCount = 8;
  104. int i_buttonId;
  105. public void initButtons() {
  106. buttons[0] = new Button(72, 10, "Add Node");
  107. buttons[1] = new Button(164, 10, "Delete Node");
  108. buttons[2] = new Button(10, 50, "Dark/Bright Mode");
  109. buttons[3] = new Button(10, 90, "Save");
  110. buttons[4] = new Button(10, 130, "Open");
  111. buttons[5] = new Button(10, 170, "Export Image");
  112. buttons[6] = new Button(10, 210, "Empty Workspace");
  113. buttons[7] = new Button(10, 10, "Menu");
  114. /* buttons[0] = new Button(10, 10, "Node Hinzufügen");
  115. buttons[1] = new Button(155, 10, "Löschen");
  116. buttons[2] = new Button(10, 50, "Farben umkehren");
  117. buttons[3] = new Button(10, 90, "Speichern");
  118. buttons[4] = new Button(10, 130, "Öffnen");
  119. buttons[5] = new Button(10, 170, "Bild Exportieren");
  120. buttons[6] = new Button(10, 210, "Arbeitsfläche leeren");
  121. buttons[7] = new Button(10, 210, "Menü");*/
  122. }
  123. public void buttonFunctions(int functionID) {
  124. switch (functionID) {
  125. case(0):
  126. addNode(PApplet.parseInt(random(50, width-50)), PApplet.parseInt(random(30, height-150)), "");
  127. i_selectedNode = nodeCount-1;
  128. break;
  129. case(1):
  130. deleteSelectedNode();
  131. break;
  132. case(2):
  133. darkMode = !darkMode;
  134. break;
  135. case(3):
  136. saveCSV(dataPath("save.csv"));
  137. selectOutput("Where to save .csv file to?", "saveCSVFile");
  138. break;
  139. case(4):
  140. selectInput("Select csv File", "loadCSVFile");
  141. break;
  142. case(5):
  143. selectOutput("Where to export .png image file to?", "savePNGFile");
  144. break;
  145. case(6):
  146. deleteAll();
  147. break;
  148. case(7):
  149. menu();
  150. break;
  151. default:
  152. break;
  153. }
  154. }
  155. Button buttons[] = new Button[buttonCount];
  156. class Button {
  157. int id, x, y, w, h;
  158. String label;
  159. public boolean hover() {
  160. return (mouseX > x && mouseX < x+w && mouseY > y && mouseY < y+h) ? true : false;
  161. }
  162. public void click() {
  163. buttonFunctions(id);
  164. }
  165. Button(int x_, int y_, String label_) {
  166. id = i_buttonId;
  167. i_buttonId++;
  168. x = x_;
  169. y = y_ ;
  170. label = label_;
  171. w = PApplet.parseInt(textWidth(label))+18;
  172. h = 32;
  173. }
  174. public void display() {
  175. stroke(0);
  176. strokeWeight(1);
  177. fill(255, 127);
  178. rect(x, y, w, h, 4);
  179. fill(0);
  180. text(label, x+9, y+h/2+5);
  181. }
  182. }
  183. boolean menuOpen;
  184. public void menu() {
  185. if (menuOpen) {
  186. } else {
  187. }
  188. menuOpen = !menuOpen;
  189. }
  190. int linkCount;
  191. boolean dragLink; //state of dragging a link
  192. int dragOriginLet = 0; //dragging from inlet: -1. dragging from outlet: 1.
  193. int dragOriginId = -1; //which nodes out- / inlet did user click on?
  194. public void Link(int parentNode, int targetNode) {
  195. /**
  196. * Check if user is adding or removing a link
  197. */
  198. int exists = -1;
  199. for (int i = 0; i < linkCount; i++) {
  200. if (links[i].parentNode == parentNode && links[i].targetNode == targetNode) {
  201. exists = i;
  202. break;
  203. }
  204. }
  205. if (exists != -1) {
  206. /**
  207. * !active state is pretty much the same as deleted state that the node-class comes with
  208. */
  209. links[exists].active = !links[exists].active;
  210. } else {
  211. links[linkCount] = new Link(parentNode, targetNode);
  212. linkCount++;
  213. }
  214. }
  215. class Link {
  216. int parentNode, targetNode;
  217. boolean active;
  218. PVector link = new PVector(0, 0);
  219. Link(int parentNode_, int targetNode_) {
  220. parentNode = parentNode_;
  221. targetNode = targetNode_;
  222. active = true;
  223. }
  224. /**
  225. * This could be an event that only gets called when nodes are moved or resized.
  226. */
  227. public void update() {
  228. link.x = nodes[targetNode].inletCenter.x - nodes[parentNode].outletCenter.x;
  229. link.y = nodes[targetNode].inletCenter.y - nodes[parentNode].outletCenter.y;
  230. }
  231. public void display() {
  232. stroke(darkMode? 255 : 0);
  233. strokeWeight(2);
  234. pushMatrix();
  235. translate(nodes[parentNode].outletCenter.x, nodes[parentNode].outletCenter.y);
  236. line(0, 0, link.x, link.y);
  237. translate(-nodes[parentNode].outletCenter.x+(nodes[parentNode].outletCenter.x+nodes[targetNode].inletCenter.x)/2, -nodes[parentNode].outletCenter.y+(nodes[parentNode].outletCenter.y+nodes[targetNode].inletCenter.y)/2);
  238. rotate(link.heading());
  239. fill(darkMode? 0 : 255);
  240. triangle(0, textSize/2, textSize, 0, 0, -textSize/2);
  241. popMatrix();
  242. }
  243. }
  244. int nodeCount;
  245. int i_selectedNode = -1;
  246. int padding = textSize/3*2; //Padding between text and surrounding box
  247. int letTolerance = 10; //Widens the clickable areas of in/outlets a bit
  248. public void addNode(int x, int y, String title) {
  249. nodes[nodeCount] = new Node(x, y, title);
  250. nodeCount++;
  251. }
  252. public void deselect() {
  253. i_selectedNode = -1;
  254. }
  255. public void deleteSelectedNode() { //Seperate from class for garbage collector reasons
  256. if (i_selectedNode != -1) {
  257. for (int i = 0; i < linkCount; i++) {
  258. if (links[i].targetNode == i_selectedNode || links[i].parentNode == i_selectedNode) {
  259. links[i].active = false;
  260. }
  261. }
  262. nodes[i_selectedNode].deleted = true;
  263. i_selectedNode = -1;
  264. }
  265. }
  266. class Node {
  267. String title;
  268. int id;
  269. int inlets = 2;
  270. int outlets = 2;
  271. int x, y;
  272. int h = textSize+2*padding;
  273. int w = textSize+2*padding;
  274. PVector dragMouseStartPoint = new PVector(0, 0);
  275. PVector outletCenter = new PVector(0, 0);
  276. PVector inletCenter = new PVector(0, 0);
  277. boolean deleted;
  278. boolean drag;
  279. public boolean hover() {
  280. if (mouseX > x-w/2 && mouseX < x+w/2 && mouseY > y-h/2 && mouseY < y+h/2) {
  281. return true;
  282. } else {
  283. return false;
  284. }
  285. }
  286. public boolean hoverOutlet() {
  287. if (mouseX > x-(textSize+padding)/2-letTolerance && mouseX < x+(textSize+padding)/2+letTolerance && mouseY > y-padding-textSize/2-(textSize+padding)/2-letTolerance && mouseY < y-padding-textSize/2) {
  288. return true;
  289. } else {
  290. return false;
  291. }
  292. }
  293. public boolean hoverInlet() {
  294. if (mouseX > x-(textSize+padding)/2-letTolerance && mouseX < x+(textSize+padding)/2+letTolerance && mouseY > y+padding+textSize/2 && mouseY < y+padding+textSize/2+(textSize+padding)/2+letTolerance) {
  295. return true;
  296. } else {
  297. return false;
  298. }
  299. }
  300. Node(int x_, int y_, String title_) {
  301. id = nodeCount;
  302. title = title_;
  303. x = x_;
  304. y = y_;
  305. setVectors();
  306. refreshSize();
  307. }
  308. public void setVectors() {
  309. outletCenter.x = x;
  310. outletCenter.y = y-(padding+textSize/2)*1.3f;
  311. inletCenter.x = x;
  312. inletCenter.y = y+(padding+textSize/2)*1.3f;
  313. }
  314. /**
  315. * Called when changing nodes title or when zooming via mouse-wheel
  316. */
  317. public void refreshSize() {
  318. w = PApplet.parseInt(textWidth(title))+2*padding;
  319. h = textSize+2*padding;
  320. }
  321. /**
  322. * Update-function is really only necessary when dragging the node
  323. */
  324. public void update() {
  325. if (drag) {
  326. x -= dragMouseStartPoint.x - mouseX;
  327. dragMouseStartPoint.x = mouseX;
  328. y -= dragMouseStartPoint.y - mouseY;
  329. dragMouseStartPoint.y = mouseY;
  330. setVectors();
  331. }
  332. }
  333. public void display() {
  334. /**
  335. * Selected nodes border
  336. */
  337. if (i_selectedNode == id) {
  338. noStroke();
  339. fill(150, 100);
  340. rect(x, y, w+20, h+20, 4);
  341. }
  342. /**
  343. * Inlet & Outlet
  344. */
  345. stroke(255, darkMode? 255 : 0);
  346. strokeWeight(1);
  347. fill(255, 0, 0);
  348. ellipse(x, y-padding-textSize/2, textSize+padding, textSize+padding);
  349. fill(0);
  350. ellipse(x, y+padding+textSize/2, textSize+padding, textSize+padding);
  351. /**
  352. * Box & Title
  353. */
  354. stroke(127);
  355. strokeWeight(2);
  356. fill(50);
  357. rect(x, y, w, h, 4);
  358. fill(255);
  359. text(title, x, y+textSize/3);
  360. }
  361. }
  362. /**
  363. * Saving and loading is a messy hack!
  364. * You'll get a blank line in your save file for each deleted object.
  365. * That's because of my lack of skills of dodging the garbage collector.
  366. * I didn't loose data to corruption yet.
  367. * Don't touch the save files if you don't want to mess them up!
  368. **/
  369. boolean exporting;
  370. String savePNGpath = dataPath("save.png");
  371. public void savePNGFile(File selection) {
  372. savePNGpath = selection.getPath();
  373. exporting = true;
  374. }
  375. public void saveCSVFile(File selection) {
  376. saveCSV(selection.getPath());
  377. }
  378. public void loadCSVFile(File selection) {
  379. loadCSV(selection.getPath());
  380. }
  381. public void loadCSV(String path) {
  382. deleteAll();
  383. String[] loadString = loadStrings(path);
  384. for (int i = 0; i < loadString.length-1; i++) {
  385. //This is ultra shitty
  386. if (loadString[i].length() == 0) {
  387. addNode(-1000, -1000, " ");
  388. i_selectedNode = nodeCount - 1;
  389. deleteSelectedNode();
  390. //nodes[nodeCount-1].deleted = true;
  391. } else {
  392. String nodeData[] = split(loadString[i], ',');
  393. addNode(PApplet.parseInt(nodeData[1]), PApplet.parseInt(nodeData[2]), nodeData[0]);
  394. }
  395. }
  396. for (int i = 0; i < loadString.length-1; i++) {
  397. String nodeData[] = split(loadString[i], ',');
  398. for (int j = 3; j < nodeData.length; j++) {
  399. Link(i, PApplet.parseInt(nodeData[j]));
  400. }
  401. }
  402. }
  403. public void saveCSV(String path) {
  404. String saveString = "";
  405. for (int i = 0; i < nodeCount; i++) {
  406. if (!nodes[i].deleted) {
  407. saveString += nodes[i].title + "," + nodes[i].x + "," + nodes[i].y;
  408. for (int j = 0; j < linkCount; j++) {
  409. if (links[j].parentNode == i && links[j].active) {
  410. saveString += "," + str(links[j].targetNode);
  411. }
  412. }
  413. }
  414. saveString += "\n";
  415. }
  416. String[] saveArray = split(saveString, '\n');
  417. saveStrings(path, saveArray);
  418. }
  419. /**
  420. * Keys only get caught for changing a nodes title.
  421. * Previous title is remembered in savedTitle and restored when hitting Escape
  422. */
  423. String savedTitle;
  424. public void keyPressed() {
  425. if (key == ESC) {
  426. key = 0;
  427. }
  428. if (i_selectedNode != -1) {
  429. if (keyCode == BACKSPACE) {
  430. if (nodes[i_selectedNode].title.length() > 0) {
  431. nodes[i_selectedNode].title = nodes[i_selectedNode].title.substring(0, nodes[i_selectedNode].title.length()-1);
  432. nodes[i_selectedNode].refreshSize();
  433. }
  434. } else if (keyCode == DELETE) {
  435. deleteSelectedNode();
  436. } else if (keyCode == ESC) {
  437. nodes[i_selectedNode].title = savedTitle;
  438. nodes[i_selectedNode].refreshSize();
  439. deselect();
  440. } else if (keyCode == RETURN || keyCode == ENTER) {
  441. deselect();
  442. } else if (keyCode != SHIFT && keyCode != RETURN && keyCode != ENTER && keyCode != CONTROL) {
  443. nodes[i_selectedNode].title = nodes[i_selectedNode].title + key;
  444. nodes[i_selectedNode].refreshSize();
  445. }
  446. }
  447. }
  448. public void mousePressed() {
  449. if (mouseButton == LEFT) {
  450. /**
  451. * Either press a button...
  452. */
  453. boolean buttonClicked = false;
  454. for (int i = 0; i < buttons.length; i++) {
  455. if (buttons[i].hover()) {
  456. buttons[i].click();
  457. //Not able to break out of here without using a flag
  458. buttonClicked = true;
  459. }
  460. }
  461. if (!buttonClicked) {
  462. for (int i = 0; i < nodeCount; i++) {
  463. /**
  464. * ...or select / drag a node...
  465. */
  466. if (nodes[i].hover()) {
  467. i_selectedNode = i;
  468. savedTitle = nodes[i].title;
  469. nodes[i].drag = true;
  470. nodes[i].dragMouseStartPoint.x = mouseX;
  471. nodes[i].dragMouseStartPoint.y = mouseY;
  472. break;
  473. }
  474. /**
  475. * ...or pull a new link out of an inlet or outlet.
  476. */
  477. else if (nodes[i].hoverInlet()) {
  478. dragLink = true;
  479. dragOriginLet = -1;
  480. dragOriginId = i;
  481. } else if (nodes[i].hoverOutlet()) {
  482. dragLink = true;
  483. dragOriginLet = 1;
  484. dragOriginId = i;
  485. }
  486. //deselecting the shit out of this for reasons I don't remember, no care to improve right now
  487. deselect();
  488. }
  489. }
  490. }
  491. }
  492. /**
  493. * Create links
  494. */
  495. public void mouseReleased() {
  496. for (int i = 0; i < nodeCount; i++) {
  497. nodes[i].drag = false;
  498. if (dragLink && dragOriginId != i) {
  499. if (nodes[i].hoverInlet() && dragOriginLet == 1) {
  500. Link(dragOriginId, i);
  501. }
  502. if (nodes[i].hoverOutlet() && dragOriginLet == -1) {
  503. Link(i, dragOriginId);
  504. }
  505. }
  506. }
  507. /**
  508. * Reset drag&drop states
  509. */
  510. dragLink = false;
  511. dragOriginLet = 0;
  512. dragOriginId = -1;
  513. }
  514. /**
  515. * Scaling is tied to textSize. Having the UI on a seperate PGraphics would be better, but it's good enough for now
  516. */
  517. int zoomDelta = 3;
  518. public void mouseWheel(MouseEvent event) {
  519. float zoom = event.getAmount();
  520. if (zoom < 0) {
  521. textSize += zoomDelta;
  522. } else if (zoom > 0) {
  523. if (textSize - zoomDelta > 0) {
  524. textSize -= zoomDelta;
  525. }
  526. }
  527. padding = textSize/3*2;
  528. for (int i = 0; i < nodeCount; i++) {
  529. nodes[i].refreshSize();
  530. nodes[i].setVectors();
  531. }
  532. }
  533. public void settings() { size(800, 600); }
  534. static public void main(String[] passedArgs) {
  535. String[] appletArgs = new String[] { "ontology" };
  536. if (passedArgs != null) {
  537. PApplet.main(concat(appletArgs, passedArgs));
  538. } else {
  539. PApplet.main(appletArgs);
  540. }
  541. }
  542. }