sclass BaseBase { S globalID = aGlobalID(); S text; S textForRender() { ret text; } } sbool traits_multiLine = true; sclass Base extends BaseBase { new L traits; bool hasTrait(S t) { ret containsIC(traits(), t); } L traits() { if (nempty(text) && neq(first(traits), text)) traits.add(0, text); ret traits; } void addTraits(L l) { setAddAll(traits(), l); } void addTrait(S t) { if (nempty(t)) setAdd(traits(), t); } S textForRender() { L traits = traits(); if (traits_multiLine) ret lines_rtrim(traits); if (l(traits) <= 1) ret first(traits); ret first(traits) + " [" + join(", ", dropFirst(traits)) + "]"; } void setText(S text) { this.text = text; traits = ll(text); } } sclass CirclesAndLines { new L circles; new L lines; Class arrowClass = Arrow; Class circleClass = Circle; S title; S globalID = aGlobalID(); long created = nowUnlessLoading(); transient Lock lock = fairLock(); transient S defaultImageID = #1007372; transient double imgZoom = 1; // zoom for the circle images transient Pt translate; Circle hoverCircle; // which one we are hovering over transient O onUserMadeArrow, onUserMadeCircle, onLayoutChange; transient O onFullLayoutChange, onDeleteCircle, onDeleteLine; transient O onRenameCircle, onRenameLine, onStructureChange; transient BufferedImage imageForUserMadeNodes; static int maxDistanceToLine = 20; // for clicking transient S backgroundImageID = defaultBackgroundImageID; static S defaultBackgroundImageID = #1007195; static Color defaultLineColor = Color.white; static bool debugRender; static O staticPopupExtender; transient double scale = 1; // zoom whole image transient bool recordHistory = true; L history; // auto-visualize Circle circle_autoVis(S text, S visualizationText, double x, double y) { ret addAndReturn(circles, nu(circleClass, +x, +y, +text, quickvis := visualizationText, img := processImage(quickVisualizeOr(visualizationText, defaultImageID)))); } S makeVisualizationText(S text) { ret possibleGlobalID(text) ? "" : text; } Circle circle_autoVis(S text, double x, double y) { ret circle_autoVis(text, makeVisualizationText(text), x, y); } Circle circle(BufferedImage img, double x, double y, S text) { ret addAndReturn(circles, nu(circleClass, +x, +y, +text, img := processImage(img)); } Circle circle(S text, BufferedImage img, double x, double y) { ret circle(img, x, y, text); } Circle circle(S text, double x, double y) { ret addAndReturn(circles, nu(circleClass, +x, +y, +text, img := processImage(imageForUserMadeNodes()); } Circle addCircle(S imageID, double x, double y) { ret addCircle(imageID, x, y, ""); } Circle addCircle(S imageID, double x, double y, S text) { ret addAndReturn(circles, nu(circleClass, +x, +y, +text, img := processImage(loadImage2(imageID)))); } Arrow findArrow(Circle a, Circle b) { for (Line l : getWhere(lines, +a, +b)) if (l instanceof Arrow) ret (Arrow) l; null; } Line addLine(Circle a, Circle b) { Line line = findWhere(lines, +a, +b); if (line == null) lines.add(line = nu(Line, +a, +b)); ret line; } Arrow arrow(Circle a, S text, Circle b) { ret addArrow(a, b, text); } Arrow addArrow(Circle a, Circle b) { ret addArrow(a, b, ""); } Arrow addArrow(Circle a, Circle b, S text) { ret addAndReturn(lines, nu(arrowClass, +a, +b, +text)); } BufferedImage makeImage(int w, int h) { BufferedImage bg = renderTiledBackground(backgroundImageID, w, h, ptX(translate), ptY(translate)); if (!lock.tryLock()) null; try { if (scale != 1) createGraphics_modulate(bg, voidfunc(Graphics2D g) { g.scale(scale, scale); }); // Lines if (debugRender) print("Have " + n(lines, "line")); // flipMap is false for bidirectional connections HashMap, Line> hasLine = new HashMap; new HashMap flipMap; for (Line l : lines) { hasLine.put(pair(l.a, l.b), l); Line x = hasLine.get(pair(l.b, l.a)); if (x != null) { if (debugRender) print("flipMap " + l.a.text + " / " + l.b.text); flipMap.put(x, false); flipMap.put(l, false); } } for (Line l : lines) { DoublePt a = translateDoublePt(translate, l.a.doublePt(w, h, this)); DoublePt b = translateDoublePt(translate, l.b.doublePt(w, h, this)); if (debugRender) print("Line " + a + " " + b); if (l instanceof Arrow) drawThoughtArrow(bg, l.a.img(this), iround(a.x), iround(a.y), l.b.img(this), iround(b.x), iround(b.y), l.color); else drawThoughtLine(bg, l.a.img(this), iround(a.x), iround(a.y), l.b.img(this), iround(b.x), iround(b.y), l.color); S text = l.textForRender(); if (nempty(text)) { Bool flip = flipMap.get(l); drawOutlineTextAlongLine_flip.set(flip); // mark bidirectional arrow labels with direction for clarity if (flip != null) text += " " + unicode_blackRightArrow(); drawThoughtLineText_multiLine(bg, l.a.img(this), iround(a.x), iround(a.y), l.b.img(this), iround(b.x), iround(b.y), text, Color.white /*l.color*/); } } // Circles for (Circle c : circles) { DoublePt p = translateDoublePt(translate, c.doublePt(w, h, this)); drawThoughtCircle(bg, c.img(this), p.x, p.y); } for (Circle c : circles) { DoublePt p = translateDoublePt(translate, c.doublePt(w, h, this)); S text = c.textForRender(); if (nempty(text)) drawThoughtCircleText(bg, c.img(this), p, text); if (c == hoverCircle) drawThoughtCirclePlus(bg, c.img(this), p.x, p.y); } } finally { lock.unlock(); createGraphics_modulate(bg, null); } ret bg; } Canvas showAsFrame(int w, int h) { Canvas canvas = showAsFrame(); frameInnerSize(canvas, w, h); centerFrame(getFrame(canvas)); ret canvas; } Canvas showAsFrame() { ret (Canvas) swing(func { Canvas canvas = makeCanvas(); showCenterFrame(canvas); ret canvas; }); } Canvas makeCanvas() { fO makeImg = func(int w, int h) { makeImage(w, h) }; final Canvas canvas = jcanvas(makeImg); disableImageSurfaceSelector(canvas); new CircleDragger(this, canvas, r { updateCanvas(canvas, makeImg) }); componentPopupMenu(canvas, voidfunc(JPopupMenu menu) { // POPUP MENU START Pt p = pointFromEvent(canvas, componentPopupMenu_mouseEvent.get()); JMenu imageMenu = jmenu("Image"); moveAllMenuItems(menu, imageMenu); addMenuItem(menu, "New Circle...", r { newCircle(canvas) }); final Line l = findLine(canvas, p); if (l != null) { addMenuItem(menu, "Rename Relation...", r { renameLine(canvas, l) }); addMenuItem(menu, "Delete Relation", r { deleteLine(l); canvas.update(); }); } final Circle c = findCircle(canvas, p); if (c != null) { addMenuItem(menu, "Rename Circle...", r { renameCircle(canvas, c) }); addMenuItem(menu, "Delete Circle", r { deleteCircle(c); canvas.update(); }); if (c.img != null || c.quickvis != null) addMenuItem(menu, "Delete Image", r { c.img = null; c.quickvis = null; canvas.update(); }); if (neqic(c.text, c.quickvis)) addMenuItem(menu, "Visualize", r { thread "Visualizing" { quickVisualize(c.text); print("Quickvis done"); swing { c.img = null; c.quickvis = c.text; canvas.update(); pcallF(onRenameCircle, c); schange(); } } }); } addMenuItem(menu, "Copy structure to clipboard", r { copyTextToClipboard(cal_simplifiedStructure(CirclesAndLines.this)) }); addMenuItem(menu, "Paste structure", r { S text = getTextFromClipboard(); if (nempty(text)) { copyCAL(cal_unstructure(text), CirclesAndLines.this); canvas.update(); schange(); } }); pcallF(staticPopupExtender, CirclesAndLines.this, canvas, menu); addMenuItem(menu, imageMenu); // POPUP MENU END }); ret canvas; } void newCircle(final Canvas canvas) { final JTextField text = jtextfield(); showFormTitled("New Circle", "Text", text, r-thread { loading { S theText = getTextTrim(text); makeCircle(theText); canvas.update(); }}); } Canvas show() { ret showAsFrame(); } Canvas show(int w, int h) { ret showAsFrame(w, h); } Circle findCircle(S text) { for (Circle c : circles) if (eq(c.text, text)) ret c; for (Circle c : circles) if (eqic(c.text, text)) ret c; null; } void renameCircle(final Canvas canvas, final Circle c) { final JTextField tf = jtextfield(c.text); showFormTitled("Rename circle", "Old name", jlabel(c.text), "New name", tf, r { c.setText(getTextTrim(tf)); canvas.update(); pcallF(onRenameCircle, c); schange(); }); } void renameLine(final Canvas canvas, final Line l) { final JTextField tf = jtextfield(l.text); showFormTitled("Rename relation", "Old name", jlabel(l.text), "New name", tf, r { l.setText(getTextTrim(tf)); canvas.update(); pcallF(onRenameLine, l); schange(); }); } void clear { clearAll(circles, lines); } // only finds actually containing circles Circle findCircle(ImageSurface canvas, Pt p) { p = untranslatePt(translate, p); new Lowest best; for (Circle c : circles) if (c.contains(this, canvas, p)) best.put(c, pointDistance(p, c.pt2(this, canvas))); ret best!; } Circle findNearestCircle(ImageSurface canvas, Pt p) { new Lowest best; for (Circle c : circles) if (c.contains(this, canvas, p)) best.put(c, pointDistance(p, c.pt2(this, canvas))); ret best.get(); } BufferedImage processImage(BufferedImage img) { ret scaleImage(img, imgZoom); } void deleteCircle(Circle c) { for (Line l : cloneList(lines)) if (l.a == c || l.b == c) deleteLine(l); circles.remove(c); pcallF(onDeleteCircle, c); schange(); } void deleteLine(Line l) { lines.remove(l); pcallF(onDeleteLine, l); schange(); } void openPlusDialog(final Circle c, final ImageSurface canvas) { if (c == null) ret; final JTextField tfFrom = jtextfield(c.text); final JTextField tfRel = jtextfield(web_defaultRelationName()); final JComboBox tfTo = autoComboBox(collect(circles, 'text)); showFormTitled("Add connection", "From node", tfFrom, "Connection name", tfRel, "To node", tfTo, func { S sA = getTextTrim(tfFrom); Circle a = eq(sA, c.text) ? c : findOrMakeCircle(sA); if (a == null) { messageBox("Not found: " + getTextTrim(tfFrom)); false; } Circle b = findOrMakeCircle(getTextTrim(tfTo)); if (b == null) { messageBox("Not found: " + getTextTrim(tfTo)); false; } if (a == b) { infoBox("Can't connect circle to itself for now"); false; } Arrow arrow = arrow(a, getTextTrim(tfRel), b); ((Canvas) canvas).update(); pcallF(onUserMadeArrow, arrow); schange(); null; }); awtLater(tfRel, 100, r { requestFocus(tfRel) }); } void schange() { pcallF(onStructureChange); logQuoted("user-web-edits", now() + " " + cal_structure(this)); markWebsPosted(); } Circle findOrMakeCircle(S text) { Circle c = findCircle(text); if (c != null) ret c; ret makeCircle(text); } Circle makeCircle(S text) { Circle c = circle(imageForUserMadeNodes(), random(0.1, 0.9), random(0.1, 0.9), text); pcallF(onUserMadeCircle, c); schange(); historyLog(lisp("Made circle", text)); ret c; } BufferedImage imageForUserMadeNodes() { if (imageForUserMadeNodes == null) imageForUserMadeNodes = whiteImage(20, 20); ret imageForUserMadeNodes; } Line findLine(Canvas is, Pt p) { p = untranslatePt(translate, p); new Lowest best; for (Line line : lines) { double d = distancePointToLineSegment(line.a.pt(this, is), line.b.pt(this, is), p); if (d <= maxDistanceToLine) best.put(line, d); } ret best!; } Pt pointFromEvent(ImageSurface canvas, MouseEvent e) { ret scalePt(canvas.pointFromEvent(e), 1/scale); } void historyLog(O o) { if (!recordHistory) ret; if (history == null) history = new L; history.add(o); } } // end of class CirclesAndLines sclass Circle extends Base { transient BufferedImage img; double x, y; //static BufferedImage defaultImage; S quickvis; BufferedImage img(CirclesAndLines cal) { if (img != null) ret img; if (nempty(quickvis)) img = quickVisualize(quickvis); //if (defaultImage == null) defaultImage = loadImage2(#1007372); ret cal.imageForUserMadeNodes(); } Pt pt2(CirclesAndLines cal, ImageSurface is) { ret pt2(is.getWidth(), is.getHeight(), cal); } Pt pt(CirclesAndLines cal, ImageSurface is) { ret pt(is.getWidth(), is.getHeight(), cal); } Pt pt(int w, int h, CirclesAndLines cal) { ret new Pt(iround(x*w/cal.scale), iround(y*h/cal.scale)); } Pt pt2(int w, int h, CirclesAndLines cal) { ret new Pt(iround(x*w), iround(y*h)); } DoublePt doublePt(int w, int h, CirclesAndLines cal) { ret new DoublePt(x*w/cal.scale, y*h/cal.scale); } bool contains(CirclesAndLines cal, ImageSurface is, Pt p) { ret pointDistance(p, pt2(cal, is)) <= iround(thoughtCircleSize(img(cal))*cal.scale)/2+1; } } sclass Line extends Base { Circle a, b; transient Color color = CirclesAndLines.defaultLineColor; Line setColor(Color color) { this.color = color; this; } } Line > Arrow {} sclass CircleDragger extends MouseAdapter { CirclesAndLines cal; ImageSurface is; O update; int dx, dy; Circle circle; Pt startPoint; *(CirclesAndLines *cal, ImageSurface *is, O *update) { if (containsInstance(is.tools, CircleDragger)) ret; is.tools.add(this); is.addMouseListener(this); is.addMouseMotionListener(this); } public void mouseMoved(MouseEvent e) { Pt p = is.pointFromEvent(e); Circle c = cal.findCircle(is, p); if (c != cal.hoverCircle) { cal.hoverCircle = c; callF(update); } } public void mousePressed(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { Pt p = is.pointFromEvent(e); startPoint = p; circle = cal.findCircle(is, p); if (circle != null) { dx = p.x-iround(circle.x*is.getWidth()); dy = p.y-iround(circle.y*is.getHeight()); //printVars("mousePressed", +dx, +dy, +p, cx := circle.x, cy := circle.y, w := is.getWidth(), h := is.getHeight()); } else { Pt t = unnull(cal.translate); dx = p.x-t.x; dy = p.y-t.y; } } } public void mouseDragged(MouseEvent e) { if (startPoint == null) ret; Pt p = is.pointFromEvent(e); if (circle != null) { circle.x = (p.x-dx)/(double) is.getWidth(); circle.y = (p.y-dy)/(double) is.getHeight(); //printVars("mouseDragged", +dx, +dy, +p, cx := circle.x, cy := circle.y, w := is.getWidth(), h := is.getHeight()); pcallF(cal.onLayoutChange, circle); callF(update); } else { cal.translate = new Pt(p.x-dx, p.y-dy); callF(update); } } public void mouseReleased(MouseEvent e) { mouseDragged(e); if (eq(is.pointFromEvent(e), startPoint)) cal.openPlusDialog(circle, is); circle = null; startPoint = null; } }