Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

551
LINES

< > BotCompany Repo | #1007303 // CirclesAndLines [include]

JavaX fragment (include)

sclass BaseBase {
  S globalID = aGlobalID();
  S text;
  S textForRender() { ret text; }
}

sbool traits_multiLine = true;

sclass Base extends BaseBase {
  new L<S> traits;
  bool hasTrait(S t) { ret containsIC(traits(), t); }
  L<S> traits() { if (nempty(text) && neq(first(traits), text)) traits.add(0, text); ret traits; }
  void addTraits(L<S> l) { setAddAll(traits(), l); }
  void addTrait(S t) { if (nempty(t)) setAdd(traits(), t); }
  
  S textForRender() {
    L<S> 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<Circle> circles;
  new L<Line> lines;
  Class<? extends Arrow> arrowClass = Arrow;
  Class<? extends Circle> 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<Pair<Circle>, Line> hasLine = new HashMap;
      new HashMap<Line, Bool> 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<Circle> 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<Circle> 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<Line> 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;
  }
}

Author comment

Began life as a copy of #1007298

download  show line numbers  debug dex  old transpilations   

Travelled to 17 computer(s): aoiabmzegqzx, bhatertpkbcr, cbybwowwnfue, cfunsshuasjs, gwrvuhgaqvyk, ishqpsrjomds, jtubtzbbkimh, lpdgvwnxivlt, mqqgnosmbjvj, onxytkatvevr, ppjhyzlbdabe, pyentgdyhuwx, pzhvpgtvlbxg, tslmcundralx, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv

No comments. add comment

Snippet ID: #1007303
Snippet name: CirclesAndLines [include]
Eternal ID of this version: #1007303/197
Text MD5: bdaf89feb430d4dbb3ef2323baace056
Author: stefan
Category: javax / gui
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2020-02-20 13:22:40
Source code size: 16898 bytes / 551 lines
Pitched / IR pitched: No / No
Views / Downloads: 1102 / 5017
Version history: 196 change(s)
Referenced in: #1007304 - Draggable Circles [shortened]
#1007305 - Draggable Circles With Arrow
#1007310 - Draggable Circles With Text
#1007312 - Draggable Circles With More Text
#1007323 - Is Donald Good Or Bad? [WORKS]
#1007363 - Donald Thumbnail
#1034167 - Standard Classes + Interfaces (LIVE, continuation of #1003674)