linny-r 1.6.4 → 1.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -305,14 +305,15 @@ class ActorManager {
305
305
  // Set its round flags
306
306
  a.round_flags = ali[2];
307
307
  // Double-check: parse expression if weight has been changed.
308
- if(a.weight.text != ali[3]) {
309
- xp.expr = monoSpacedVariables(ali[3]);
308
+ const awx = monoSpacedVariables(ali[3]);
309
+ if(a.weight.text != awx) {
310
+ xp.expr = awx;
310
311
  xp.compile();
311
312
  if(xp.error) {
312
313
  UI.warningInvalidWeightExpression(a, xp.error);
313
314
  ok = false;
314
315
  } else {
315
- a.weight.update(ali[3]);
316
+ a.weight.update(xp);
316
317
  }
317
318
  }
318
319
  // Update import/export status
@@ -549,9 +549,9 @@ class GUIController extends Controller {
549
549
  this.buttons.actors.addEventListener('click',
550
550
  () => ACTOR_MANAGER.showDialog());
551
551
  this.buttons.diagram.addEventListener('click',
552
- () => FILE_MANAGER.renderDiagramAsPNG());
552
+ () => FILE_MANAGER.renderDiagramAsPNG(event.shiftKey));
553
553
  this.buttons.savediagram.addEventListener('click',
554
- () => FILE_MANAGER.saveDiagramAsSVG());
554
+ () => FILE_MANAGER.saveDiagramAsSVG(event.shiftKey));
555
555
  this.buttons.receiver.addEventListener('click',
556
556
  () => RECEIVER.toggle());
557
557
  // NOTE: All draggable & resizable dialogs "toggle" show/hide.
@@ -332,6 +332,9 @@ class GUIExperimentManager extends ExperimentManager {
332
332
  document.getElementById('xp-reset-btn').classList.add('off');
333
333
  }
334
334
  this.updateParameters();
335
+ // NOTE: When UpdateDialog is called after an entity has been renamed,
336
+ // its variable list should be updated.
337
+ this.updateViewerVariable();
335
338
  }
336
339
 
337
340
  updateParameters() {
@@ -515,6 +518,10 @@ class GUIExperimentManager extends ExperimentManager {
515
518
  addDistinct(vn, vl);
516
519
  }
517
520
  vl.sort((a, b) => UI.compareFullNames(a, b));
521
+ // NOTE: When the selected variable entity has been renamed, its
522
+ // name will not be in the list (and its old name cannot be inferred)
523
+ // so then clear it.
524
+ if(vl.indexOf(x.selected_variable) < 0) x.selected_variable = '';
518
525
  for(let i = 0; i < vl.length; i++) {
519
526
  const vn = vl[i];
520
527
  // NOTE: FireFox selector dropdown areas have a pale gray
@@ -342,11 +342,18 @@ class GUIFileManager {
342
342
  });
343
343
  }
344
344
 
