Minsky
group.cc
Go to the documentation of this file.
1 /*
2  @copyright Steve Keen 2015
3  @author Russell Standish
4  This file is part of Minsky.
5 
6  Minsky is free software: you can redistribute it and/or modify it
7  under the terms of the GNU General Public License as published by
8  the Free Software Foundation, either version 3 of the License, or
9  (at your option) any later version.
10 
11  Minsky is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with Minsky. If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "minsky.h"
21 #include "cairoItems.h"
22 #include "group.h"
23 #include "wire.h"
24 #include "operation.h"
25 #include "autoLayout.h"
26 #include "equations.h"
27 #include <cairo_base.h>
28 #include "group.rcd"
29 #include "itemT.rcd"
30 #include "bookmark.rcd"
31 #include "progress.h"
32 #include "minsky_epilogue.h"
33 using namespace std;
34 using namespace ecolab::cairo;
35 
36 // size of the top and bottom margins of the group icon
37 static const int topMargin=10;
38 
39 namespace minsky
40 {
41  SVGRenderer Group::svgRenderer;
42 
43  namespace
44  {
45  // return true if scope g refers to the global model group
46  bool isGlobal(const GroupPtr& g)
47  {return !g || g==cminsky().model;}
48  }
49 
50  double Group::operator()(const std::vector<double>& p)
51  {
52  if (outVariables.empty()) return nan("");
53 
54  MathDAG::SystemOfEquations system(minsky(), *this);
55  EvalOpVector equations;
56  vector<Integral> integrals;
57  system.populateEvalOpVector(equations, integrals);
58  auto flow(ValueVector::flowVars);
59 
60  // assign values to unattached input variables
61  auto iVar=inVariables.begin();
62  for (auto v: p)
63  {
64  while (iVar!=inVariables.end() && (*iVar)->inputWired()) ++iVar;
65  if (iVar==inVariables.end()) break;
66  flow[(*iVar)->vValue()->idx()]=v;
67  }
68 
69  for (auto& i: equations)
70  i->eval(flow.data(), flow.size(),ValueVector::stockVars.data());
71  return flow[outVariables[0]->vValue()->idx()];
72  }
73 
74  std::string Group::formula() const
75  {
76  if (outVariables.empty()) return "0";
77  MathDAG::SystemOfEquations system(minsky(), *this);
78  ostringstream o;
79  auto node=system.getNodeFromVar(*outVariables[0]);
80  if (!node) return "0";
81  if (node->rhs)
82  node->rhs->matlab(o);
83  else
84  node->matlab(o);
85  return o.str();
86  }
87 
88  string Group::arguments() const
89  {
90  MathDAG::SystemOfEquations system(minsky(), *this);
91  ostringstream r;
92  r<<"(";
93  for (auto& i: inVariables)
94  if (!i->inputWired())
95  {
96  if (r.str().size()>1) r<<",";
97  if (auto node=system.getNodeFromVar(*i))
98  node->matlab(r);
99  }
100  r<<")";
101  return r.str();
102  }
103 
104 
105  // assigned the cloned equivalent of a port
106  void asgClonedPort(shared_ptr<Port>& p, const map<Item*,ItemPtr>& cloneMap)
107  {
108  auto clone=cloneMap.find(&p->item());
109  if (clone!=cloneMap.end())
110  {
111  for (size_t i=0; i<p->item().portsSize(); ++i)
112  if (p->item().ports(i).lock()==p)
113  {
114  // set the new port to have the equivalent position in the clone
115  p=clone->second->ports(i).lock();
116  assert(p);
117  }
118  }
119  }
120 
121  GroupPtr Group::copy() const
122  {
123  // make new group owned by the top level group to prevent
124  if (auto g=group.lock())
125  return g->addGroup(copyUnowned());
126  return GroupPtr(); // do nothing if we attempt to clone the entire model
127  }
128 
129  GroupPtr Group::copyUnowned() const
130  {
131  auto r=make_shared<Group>();
132  r->self=r;
133  r->moveTo(x(),y());
134  // make new group owned by the top level group to prevent snarlups when called recursively
135  if (group.lock())
136  const_cast<Group*>(this)->globalGroup().addGroup(r);
137  else
138  return GroupPtr(); // do nothing if we attempt to clone the entire model
139 
140  // a map of original to cloned items (weak references)
141  map<Item*,ItemPtr> cloneMap;
142  map<IntOp*,bool> integrals;
143  // cloning IntOps mutates items, as intVars get inserted and removed
144  auto itemsCopy=items;
145  for (auto& i: itemsCopy)
146  if (auto integ=dynamic_cast<IntOp*>(i.get()))
147  integrals.emplace(integ, integ->coupled());
148  for (auto& i: itemsCopy)
149  cloneMap[i.get()]=r->addItem(i->clone(),true);
150  for (auto& i: groups)
151  cloneMap[i.get()]=r->addGroup(i->copyUnowned());
152  for (auto& w: wires)
153  {
154  auto f=w->from(), t=w->to();
155  asgClonedPort(f,cloneMap);
156  asgClonedPort(t,cloneMap);
157  r->addWire(new Wire(f,t,w->coords()));
158  }
159 
160  for (auto& v: inVariables)
161  {
162  assert(cloneMap.count(v.get()));
163  r->inVariables.push_back(dynamic_pointer_cast<VariableBase>(cloneMap[v.get()]));
164  r->inVariables.back()->controller=self;
165  }
166  for (auto& v: outVariables)
167  {
168  assert(cloneMap.count(v.get()));
169  r->outVariables.push_back(dynamic_pointer_cast<VariableBase>(cloneMap[v.get()]));
170  r->outVariables.back()->controller=self;
171  }
172  // reattach integral variables to their cloned counterparts
173  for (auto i: integrals)
174  {
175  if (auto newIntegral=dynamic_cast<IntOp*>(cloneMap[i.first].get()))
176  {
177  auto newIntVar=dynamic_pointer_cast<VariableBase>(cloneMap[i.first->intVar.get()]);
178  if (newIntVar && newIntegral->intVar != newIntVar)
179  {
180  r->removeItem(*newIntegral->intVar);
181  newIntegral->intVar=newIntVar;
182  }
183  if (i.second != newIntegral->coupled())
184  newIntegral->toggleCoupled();
185  }
186  }
187  r->computeRelZoom();
188  return r;
189  }
190 
191  void Group::makeSubroutine()
192  {
193  recursiveDo(&GroupItems::items, [this](Items&,Items::iterator i)
194  {
195  if (auto v=(*i)->variableCast())
196  if (v->rawName()[0]==':')
197  {
198  // walk up parent groups to see if this variable is mentioned
199  for (auto g=group.lock(); g; g=g->group.lock())
200  for (auto& item: g->items)
201  if (auto vi=item->variableCast())
202  if (vi->valueId()==v->valueId())
203  goto outer_scope_variable_found;
204  v->name(v->rawName().substr(1)); // make variable local
205  }
206  outer_scope_variable_found:
207  return false;
208  });
209  }
210 
211 
212  ItemPtr GroupItems::removeItem(const Item& it)
213  {
214  if (it.plotWidgetCast()==displayPlot.get()) removeDisplayPlot();
215  if (!inDestructor) bookmarks.erase(it.bookmarkId());
216  for (auto i=items.begin(); i!=items.end(); ++i)
217  if (i->get()==&it)
218  {
219  ItemPtr r=*i;
220  items.erase(i);
221  if (auto v=r->variableCast())
222  if (v->ioVar())
223  {
224  remove(inVariables, r);
225  remove(outVariables, r);
226  remove(createdIOvariables, r);
227  v->controller.reset();
228  }
229  return r;
230  }
231 
232  for (auto i=groups.begin(); i!=groups.end(); ++i)
233  if (i->get()==&it)
234  {
235  ItemPtr r=*i;
236  groups.erase(i);
237  return r;
238  }
239 
240 
241  for (auto& g: groups)
242  if (ItemPtr r=g->removeItem(it))
243  return r;
244  return ItemPtr();
245  }
246 
247  void Group::deleteItem(const Item& i)
248  {
249  if (auto r=removeItem(i))
250  {
251  r->deleteAttachedWires();
252  r->removeControlledItems();
255  }
256  }
257 
258 
259  WirePtr GroupItems::removeWire(const Wire& w)
260  {
261  for (auto i=wires.begin(); i!=wires.end(); ++i)
262  if (i->get()==&w)
263  {
264  WirePtr r=*i;
265  wires.erase(i);
266  return r;
267  }
268 
269  for (auto& g: groups)
270  if (WirePtr r=g->removeWire(w))
271  return r;
272  return WirePtr();
273  }
274 
275  GroupPtr GroupItems::removeGroup(const Group& group)
276  {
277  for (auto i=groups.begin(); i!=groups.end(); ++i)
278  if (i->get()==&group)
279  {
280  GroupPtr r=*i;
281  groups.erase(i);
282  return r;
283  }
284 
285  for (auto& g: groups)
286  if (GroupPtr r=g->removeGroup(group))
287  return r;
288  return GroupPtr();
289  }
290 
291  ItemPtr GroupItems::findItem(const Item& it) const
292  {
293  // start by looking in the group it thnks it belongs to
294  if (auto g=it.group.lock())
295  if (g.get()!=this)
296  {
297  auto i=g->findItem(it);
298  if (i) return i;
299  }
300  return findAny(&Group::items, [&](const ItemPtr& x){return x.get()==&it;});
301  }
302 
303  void GroupItems::renameVar(const GroupPtr& origGroup, VariableBase& v)
304  {
305  if (auto thisGroup=self.lock())
306  {
307  if (origGroup->higher(*thisGroup))
308  {
309  // moving local var into an inner group, make global
310  if (v.rawName()[0]!=':')
311  v.name(':'+v.rawName());
312  }
313  else if (thisGroup->higher(*origGroup))
314  {
315  // moving global var into an outer group, link up with variable of same name (if existing)
316  if (v.name()[0]==':')
317  for (auto& i: items)
318  if (auto vv=i->variableCast())
319  if (vv->name()==v.name().substr(1))
320  v.name(v.name().substr(1));
321  }
322  else
323  // moving between unrelated groups
324  if (v.name()[0]==':' && valueId(thisGroup,v.name()) != valueId(origGroup,v.name()))
325  // maintain linkage if possible, otherwise make local
326  v.name(v.name().substr(1));
327  }
328  }
329 
330 
331  ItemPtr GroupItems::addItem(const shared_ptr<Item>& it, bool inSchema)
332  {
333  if (!it) return it;
335  if (auto x=dynamic_pointer_cast<Group>(it))
336  return addGroup(x);
337 
338  // stash position
339  const float x=it->x(), y=it->y();
340  auto origGroup=it->group.lock();
341 
342  if (origGroup.get()==this) return it; // nothing to do.
343  if (origGroup)
344  origGroup->removeItem(*it);
345 
346  // stash init value to initialise new variableValue
347  string init;
348  string units;
349  if (auto v=it->variableCast())
350  {
351  init=v->init();
352  units=v->unitsStr();
353  }
354 
355  it->group=self;
356  if (!inSchema) it->moveTo(x,y);
357 
358  // take into account new scope
359  if (auto v=it->variableCast())
360  {
361  if (!inSchema && origGroup)
362  renameVar(origGroup, *v);
363  v->init(init); //NB calls ensureValueExists()
364  // if value didn't exist before, units will be empty. Do not overwrite any previous units set. For #1461.
365  if (units.length()) v->setUnits(units);
366  }
367 
368  // move wire to highest common group
369  for (size_t i=0; i<it->portsSize(); ++i)
370  {
371  auto p=it->ports(i).lock();
372  assert(p);
373  for (auto& w: p->wires())
374  {
375  assert(w);
376  adjustWiresGroup(*w);
377  }
378  }
379 
380  // need to deal with integrals especially because of the attached variable
381  if (auto intOp=dynamic_cast<IntOp*>(it.get()))
382  if (intOp->intVar)
383  {
384  if (!inSchema && origGroup)
385  renameVar(origGroup, *intOp->intVar);
386  if (auto oldG=intOp->intVar->group.lock())
387  {
388 
389  if (oldG.get()!=this)
390  addItem(oldG->removeItem(*intOp->intVar),inSchema);
391  }
392  else
393  addItem(intOp->intVar,inSchema);
394  if (intOp->coupled())
395  intOp->intVar->controller=it;
396  else
397  intOp->intVar->controller.reset();
398  }
399 
400  items.push_back(it);
401  return items.back();
402  }
403 
404  void GroupItems::adjustWiresGroup(Wire& w)
405  {
406  // Find common ancestor group, and move wire to it
407  assert(w.from() && w.to());
408  shared_ptr<Group> p1=w.from()->item().group.lock(), p2=w.to()->item().group.lock();
409  assert(p1 && p2);
410  unsigned l1=p1->level(), l2=p2->level();
411  for (; p1 && l1>l2; l1--) p1=p1->group.lock();
412  for (; p2 && l2>l1; l2--) p2=p2->group.lock();
413 
414  while (p1 && p2 && p1!=p2)
415  {
416  assert(p1 && p2);
417  p1=p1->group.lock();
418  p2=p2->group.lock();
419  }
420  if (!p1 || !p2) return;
421  w.moveIntoGroup(*p1);
422  }
423 
424  void Group::splitBoundaryCrossingWires()
425  {
426  // Wire::split will invalidate the Items::iterator, so collect
427  // wires to split first
428  set<Wire*> wiresToSplit;
429  for (auto& i: items)
430  for (size_t p=0; p<i->portsSize(); ++p)
431  for (auto w: i->ports(p).lock()->wires())
432  wiresToSplit.insert(w);
433 
434  for (auto w: wiresToSplit)
435  w->split();
436 
437  // check if any created I/O variables can be removed
438  auto varsToCheck=createdIOvariables;
439  for (auto& iv: varsToCheck)
440  {
441  assert(iv->ports(1).lock()->input() && !iv->ports(1).lock()->multiWireAllowed());
442  // firstly join wires that don't cross boundaries
443  // determine if this is input or output var
444  if (!iv->ports(1).lock()->wires().empty())
445  {
446  auto fromGroup=iv->ports(1).lock()->wires()[0]->from()->item().group.lock();
447  if (fromGroup.get() == this)
448  {
449  // not an input var
450  for (auto& w: iv->ports(0).lock()->wires())
451  if (w->to()->item().group.lock().get() == this)
452  // join wires, as not crossing boundary
453  {
454  auto to=w->to();
455  iv->ports(0).lock()->eraseWire(w);
456  removeWire(*w);
457  addWire(iv->ports(1).lock()->wires()[0]->from(), to);
458  }
459  }
460  else
461  for (auto& w: iv->ports(0).lock()->wires())
462  if (w->to()->item().group.lock() == fromGroup)
463  // join wires, as not crossing boundary
464  {
465  auto to=w->to();
466  iv->ports(0).lock()->eraseWire(w);
467  globalGroup().removeWire(*w);
468  adjustWiresGroup(*addWire(iv->ports(1).lock()->wires()[0]->from(), to));
469  }
470  }
471 
472  if (iv->ports(0).lock()->wires().empty() || iv->ports(1).lock()->wires().empty())
473  removeItem(*iv);
474  }
475  }
476 
477  size_t GroupItems::numItems() const
478  {
479  size_t count=items.size();
480  for (auto& i: groups) count+=i->numItems();
481  return count;
482  }
483 
484  size_t GroupItems::numWires() const
485  {
486  size_t count=wires.size();
487  for (auto& i: groups) count+=i->numWires();
488  return count;
489  }
490 
491  size_t GroupItems::numGroups() const
492  {
493  size_t count=groups.size();
494  for (auto& i: groups) count+=i->numGroups();
495  return count;
496  }
497 
498 
499  void Group::moveContents(Group& source) {
500  if (&source!=this)
501  {
502  if (source.higher(*this))
503  throw error("attempt to move a group into itself");
504  // make temporary copies as addItem removes originals
505  auto copyOfItems=source.items;
506  for (auto& i: copyOfItems)
507  {
508  addItem(i);
509  assert(!i->ioVar());
510  }
511  auto copyOfGroups=source.groups;
512  for (auto& i: copyOfGroups)
513  addGroup(i);
515  source.clear();
516  }
517  }
518 
519  VariablePtr Group::addIOVar(const char* prefix)
520  {
521  const VariablePtr v(VariableType::flow,
522  uqName(cminsky().variableValues.newName(to_string(size_t(this))+":"+prefix)));
523  addItem(v,true);
524  createdIOvariables.push_back(v);
525  v->rotation(rotation());
526  v->controller=self;
527  bb.update(*this);
528  return v;
529  }
530 
531  Group::IORegion::type Group::inIORegion(float x, float y) const
532  {
533  float left, right;
534  const float z=zoomFactor();
535  margins(left,right);
536  const float dx=(x-this->x())*cos(rotation()*M_PI/180)-
537  (y-this->y())*sin(rotation()*M_PI/180);
538  const float dy=(x-this->x())*sin(rotation()*M_PI/180)+
539  (y-this->y())*cos(rotation()*M_PI/180);
540  const float w=0.5*iWidth()*z,h=0.5*iHeight()*z;
541  if (w-right<dx)
542  return IORegion::output;
543  if (-w+left>dx)
544  return IORegion::input;
545  if ((-h+topMargin*z>dy && dy<0) || (h-topMargin*z<dy && dy>0))
546  return IORegion::topBottom;
547  return IORegion::none;
548  }
549 
550  void Group::checkAddIORegion(const ItemPtr& x)
551  {
552  if (auto v=dynamic_pointer_cast<VariableBase>(x))
553  {
554  remove(inVariables, v);
555  remove(outVariables, v);
556  switch (inIORegion(v->x(),v->y()))
557  {
558  case IORegion::input:
559  inVariables.push_back(v);
560  v->controller=self;
561  break;
562  case IORegion::output:
563  outVariables.push_back(v);
564  v->controller=self;
565  break;
566  default:
567  v->controller.reset();
568  break;
569  }
570  }
571  }
572 
573 
574  void Group::resizeOnContents()
575  {
576  double x0, x1, y0, y1;
577  contentBounds(x0,y0,x1,y1);
578  const double xx=0.5*(x0+x1), yy=0.5*(y0+y1);
579  const double dx=xx-x(), dy=yy-y();
580  float l,r; margins(l,r);
581  const float z=zoomFactor();
582  m_width=((x1-x0)+l+r)/z;
583  m_height=((y1-y0)+20*z)/z;
584 
585  // adjust contents by the offset
586  for (auto& i: items)
587  i->moveTo(i->x()-dx, i->y()-dy);
588  for (auto& i: groups)
589  i->moveTo(i->x()-dx, i->y()-dy);
590 
591  moveTo(xx,yy);
592  bb.update(*this);
593  }
594 
595  namespace
596  {
597  template <class T>
598  void resizeItems(T& items, double sx, double sy)
599  {
600  for (auto& i: items)
601  if (!i->ioVar())
602  {
603  i->m_x*=sx;
604  i->m_y*=sy;
605  }
606  }
607 
608 
609 
610  template <class T>
611  void recentreItems(const T& items, float xc, float yc)
612  {
613  for (auto& i: items)
614  if (!i->ioVar())
615  {
616  i->m_x-=xc;
617  i->m_y-=yc;
618  }
619  }
620 
621 
622  }
623 
624  void Group::resize(const LassoBox& b)
625  {
626  float z=zoomFactor();
627  // account for margins
628  float l, r;
629  margins(l,r);
630  if (fabs(b.x0-b.x1) < l+r || fabs(b.y0-b.y1)<2*z*topMargin) return;
631  iWidth(fabs(b.x0-b.x1)/z);
632  iHeight((fabs(b.y0-b.y1)-2*topMargin)/z);
633  // TODO - it is wasteful to call contentBounds within computeRelZoom, then again a few lines later
634  computeRelZoom(); // needed to ensure grouped items scale properly with resize operation. for ticket 1243
635  z=zoomFactor(); // recalculate zoomFactor because relZoom changed above. for ticket 1243
636  double x0, x1, y0, y1;
637  contentBounds(x0,y0,x1,y1);
638  recentreItems(items,0.5*(x0+x1)-x(),0.5*(y0+y1)-y());
639  recentreItems(groups,0.5*(x0+x1)-x(),0.5*(y0+y1)-y());
640  double sx=(fabs(b.x0-b.x1)-z*(l+r))/(x1-x0), sy=(fabs(b.y0-b.y1)-2*z*topMargin)/(y1-y0);
641  sx=std::min(sx,sy);
642  resizeItems(items,sx,sx);
643  resizeItems(groups,sx,sx);
644 
645  moveTo(0.5*(b.x0+b.x1), 0.5*(b.y0+b.y1));
646  bb.update(*this);
647  }
648 
649  bool Group::nocycles() const
650  {
651  set<const Group*> sg;
652  sg.insert(this);
653  for (auto i=group.lock(); i; i=i->group.lock())
654  if (!sg.insert(i.get()).second)
655  return false;
656  return true;
657  }
658 
659  GroupPtr GroupItems::addGroup(const std::shared_ptr<Group>& g)
660  {
661  assert(g);
662  auto origGroup=g->group.lock();
663  if (origGroup.get()==this) return g; // nothing to do
664  if (origGroup)
665  origGroup->removeGroup(*g);
666  groups.push_back(g);
667  g->group=self;
668  g->self=groups.back();
669  assert(nocycles());
670  return groups.back();
671  }
672 
673  WirePtr GroupItems::addWire(const std::shared_ptr<Wire>& w)
674  {
675  assert(w->from() && w->to());
676  wires.push_back(w);
677  return wires.back();
678  }
679  WirePtr GroupItems::addWire
680  (const weak_ptr<Port>& fromPw, const weak_ptr<Port>& toPw, const vector<float>& coords)
681  {
682  auto fromP=fromPw.lock(), toP=toPw.lock();
683  // disallow self-wiring
684  if (!fromP || !toP || &fromP->item()==&toP->item())
685  return WirePtr();
686 
687  // wire must go from an output port to an input port
688  if (fromP->input() || !toP->input())
689  return WirePtr();
690 
691  // check that multiple input wires are only to binary ops.
692  if (!toP->wires().empty() && !toP->multiWireAllowed())
693  return WirePtr();
694 
695  // check that a wire doesn't already exist connecting these two ports
696  for (auto& w: toP->wires())
697  if (w->from()==fromP)
698  return WirePtr();
699 
700  // disallow wiring the input of an already defined variable
701  if (auto v=toP->item().variableCast())
702  if (cminsky().inputWired(v->valueId()))
703  return {};
704 
705  auto w=addWire(new Wire(fromP, toP, coords));
706  adjustWiresGroup(*w);
707 
708  return w;
709  }
710 
711 
712  bool Group::higher(const Group& x) const
713  {
714  for (auto& i: groups)
715  if (i.get()==&x) return true;
716  return any_of(groups.begin(), groups.end(), [&](const GroupPtr& i){return i->higher(x);});
717  }
718 
719  unsigned Group::level() const
720  {
721  assert(nocycles());
722  unsigned l=0;
723  for (auto i=group.lock(); i; i=i->group.lock()) l++;
724  return l;
725  }
726 
727  namespace
728  {
729  template <class G>
730  G& globalGroup(G& start)
731  {
732  auto g=&start;
733  for (auto i=start.group.lock(); i; i=i->group.lock())
734  g=i.get();
735  return *g;
736  }
737  }
738 
739  const Group& Group::globalGroup() const
740  {return minsky::globalGroup(*this);}
742  {return minsky::globalGroup(*this);}
743 
744 
745 
746 
747  bool Group::uniqueItems(set<void*>& idset) const
748  {
749  for (auto& i: items)
750  if (!idset.insert(i.get()).second) return false;
751  for (auto& i: wires)
752  if (!idset.insert(i.get()).second) return false;
753  for (auto& i: groups)
754  if (!idset.insert(i.get()).second || !i->uniqueItems(idset))
755  return false;
756  return true;
757  }
758 
759  float Group::contentBounds(double& x0, double& y0, double& x1, double& y1) const
760  {
761  const float localZoom=1;
762  x0=numeric_limits<float>::max();
763  x1=-numeric_limits<float>::max();
764  y0=numeric_limits<float>::max();
765  y1=-numeric_limits<float>::max();
766 
767  for (auto& i: items)
768  if (!i->ioVar())
769  {
770  if (i->left()<x0) x0=i->left();
771  if (i->right()>x1) x1=i->right();
772  if (i->top()<y0) y0=i->top();
773  if (i->bottom()>y1) y1=i->bottom();
774  }
775 
776  for (auto& i: groups)
777  if (i->displayContents())
778  {
779  double left, top, right, bottom;
780  i->contentBounds(left,top,right,bottom);
781  if (left<x0) x0=left;
782  if (right>x1) x1=right;
783  if (top<y0) y0=top;
784  if (bottom>y1) y1=bottom;
785  }
786  else
787  {
788  if (i->left()<x0) x0=i->left();
789  if (i->right()>x1) x1=i->right();
790  if (i->top()<y0) y0=i->top();
791  if (i->bottom()>y1) y1=i->bottom();
792  }
793 
794  // if there are no contents, result is not finite. In this case,
795  // set the content bounds to a 10x10 sized box around the centroid of the I/O variables.
796  if (x0==numeric_limits<float>::max())
797  {
798  float cx=0, cy=0;
799  for (auto& i: inVariables)
800  {
801  cx+=i->x();
802  cy+=i->y();
803  }
804  for (auto& i: outVariables)
805  {
806  cx+=i->x();
807  cy+=i->y();
808  }
809  const int n=inVariables.size()+outVariables.size();
810  if (n>0)
811  {
812  cx/=n;
813  cy/=n;
814  }
815  x0=cx-10;
816  x1=cx+10;
817  y0=cy-10;
818  y1=cy+10;
819  }
820 
821  return localZoom;
822  }
823 
824  float Group::computeDisplayZoom()
825  {
826  double x0, x1, y0, y1;
827  const float z=zoomFactor();
828  const float lz=contentBounds(x0,y0,x1,y1);
829  x0=min(x0,double(x()));
830  x1=max(x1,double(x()));
831  y0=min(y0,double(y()));
832  y1=max(y1,double(y()));
833  // first compute the value assuming margins are of zero width
834  displayZoom = 2*max( max(x1-x(), x()-x0)/(iWidth()*z), max(y1-y(), y()-y0)/(iHeight()*z));
835 
836  // account for shrinking margins
837  const float readjust=zoomFactor()/edgeScale() / (displayZoom>1? displayZoom:1);
838  float l, r;
839  margins(l,r);
840  l*=readjust; r*=readjust;
841  displayZoom = max(displayZoom,
842  float(max((x1-x())/(0.5f*iWidth()*z-r), (x()-x0)/(0.5f*iWidth()*z-l))));
843 
844  displayZoom*=1.1*rotFactor()/lz;
845 
846  // displayZoom should never be less than 1
847  displayZoom=max(displayZoom, 1.0f);
848  return displayZoom;
849  }
850 
851  void Group::computeRelZoom()
852  {
853  double x0, x1, y0, y1;
854  const double z=zoomFactor();
855  relZoom=1;
856  contentBounds(x0,y0,x1,y1);
857  float l, r;
858  margins(l,r);
859  const double dx=x1-x0, dy=y1-y0;
860  if (width()-l-r>0 && dx>0 && dy>0)
861  relZoom=std::min(1.0, std::min((width()-l-r)/(dx), (height()-20*z)/(dy)));
862  }
863 
864  const Group* Group::minimalEnclosingGroup(float x0, float y0, float x1, float y1, const Item* ignore) const
865  {
866  const float z=zoomFactor();
867  if (x0<x()-0.5*z*iWidth() || x1>x()+0.5*z*iWidth() ||
868  y0<y()-0.5*z*iHeight() || y1>y()+0.5*z*iHeight())
869  return nullptr;
870  // at this point, this is a candidate. Check if any child groups are also
871  for (auto& g: groups)
872  if (auto mg=g->minimalEnclosingGroup(x0,y0,x1,y1, ignore))
873  if (mg->visible())
874  return mg;
875  return this!=ignore? this: nullptr;
876  }
877 
878  void Group::setZoom(float factor)
879  {
880  const bool dpc=displayContents();
881  if (!group.lock())
882  relZoom=factor;
883  else
884  computeRelZoom();
885  const float lzoom=localZoom();
886  m_displayContentsChanged = dpc!=displayContents();
887  for (auto& i: groups)
888  {
889  i->setZoom(lzoom);
890  m_displayContentsChanged|=i->displayContentsChanged();
891  }
892  }
893 
894  void Group::zoom(float xOrigin, float yOrigin,float factor)
895  {
896  const bool dpc=displayContents();
897  minsky::zoom(m_x,xOrigin+m_x-x(),factor);
898  minsky::zoom(m_y,yOrigin+m_y-y(),factor);
899  m_displayContentsChanged = dpc!=displayContents();
900  if (!group.lock())
901  relZoom*=factor;
902  for (auto& i: groups)
903  {
904  if (displayContents() && !m_displayContentsChanged)
905  i->zoom(i->x(), i->y(), factor);
906  m_displayContentsChanged|=i->displayContentsChanged();
907  }
908  }
909 
910  ClickType::Type Group::clickType(float x, float y) const
911  {
912  auto z=zoomFactor();
913  const double w=0.5*iWidth()*z, h=0.5*iHeight()*z;
914  if (onResizeHandle(x,y)) return ClickType::onResize;
915  if (displayContents() && inIORegion(x,y)==IORegion::none)
916  return ClickType::outside;
917  if (auto item=select(x,y))
918  return item->clickType(x,y);
919  if ((abs(x-this->x())<w && abs(y-this->y())<h+topMargin*z)) // check also if (x,y) is within top and bottom margins of group. for feature 88
920  return ClickType::onItem;
921  return ClickType::outside;
922  }
923 
924  void Group::draw(cairo_t* cairo) const
925  {
926  auto [angle,flipped]=rotationAsRadians();
927 
928  // determine how big the group icon should be to allow
929  // sufficient space around the side for the edge variables
930  float leftMargin, rightMargin;
931  margins(leftMargin, rightMargin);
932  const float z=zoomFactor();
933 
934  const unsigned width=z*this->iWidth(), height=z*this->iHeight();
935  const cairo::CairoSave cs(cairo);
936  cairo_rotate(cairo,angle);
937 
938  // In docker environments, something invisible gets drawn outside
939  // the horizontal dimensions, stuffing up the bb.width()
940  // calculation, and then causing the groupResize test to
941  // fail. This extra clip path fixes the problem.
942  cairo_rectangle(cairo,-0.5*width,-0.5*height-topMargin*z, width, height+2*topMargin*z);
943  cairo_clip(cairo);
944 
945  // draw default group icon
946 
947  // display I/O region in grey
948  drawIORegion(cairo);
949 
950  {
951  const cairo::CairoSave cs(cairo);
952  cairo_translate(cairo, -0.5*width+leftMargin, -0.5*height);
953 
954 
955  const double scalex=double(width-leftMargin-rightMargin)/width;
956  cairo_scale(cairo, scalex, 1);
957 
958  // draw a simple frame
959  cairo_rectangle(cairo,0,0,width,height);
960  {
961  const cairo::CairoSave cs(cairo);
962  cairo_identity_matrix(cairo);
963  cairo_set_line_width(cairo,1);
964  cairo_stroke(cairo);
965  }
966 
967  if (!displayContents())
968  {
969  if (displayPlot)
970  {
971  const cairo::CairoSave cs(cairo);
972  if (flipped)
973  {
974  cairo_translate(cairo,width,height);
975  cairo_rotate(cairo,M_PI); // rotate plot to keep it right way up.
976  }
977  // propagate plot type to underling ecolab::Plot
978  auto& pt=const_cast<Plot*>(static_cast<const Plot*>(displayPlot.get()))->plotType;
979  switch (displayPlot->plotType)
980  {
981  case PlotWidget::line: pt=Plot::line; break;
982  case PlotWidget::bar: pt=Plot::bar; break;
983  default: break;
984  }
985  displayPlot->Plot::draw(cairo, width, height);
986  }
987  else
988  {
989  cairo_scale(cairo,width/svgRenderer.width(),height/svgRenderer.height());
990  cairo_rectangle(cairo,0, 0,svgRenderer.width(), svgRenderer.height());
991  cairo_clip(cairo);
992  svgRenderer.render(cairo);
993  }
994  }
995  }
996 
997  drawEdgeVariables(cairo);
998 
999 
1000  // display text label
1001  if (!title.empty())
1002  {
1003  const cairo::CairoSave cs(cairo);
1004  cairo_scale(cairo, z, z);
1005  cairo_select_font_face
1006  (cairo, "sans-serif", CAIRO_FONT_SLANT_ITALIC,
1007  CAIRO_FONT_WEIGHT_NORMAL);
1008  cairo_set_font_size(cairo,12);
1009 
1010  // extract the bounding box of the text
1011  cairo_text_extents_t bbox;
1012  cairo_text_extents(cairo,title.c_str(),&bbox);
1013  const double w=0.5*bbox.width+2;
1014  const double h=0.5*bbox.height+5;
1015 
1016  // if rotation is in 1st or 3rd quadrant, rotate as
1017  // normal, otherwise flip the text so it reads L->R
1018  if (flipped)
1019  cairo_rotate(cairo, M_PI);
1020 
1021  // prepare a background for the text, partially obscuring graphic
1022  const double transparency=displayContents()? 0.25: 1;
1023 
1024  // display text
1025  cairo_move_to(cairo, -w+1, h-12-0.5*(height)/z);
1026  cairo_set_source_rgba(cairo,0,0,0,transparency);
1027  cairo_show_text(cairo,title.c_str());
1028  }
1029 
1030  if (mouseFocus)
1031  {
1032  displayTooltip(cairo,tooltip());
1033  // Resize handles always visible on mousefocus. For ticket 92.
1034  drawResizeHandles(cairo);
1035  }
1036 
1037  cairo_rectangle(cairo,-0.5*width,-0.5*height,width,height);
1038  cairo_clip(cairo);
1039  if (selected)
1040  drawSelected(cairo);
1041 
1042  }
1043 
1044  void Group::draw1edge(const vector<VariablePtr>& vars, cairo_t* cairo,
1045  float x) const
1046  {
1047  float top=0, bottom=0;
1048  const double angle=rotation() * M_PI / 180.0;
1049  for (size_t i=0; i<vars.size(); ++i)
1050  {
1051  const Rotate r(rotation(),0,0);
1052  auto& v=vars[i];
1053  float y=0;
1054  auto z=v->zoomFactor();
1055  auto t=v->bb.top()*z, b=v->bb.bottom()*z;
1056  if (i>0) y = i%2? top-b: bottom-t;
1057  v->moveTo(r.x(x,y)+this->x(), r.y(x,y)+this->y());
1058  const cairo::CairoSave cs(cairo);
1059  cairo_translate(cairo,x,y);
1060  // cairo context is already rotated, so antirotate
1061  cairo_rotate(cairo,-angle);
1062  v->rotation(rotation());
1063  v->draw(cairo);
1064  if (i==0)
1065  {
1066  bottom=b;
1067  top=t;
1068  }
1069  else if (i%2)
1070  top-=v->height();
1071  else
1072  bottom+=v->height();
1073  }
1074  }
1075 
1076  void Group::drawEdgeVariables(cairo_t* cairo) const
1077  {
1078  float left, right; margins(left,right);
1079  const cairo::CairoSave cs(cairo);
1080  const float z=zoomFactor();
1081  draw1edge(inVariables, cairo, -0.5*(iWidth()*z-left));
1082  draw1edge(outVariables, cairo, 0.5*(iWidth()*z-right));
1083  }
1084 
1085  namespace
1086  {
1087  // return the y position of the notch
1088  float notchY(const vector<VariablePtr>& vars)
1089  {
1090  if (vars.empty()) return 0;
1091  const float z=vars[0]->zoomFactor();
1092  float top=vars[0]->bb.top()*z, bottom=vars[0]->bb.top()*z;
1093  for (size_t i=0; i<vars.size(); ++i)
1094  {
1095  if (i%2)
1096  top-=vars[i]->height();
1097  else
1098  bottom+=vars[i]->height();
1099  }
1100  return vars.size()%2? top-vars.back()->bb.bottom()*z: bottom-vars.back()->bb.top()*z;
1101  }
1102  }
1103 
1104  // draw notches in the I/O region to indicate docking capability
1105  void Group::drawIORegion(cairo_t* cairo) const
1106  {
1107  const cairo::CairoSave cs(cairo);
1108  float left, right;
1109  margins(left,right);
1110  const float z=zoomFactor();
1111  float y=notchY(inVariables), dy=topMargin*edgeScale();
1112  cairo_set_source_rgba(cairo,0,1,1,0.5);
1113  const float w=0.5*z*iWidth(), h=0.5*z*iHeight();
1114 
1115  cairo_move_to(cairo,-w,-h);
1116  // create notch in input region
1117  cairo_line_to(cairo,-w,y-dy);
1118  cairo_line_to(cairo,left-w-4*z,y-dy);
1119  cairo_line_to(cairo,left-w,y);
1120  cairo_line_to(cairo,left-w-4*z,y+dy);
1121  cairo_line_to(cairo,-w,y+dy);
1122  cairo_line_to(cairo,-w,h);
1123  cairo_line_to(cairo,left-w,h);
1124  cairo_line_to(cairo,left-w,-h);
1125  cairo_close_path(cairo);
1126  cairo_fill(cairo);
1127 
1128  y=notchY(outVariables);
1129  cairo_move_to(cairo,w,-h);
1130  // create notch in output region
1131  cairo_line_to(cairo,w,y-dy);
1132  cairo_line_to(cairo,w-right-2*z,y-dy);
1133  cairo_line_to(cairo,w-right+2*z,y);
1134  cairo_line_to(cairo,w-right-2*z,y+dy);
1135  cairo_line_to(cairo,w,y+dy);
1136  cairo_line_to(cairo,w,h);
1137  cairo_line_to(cairo,w-right,h);
1138  cairo_line_to(cairo,w-right,-h);
1139  cairo_close_path(cairo);
1140  cairo_fill(cairo);
1141 
1142  // draw top margin. for feature 88
1143  cairo_rectangle(cairo,-w,-h,2*w,-topMargin*z);
1144  cairo_fill(cairo);
1145 
1146  // draw bottom margin. for feature 88
1147  cairo_rectangle(cairo,-w,h,2*w,topMargin*z);
1148  cairo_fill(cairo);
1149  }
1150 
1151 
1152  void Group::margins(float& left, float& right) const
1153  {
1154  left=right=10;
1155  auto tmpMouseFocus=mouseFocus;
1156  mouseFocus=false; // disable mouseFocus for this calculation
1157  for (auto& i: inVariables)
1158  {
1159  assert(i->type()!=VariableType::undefined);
1160  i->bb.update(*i);
1161  if (i->width()>left) left=i->width();
1162  }
1163  for (auto& i: outVariables)
1164  {
1165  assert(i->type()!=VariableType::undefined);
1166  i->bb.update(*i);
1167  if (i->width()>right) right=i->width();
1168  }
1169  mouseFocus=tmpMouseFocus;
1170  }
1171 
1172  float Group::rotFactor() const
1173  {
1174  float rotFactor;
1175  const float ac=abs(cos(rotation()*M_PI/180));
1176  static const float invSqrt2=1/sqrt(2);
1177  if (ac>=invSqrt2)
1178  rotFactor=1.15/ac; //use |1/cos(angle)| as rotation factor
1179  else
1180  rotFactor=1.15/sqrt(1-ac*ac);//use |1/sin(angle)| as rotation factor
1181  return rotFactor;
1182  }
1183 
1184  ItemPtr Group::select(float x, float y) const
1185  {
1186  for (auto& v: inVariables)
1187  if (RenderVariable(*v).inImage(x,y))
1188  return v;
1189  for (auto& v: outVariables)
1190  if (RenderVariable(*v).inImage(x,y))
1191  return v;
1192  return nullptr;
1193  }
1194 
1195  void Group::normaliseGroupRefs(const shared_ptr<Group>& self)
1196  {
1197  for (auto& i: items)
1198  i->group=self;
1199  for (auto& g: groups)
1200  {
1201  g->group=self;
1202  g->normaliseGroupRefs(g);
1203  }
1204  }
1205 
1206 
1207  void Group::flipContents()
1208  {
1209  for (auto& i: items)
1210  {
1211  i->moveTo(x()-i->m_x,i->y());
1212  i->flip();
1213  }
1214  for (auto& i: groups)
1215  {
1216  i->moveTo(x()-i->m_x,i->y());
1217  i->flip();
1218  }
1219  }
1220 
1221  vector<string> Group::accessibleVars() const
1222 {
1223  set<string> r;
1224  // first add local variables
1225  for (auto& i: items)
1226  if (auto v=i->variableCast())
1227  r.insert(v->name());
1228  // now add variables in outer scopes, ensuring they qualified
1229  auto g=this;
1230  for (g=g->group.lock().get(); g; g=g->group.lock().get())
1231  for (auto& i: g->items)
1232  if (auto v=i->variableCast())
1233  {
1234  auto n=v->name();
1235  if (n[0]==':')
1236  r.insert(n);
1237  else
1238  r.insert(':'+n);
1239  }
1240 
1241  return vector<string>(r.begin(),r.end());
1242 }
1243 
1244  void Group::gotoBookmark_b(const Bookmark& b)
1245  {
1246  moveTo(b.x, b.y);
1247  zoom(x(),y(),b.zoom/(relZoom*zoomFactor()));
1248  // TODO add canvas::gotoBookmark to avoid dependency inversion
1249  if (this==cminsky().canvas.model.get()) minsky().canvas.requestRedraw();
1250  }
1251 
1252  std::string Group::defaultExtension() const
1253  {
1254  if (findAny(&GroupItems::items, [](const ItemPtr& i){return dynamic_cast<Ravel*>(i.get());}))
1255  return ".rvl";
1256  return ".mky";
1257  }
1258 
1259  void Group::autoLayout()
1260  {
1261  const BusyCursor busy(minsky());
1262  layoutGroup(*this);
1263  }
1264 
1265  void Group::randomLayout()
1266  {
1267  const BusyCursor busy(minsky());
1268  randomizeLayout(*this);
1269  }
1270 
1271  vector<Summary> Group::summariseGodleys() const
1272  {
1273  vector<Summary> r;
1274  recursiveDo(&GroupItems::items, [&](const Items&,Items::const_iterator i) {
1275  if (auto g=dynamic_cast<GodleyIcon*>(i->get()))
1276  {
1277  auto summary=g->summarise();
1278  r.insert(r.end(),summary.begin(), summary.end());
1279  }
1280  return false;
1281  });
1282  return r;
1283  }
1284 
1285  void Group::renameAllInstances(const std::string& valueId, const std::string& newName)
1286  {
1287  // unqualified versions of the names
1288  auto p=valueId.find(':');
1289  string uqFromName=(p!=string::npos)? valueId.substr(p+1): valueId;
1290  string uqNewName = newName.substr(newName[0]==':'? 1: 0);
1291  set<GodleyIcon*> godleysToUpdate;
1292 #ifndef NDEBUG
1293  auto numItems=this->numItems();
1294 #endif
1295  recursiveDo
1296  (&GroupItems::items, [&](Items&,Items::iterator i)
1297  {
1298  if (auto v=(*i)->variableCast())
1299  if (v->valueId()==valueId)
1300  {
1301  auto varScope=scope(v->group.lock(), valueId);
1302  if (auto g=dynamic_cast<GodleyIcon*>(v->controller.lock().get()))
1303  {
1304  if (varScope==g->group.lock() ||
1305  (!varScope && g->group.lock()==cminsky().model)) // fix local variables
1306  g->table.rename(uqFromName, uqNewName);
1307 
1308  // scope of an external ref in the Godley Table
1309  auto externalVarScope=scope(g->group.lock(), ':'+uqNewName);
1310  // if we didn't find it, perhaps the outerscope variable hasn't been changed
1311  if (!externalVarScope)
1312  externalVarScope=scope(g->group.lock(), ':'+uqFromName);
1313 
1314  if (varScope==externalVarScope || (isGlobal(varScope) && isGlobal(externalVarScope)))
1315  // fix external variable references
1316  g->table.rename(':'+uqFromName, ':'+uqNewName);
1317  // GodleyIcon::update invalidates the iterator, so postpone update
1318  godleysToUpdate.insert(g);
1319  }
1320  else
1321  {
1322  // ensure locality is preserved across the rename
1323  v->name(varScope==v->group.lock()? uqNewName: (':'+uqNewName));
1324  if (auto vv=v->vValue())
1325  v->retype(vv->type()); // ensure correct type. Note this invalidates v.
1326  }
1327  }
1328  return false;
1329  });
1330  assert(this->numItems()==numItems);
1331  for (auto g: godleysToUpdate)
1332  {
1333  g->update();
1334  assert(this->numItems()==numItems);
1335  }
1336  minsky().requestReset(); // Updates model after variables rename. For ticket 1109.
1337  }
1338 
1339 }
1340 
#define M_PI
some useful geometry types, defined from boost::geometry
Definition: geometry.h:29
function f
Definition: canvas.m:1
zoomfactor
Definition: wiring.tcl:288
Expr sin(const Expr &x)
Definition: expr.h:131
const std::string & rawName() const
accessor for the name member (may differ from name() with top level variables)
Definition: variable.h:150
Expr cos(const Expr &x)
Definition: expr.h:137
void requestReset()
Definition: minsky.cc:467
bool inputWired(const std::string &name) const
returns true if any variable of name name has a wired input
Definition: minsky.h:399
virtual void runItemDeletedCallback(const Item &)
run callback attached to item
Definition: minsky.h:459
Expr sqrt(const Expr &x)
Definition: expr.h:154
minsky::Minsky minsky
Definition: pyminsky.cc:28
represents rectangular region of a lasso operation
Definition: lasso.h:28
STL namespace.
float y(float x, float y) const
Definition: geometry.h:60
size_t scope(const string &name)
extract scope from a qualified variable name
Definition: valueId.cc:83
void populateEvalOpVector(EvalOpVector &equations, std::vector< Integral > &integrals)
Definition: equations.cc:1011
std::shared_ptr< Item > ItemPtr
Definition: item.h:57
float notchY(const vector< VariablePtr > &vars)
Definition: group.cc:1088
std::string uqName(const std::string &name)
extract unqualified portion of name
Definition: valueId.cc:135
std::shared_ptr< Wire > WirePtr
Definition: wire.h:102
string valueId(const string &name)
construct a valueId from fully qualified name @ name should not be canonicalised
Definition: valueId.cc:75
rotate (x,y) by rot (in degrees) around the origin (x0, y0) can be used for rotating multiple points ...
Definition: geometry.h:44
bool flipped(double rotation)
returns if the angle (in degrees) is in the second or third quadrant
Definition: geometry.h:102
struct TCLcmd::trap::init_t init
void recentreItems(const T &items, float xc, float yc)
Definition: group.cc:611
void randomizeLayout(Group &g)
randomly place items on canvas
Definition: autoLayout.cc:88
Creation and access to the minskyTCL_obj object, which has code to record whenever Minsky&#39;s state cha...
Definition: constMap.h:22
classdesc::Exclude< std::weak_ptr< Item > > controller
reference to a controlling item - eg GodleyIcon, IntOp or a Group if an IOVar.
Definition: variable.h:129
std::vector< ItemPtr > Items
Definition: item.h:366
static const int topMargin
Definition: group.cc:37
Canvas canvas
Definition: minsky.h:256
void clear()
Definition: group.h:91
virtual void bookmarkRefresh()
refresh the bookmark menu after changes
Definition: minsky.h:450
void asgClonedPort(shared_ptr< Port > &p, const map< Item *, ItemPtr > &cloneMap)
Definition: group.cc:106
const Minsky & cminsky()
const version to help in const correctness
Definition: minsky.h:549
CLASSDESC_ACCESS_EXPLICIT_INSTANTIATION(minsky::Group)
bool inImage(float x, float y)
return the boost geometry corresponding to this variable&#39;s shape
Definition: cairoItems.cc:89
VariableDAGPtr getNodeFromVar(const VariableBase &v)
Definition: equations.cc:882
void requestRedraw()
request a redraw on the screen
Definition: canvas.h:292
Groups groups
Definition: group.h:79
bool higher(const Group &) const
returns true if this is higher in the heirarchy than the argument this->higher(*this) is false ...
Definition: group.cc:712
virtual std::string name() const
variable displayed name
Definition: variable.cc:201
float x0
Definition: lasso.h:30
void resizeItems(T &items, double sx, double sy)
Definition: group.cc:598
void layoutGroup(Group &g)
auto layout group items
Definition: autoLayout.cc:101
std::string bookmarkId() const
Id of bookmark associated with this.
Definition: item.h:251
float y1
Definition: lasso.h:30
float x1
Definition: lasso.h:30
std::shared_ptr< Group > GroupPtr
Definition: port.h:32
GroupPtr model
Definition: minsky.h:255
bool isGlobal(const GroupPtr &g)
Definition: group.cc:46
float y0
Definition: lasso.h:30
Definition: group.tcl:84
void zoom(float &val, float origin, float factor)
base zooming transformation
Definition: zoom.h:26
string to_string(CONST84 char *x)
Definition: minskyTCLObj.h:33
float x(float x, float y) const
Definition: geometry.h:59