345
- renderDiagramAsPNG() {
345
+ renderDiagramAsPNG(tight) {
346
+ // When `tight` is TRUE, add no whitespace around the diagram.
346
347
  window.localStorage.removeItem('png-url');
347
- UI.paper.fitToSize();
348
- MODEL.alignToGrid();
349
- this.renderSVGAsPNG(UI.paper.svg.outerHTML);
348
+ if(tight) {
349
+ // First align to grid and then fit to size.
350
+ MODEL.alignToGrid();
351
+ UI.paper.fitToSize(1);
352
+ } else {
353
+ UI.paper.fitToSize();
354
+ MODEL.alignToGrid();
355
+ }
356
+ this.renderSVGAsPNG(UI.paper.opaqueSVG);
350
357
  }
351
358
 
352
359
  renderSVGAsPNG(svg) {
@@ -374,10 +381,17 @@ class GUIFileManager {
374
381
  .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
375
382
  }
376
383
 
377
- saveDiagramAsSVG() {
378
- UI.paper.fitToSize();
379
- MODEL.alignToGrid();
380
- this.pushOutSVG(UI.paper.outerHTML);
384
+ saveDiagramAsSVG(tight) {
385
+ // Output SVG as string with nodes and arrows 100% opaque.
386
+ if(tight) {
387
+ // First align to grid and then fit to size.
388
+ MODEL.alignToGrid();
389
+ UI.paper.fitToSize(1);
390
+ } else {
391
+ UI.paper.fitToSize();
392
+ MODEL.alignToGrid();
393
+ }
394
+ this.pushOutSVG(UI.paper.opaqueSVG);
381
395
  }
382
396
 
383
397
  pushOutSVG(svg) {
@@ -132,7 +132,11 @@ class Shape {
132
132
  const ts = UI.paper.newSVGElement('tspan');
133
133
  ts.setAttribute('x', x);
134
134
  ts.setAttribute('dy', fh);
135
- ts.textContent = lines[i];
135
+ // NOTE: Non-breaking space must now (inside a TSPAN) be converted
136
+ // to normal spaces, or they will be rendered as '&nbsp;' and this
137
+ // will cause the SVG to break when it is inserted as picture into
138
+ // an MS Word document.
139
+ ts.textContent = lines[i].replaceAll('\u00A0', ' ');
136
140
  el.appendChild(ts);
137
141
  }
138
142
  this.element.appendChild(el);
@@ -348,6 +352,13 @@ class Paper {
348
352
  this.clear();
349
353
  }
350
354
 
355
+ get opaqueSVG() {
356
+ // Return SVG as string with nodes and arrows 100% opaque.
357
+ // NOTE: The semi-transparent ovals behind rates on links have
358
+ // opacity 0.8 and hence are not affected.
359
+ return this.svg.outerHTML.replaceAll(' opacity="0.9"', ' opacity="1"');
360
+ }
361
+
351
362
  clear() {
352
363
  // First, clear the entire SVG
353
364
  this.clearSVGElement(this.svg);
@@ -471,12 +482,12 @@ class Paper {
471
482
  }
472
483
 
473
484
  clearSVGElement(el) {
474
- // Clears all sub-nodes of the specified SVG node
485
+ // Clear all sub-nodes of the specified SVG node.
475
486
  if(el) while(el.lastChild) el.removeChild(el.lastChild);
476
487
  }
477
488
 
478
489
  addSVGAttributes(el, obj) {
479
- // Adds attributes specified by `obj` to (SVG) element `el`
490
+ // Add attributes specified by `obj` to (SVG) element `el`.
480
491
  for(let prop in obj) {
481
492
  if(obj.hasOwnProperty(prop)) el.setAttribute(prop, obj[prop]);
482
493
  }
@@ -686,35 +697,35 @@ class Paper {
686
697
  return el;
687
698
  }
688
699
 
689
- fitToSize() {
700
+ fitToSize(margin=30) {
690
701
  // Adjust the dimensions of the main SVG to fit the graph plus 15px margin
691
702
  // all around
692
703
  this.removeInvisibleSVG();
693
704
  const
694
705
  bb = this.svg.getBBox(),
695
- w = bb.width + 30,
696
- h = bb.height + 30;
706
+ w = bb.width + margin,
707
+ h = bb.height + margin;
697
708
  if(w !== this.width || h !== this.height) {
698
- MODEL.translateGraph(-bb.x + 15, -bb.y + 25);
709
+ MODEL.translateGraph(-bb.x + margin / 2, -bb.y + margin);
699
710
  this.width = w;
700
711
  this.height = h;
701
712
  this.svg.setAttribute('width', this.width);
702
713
  this.svg.setAttribute('height', this.height);
703
714
  this.zoom_factor = 1;
704
715
  this.zoom_label.innerHTML = Math.round(100 / this.zoom_factor) + '%';
705
- this.extend();
716
+ this.extend(margin);
706
717
  }
707
718
  }
708
719
 
709
- extend() {
720
+ extend(margin=30) {
710
721
  // Adjust the paper size to fit all objects WITHOUT changing the origin (0, 0)
711
722
  // NOTE: keep a minimum page size to keep the scrolling more "natural"
712
723
  this.removeInvisibleSVG();
713
724
  const
714
725
  bb = this.svg.getBBox(),
715
726
  // Let `w` and `h` be the actual width and height in pixels
716
- w = bb.x + bb.width + 30,
717
- h = bb.y + bb.height + 30,
727
+ w = bb.x + bb.width + margin,
728
+ h = bb.y + bb.height + margin,
718
729
  // Let `ccw` and `cch` be the size of the scrollable area
719
730
  ccw = w / this.zoom_factor,
720
731
  cch = h / this.zoom_factor;
@@ -1391,7 +1402,7 @@ class Paper {
1391
1402
  // Add 2px margin
1392
1403
  shift = 2;
1393
1404
  const lfd = (luc.actualDelay(MODEL.t));
1394
- if(lfd > 0) {
1405
+ if(lfd != 0) {
1395
1406
  // If delay, draw it in a circle behind arrow head
1396
1407
  s = lfd;
1397
1408
  bb = this.numberSize(s, 7);
@@ -1419,6 +1430,7 @@ class Paper {
1419
1430
  {stroke:stroke_color, 'stroke-width': 0.5, fill: 'white'});
1420
1431
  // MU symbol does not center prettily => raise by 1 px
1421
1432
  const raise = (luc.multiplier === VM.LM_MEAN ||
1433
+ luc.multiplier === VM.LM_STARTUP ||
1422
1434
  luc.multiplier === VM.LM_THROUGHPUT ? 1 :
1423
1435
  (luc.multiplier === VM.LM_PEAK_INC ? 1.5 : 0));
1424
1436
  arrw.shape.addText(epx, epy - raise, VM.LM_SYMBOLS[luc.multiplier],
@@ -1462,12 +1474,13 @@ class Paper {
1462
1474
  epy = arrw.from_y + (shift + bi) * dy / l;
1463
1475
  font_color = this.palette.produced;
1464
1476
  }
1465
- // Draw the rate in a semi-transparent white ellipse
1466
- arrw.shape.addEllipse(epx, epy, tw/2, th/2, {fill: 'white', opacity: 0.8});
1477
+ // Draw the rate in a semi-transparent white roundbox.
1478
+ arrw.shape.addRect(epx, epy, tw, th,
1479
+ {fill: 'white', opacity: 0.8, rx: 2, ry: 2});
1467
1480
  arrw.shape.addNumber(epx, epy, s, {fill: font_color, 'font-style': rrfs});
1468
1481
 
1469
1482
  // Draw the share of cost (only if relevant and > 0) behind the rate
1470
- // in a pale yellow filled box
1483
+ // in a pale yellow filled box.
1471
1484
  if(MODEL.infer_cost_prices && luc.share_of_cost > 0) {
1472
1485
  // Keep the right distance from the rate: the midpoint should
1473
1486
  // increase by a varying length: number lengths / 2 when arrow is
@@ -8437,8 +8437,8 @@ class Link {
8437
8437
  // Scales delay expression value to number of time steps on model
8438
8438
  // time scale
8439
8439
  let d = Math.floor(VM.SIG_DIF_FROM_ZERO + this.flow_delay.result(t));
8440
- // NOTE: negative values are interpreted as 0 (no warning)
8441
- if(d <= 0) return 0;
8440
+ // NOTE: Negative values are permitted. This might invalidate cost
8441
+ // price calculation -- to be checked!!
8442
8442
  return d;
8443
8443
  }
8444
8444
 
@@ -4295,7 +4295,7 @@ class VirtualMachine {
4295
4295
  cv = this.chunk_variables[ci - this.chunk_offset];
4296
4296
  }
4297
4297
  // NOTE: Do not scale the coefficient of the cash variable.
4298
- if(!cv[0].startsWith('C')) cc[ci] *= m;
4298
+ if(cv && !cv[0].startsWith('C')) cc[ci] *= m;
4299
4299
  }
4300
4300
  }
4301
4301
  }