linny-r 2.0.12 → 2.1.1
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 +1 -1
- package/static/images/update.png +0 -0
- package/static/index.html +34 -3
- package/static/linny-r.css +42 -3
- package/static/scripts/linny-r-ctrl.js +6 -0
- package/static/scripts/linny-r-gui-actor-manager.js +6 -6
- package/static/scripts/linny-r-gui-chart-manager.js +54 -32
- package/static/scripts/linny-r-gui-controller.js +52 -35
- package/static/scripts/linny-r-gui-dataset-manager.js +7 -16
- package/static/scripts/linny-r-gui-monitor.js +3 -2
- package/static/scripts/linny-r-gui-paper.js +10 -1
- package/static/scripts/linny-r-gui-repository-browser.js +304 -17
- package/static/scripts/linny-r-gui-undo-redo.js +41 -52
- package/static/scripts/linny-r-model.js +429 -308
- package/static/scripts/linny-r-utils.js +21 -23
- package/static/scripts/linny-r-vm.js +103 -17
@@ -115,13 +115,16 @@ class Repository {
|
|
115
115
|
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
116
116
|
}
|
117
117
|
|
118
|
-
loadModule(n, include=false) {
|
119
|
-
//
|
118
|
+
loadModule(n, include=false, update=false) {
|
119
|
+
// Load Linny-R model with index `n` from this repository.
|
120
120
|
// NOTES:
|
121
|
-
// (1)
|
121
|
+
// (1) When `include` is FALSE, this function behaves as the `loadModel`
|
122
122
|
// method of FileManager; when `include` is TRUE, the module is included
|
123
|
-
// as a cluster (with parameterization via an IO context)
|
124
|
-
// (2)
|
123
|
+
// as a cluster (with parameterization via an IO context).
|
124
|
+
// (2) When `update` is TRUE, all clusters that were previously included
|
125
|
+
// using the specified module are replaced by clusters for the (new)
|
126
|
+
// module using the bindings inferred from the existing cluster.
|
127
|
+
// (3) Loading a module requires no authentication.
|
125
128
|
fetch('repo/', postData({
|
126
129
|
action: 'load',
|
127
130
|
repo: this.name,
|
@@ -135,12 +138,18 @@ class Repository {
|
|
135
138
|
})
|
136
139
|
.then((data) => {
|
137
140
|
if(data !== '' && UI.postResponseOK(data)) {
|
138
|
-
// Server returns Linny-R model file
|
141
|
+
// Server returns Linny-R model file.
|
139
142
|
if(include) {
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
143
|
+
const xml = parseXML(data.replace(/%23/g, '#'));
|
144
|
+
if(update) {
|
145
|
+
// Include module into current model.
|
146
|
+
REPOSITORY_BROWSER.promptForUpdate(
|
147
|
+
this.name, this.module_names[n], xml);
|
148
|
+
} else {
|
149
|
+
// Include module into current model.
|
150
|
+
REPOSITORY_BROWSER.promptForInclusion(
|
151
|
+
this.name, this.module_names[n], xml);
|
152
|
+
}
|
144
153
|
} else {
|
145
154
|
if(UI.loadModelFromXML(data)) {
|
146
155
|
UI.notify(`Model <tt>${this.module_names[n]}</tt> ` +
|
@@ -153,8 +162,8 @@ class Repository {
|
|
153
162
|
}
|
154
163
|
|
155
164
|
storeModelAsModule(name, black_box=false) {
|
156
|
-
//
|
157
|
-
// NOTE:
|
165
|
+
// Store the current model in this repository.
|
166
|
+
// NOTE: This requires authentication.
|
158
167
|
UI.waitingCursor();
|
159
168
|
fetch('repo/', postData({
|
160
169
|
action: 'store',
|
@@ -246,6 +255,8 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
246
255
|
'click', () => REPOSITORY_BROWSER.includeModule());
|
247
256
|
document.getElementById('repo-load-btn').addEventListener(
|
248
257
|
'click', () => REPOSITORY_BROWSER.confirmLoadModuleAsModel());
|
258
|
+
document.getElementById('repo-update-btn').addEventListener(
|
259
|
+
'click', () => REPOSITORY_BROWSER.includeModule(true));
|
249
260
|
document.getElementById('repo-store-btn').addEventListener(
|
250
261
|
'click', () => REPOSITORY_BROWSER.promptForStoring());
|
251
262
|
document.getElementById('repo-black-box-btn').addEventListener(
|
@@ -294,6 +305,14 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
294
305
|
this.include_modal.element('actor').addEventListener(
|
295
306
|
'blur', () => REPOSITORY_BROWSER.updateActors());
|
296
307
|
|
308
|
+
this.update_modal = new ModalDialog('update');
|
309
|
+
this.update_modal.ok.addEventListener(
|
310
|
+
'click', () => REPOSITORY_BROWSER.performUpdate());
|
311
|
+
this.update_modal.cancel.addEventListener(
|
312
|
+
'click', () => REPOSITORY_BROWSER.cancelUpdate());
|
313
|
+
this.update_modal.element('module').addEventListener(
|
314
|
+
'change', () => REPOSITORY_BROWSER.checkUpdateBindings());
|
315
|
+
|
297
316
|
this.confirm_load_modal = new ModalDialog('confirm-load-from-repo');
|
298
317
|
this.confirm_load_modal.ok.addEventListener(
|
299
318
|
'click', () => REPOSITORY_BROWSER.loadModuleAsModel());
|
@@ -566,6 +585,13 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
566
585
|
this.modules_count.innerHTML = pluralS(mcount, 'module');
|
567
586
|
if(this.module_index >= 0) {
|
568
587
|
UI.enableButtons('repo-load repo-include');
|
588
|
+
// Enable update button only when model contains included clusters.
|
589
|
+
const ninc = Object.keys(MODEL.includedModules).length;
|
590
|
+
if(ninc) {
|
591
|
+
UI.enableButtons(' repo-update');
|
592
|
+
} else {
|
593
|
+
UI.disableButtons(' repo-update');
|
594
|
+
}
|
569
595
|
// NOTE: Only allow deletion from local host repository.
|
570
596
|
if(this.repository_index === 0 && this.isLocalHost) {
|
571
597
|
UI.enableButtons(' repo-delete');
|
@@ -573,7 +599,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
573
599
|
UI.disableButtons(' repo-delete');
|
574
600
|
}
|
575
601
|
} else {
|
576
|
-
UI.disableButtons('repo-load repo-include repo-delete');
|
602
|
+
UI.disableButtons('repo-load repo-include repo-update repo-delete');
|
577
603
|
}
|
578
604
|
}
|
579
605
|
|
@@ -604,6 +630,10 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
604
630
|
}
|
605
631
|
this.updateModulesTable();
|
606
632
|
}
|
633
|
+
|
634
|
+
//
|
635
|
+
// Methods that implement the inclusion of the selected module.
|
636
|
+
//
|
607
637
|
|
608
638
|
promptForInclusion(repo, file, node) {
|
609
639
|
// Add entities defined in the parsed XML tree with root `node`.
|
@@ -631,7 +661,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
631
661
|
if(ids.indexOf(oid) >= 0) sel.value = oid;
|
632
662
|
}
|
633
663
|
}
|
634
|
-
|
664
|
+
|
635
665
|
updateActors() {
|
636
666
|
// Add actor (if specified) to model, and then updates the selector options
|
637
667
|
// for each actor binding selector.
|
@@ -709,8 +739,11 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
709
739
|
`<strong>${IO_CONTEXT.repo_name}</strong>${counts}`);
|
710
740
|
// Get the containing cluster.
|
711
741
|
obj = MODEL.objectByName(IO_CONTEXT.clusterName);
|
712
|
-
// Position it in the focal cluster.
|
713
742
|
if(obj instanceof Cluster) {
|
743
|
+
// Record from which module it has been included with what bindings.
|
744
|
+
obj.module = {name: IO_CONTEXT.file_name,
|
745
|
+
bindings: IO_CONTEXT.copyOfBindings};
|
746
|
+
// Position it in the focal cluster.
|
714
747
|
obj.x = IO_CONTEXT.centroid_x;
|
715
748
|
obj.y = IO_CONTEXT.centroid_y;
|
716
749
|
obj.clearAllProcesses();
|
@@ -734,6 +767,260 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
734
767
|
IO_CONTEXT = null;
|
735
768
|
this.include_modal.hide();
|
736
769
|
}
|
770
|
+
|
771
|
+
//
|
772
|
+
// Methods that implement updating of perviously included clusters.
|
773
|
+
//
|
774
|
+
|
775
|
+
promptForUpdate(repo, file, node) {
|
776
|
+
// Add entities defined in the parsed XML tree with root `node`.
|
777
|
+
this.included_modules = MODEL.includedModules;
|
778
|
+
const
|
779
|
+
md = this.update_modal,
|
780
|
+
options = [],
|
781
|
+
keys = Object.keys(this.included_modules).sort(compareWithTailNumbers),
|
782
|
+
// Use file name without tail number as basis for comparison.
|
783
|
+
fwot = file.replace(/\-\d+$/, '');
|
784
|
+
// Do not prompt if no modules referenced by clusters.
|
785
|
+
if(!keys.length) return;
|
786
|
+
IO_CONTEXT = new IOContext(repo, file, node);
|
787
|
+
let max = 0,
|
788
|
+
index = -1,
|
789
|
+
mcnt = '';
|
790
|
+
for(const k of keys) {
|
791
|
+
const tn = endsWithDigits(k);
|
792
|
+
if(tn && k === `${fwot}-${tn}`) {
|
793
|
+
max = Math.max(max, parseInt(tn));
|
794
|
+
index = options.length;
|
795
|
+
mcnt = `(${pluralS(this.included_modules[k].length, 'cluster')})`;
|
796
|
+
}
|
797
|
+
options.push('<option value="', k, '">', k, '</option>');
|
798
|
+
}
|
799
|
+
if(index >= 0) options[index].replace('">', '" selected>');
|
800
|
+
md.element('name').innerHTML = IO_CONTEXT.file_name;
|
801
|
+
md.element('module').innerHTML = options.join('');
|
802
|
+
md.element('count').innerText = mcnt;
|
803
|
+
md.element('issues').style.display = 'none';
|
804
|
+
md.element('remove').style.display = 'none';
|
805
|
+
md.show('module');
|
806
|
+
this.checkUpdateBindings();
|
807
|
+
}
|
808
|
+
|
809
|
+
checkUpdateBindings() {
|
810
|
+
// Verify that all module parameters are bound by the previous
|
811
|
+
// bindings, or that such binding can be inferred.
|
812
|
+
const
|
813
|
+
md = this.update_modal,
|
814
|
+
mkey = md.element('module').value,
|
815
|
+
iml = this.included_modules[mkey],
|
816
|
+
mcnt = `(${pluralS(iml.length, 'cluster')})`,
|
817
|
+
missing_params = {},
|
818
|
+
cnlist = [];
|
819
|
+
for(const im of iml) cnlist.push(safeDoubleQuotes(im.displayName));
|
820
|
+
md.element('count').innerText = mcnt;
|
821
|
+
md.element('count').title = cnlist.sort().join('\n');
|
822
|
+
this.obsolete_items = [];
|
823
|
+
// Check bindings of the included clusters.
|
824
|
+
for(const im of iml) {
|
825
|
+
const
|
826
|
+
cn = im.name,
|
827
|
+
iob = im.module.bindings,
|
828
|
+
bk = Object.keys(iob),
|
829
|
+
ck = Object.keys(IO_CONTEXT.bindings),
|
830
|
+
missing = complement(ck, bk);
|
831
|
+
if(missing.length) {
|
832
|
+
// Try to match name in module with existing prefixed entity in model.
|
833
|
+
for(let mi = missing.length - 1; mi >= 0; mi++) {
|
834
|
+
const mb = IO_CONTEXT.bindings[missing[mi]];
|
835
|
+
if(mb.io_type === 2) {
|
836
|
+
// Actual name = formal name, so known.
|
837
|
+
missing.splice(mi, 1);
|
838
|
+
} else {
|
839
|
+
const
|
840
|
+
pent = cn + UI.PREFIXER + mb.name_in_module,
|
841
|
+
obj = MODEL.objectByName(pent);
|
842
|
+
if(obj && obj.type === mb.entity_type) {
|
843
|
+
mb.actual_name = pent;
|
844
|
+
console.log('HERE -- completed original bindings', mb);
|
845
|
+
} else {
|
846
|
+
console.log('HERE -- incomplete original bindings', obj, mb);
|
847
|
+
}
|
848
|
+
}
|
849
|
+
}
|
850
|
+
}
|
851
|
+
// If not resolved, add module to the missing record.
|
852
|
+
for(const m of missing) {
|
853
|
+
if(missing_params.hasOwnProperty(m)) {
|
854
|
+
missing_params[m].push(im);
|
855
|
+
} else {
|
856
|
+
missing_params[m] = [im];
|
857
|
+
}
|
858
|
+
}
|
859
|
+
for(const k of MODEL.datasetKeysByPrefix(cn)) {
|
860
|
+
this.obsolete_items.push(MODEL.datasets[k]);
|
861
|
+
}
|
862
|
+
for(const e of MODEL.equationsByPrefix(cn)) this.obsolete_items.push(e);
|
863
|
+
for(const c of MODEL.chartsByPrefix(cn)) this.obsolete_items.push(c);
|
864
|
+
}
|
865
|
+
const
|
866
|
+
remove_div = md.element('remove'),
|
867
|
+
remove_count = md.element('remove-count'),
|
868
|
+
remove_list = md.element('remove-area');
|
869
|
+
if(this.obsolete_items.length) {
|
870
|
+
remove_count.innerHTML = pluralS(this.obsolete_items.length,
|
871
|
+
'obsolete item');
|
872
|
+
const html = [];
|
873
|
+
for(const item of this.obsolete_items.sort(
|
874
|
+
(a, b) => {
|
875
|
+
const
|
876
|
+
at = a.type,
|
877
|
+
bt = b.type,
|
878
|
+
order = ['Dataset', 'Equation', 'Chart'];
|
879
|
+
if(at === bt) return ciCompare(a.displayName, b.displayName);
|
880
|
+
return order.indexOf(at) - order.indexOf(bt);
|
881
|
+
})) {
|
882
|
+
html.push('<div><img src="images/', item.type.toLowerCase(),
|
883
|
+
'.png" class="sbtn">', item.displayName, '</div>');
|
884
|
+
}
|
885
|
+
remove_list.innerHTML = html.join('');
|
886
|
+
remove_div.style.display = 'block';
|
887
|
+
} else {
|
888
|
+
remove_count.innerHTML = '';
|
889
|
+
remove_list.innerHTML = '';
|
890
|
+
remove_div.style.display = 'none';
|
891
|
+
}
|
892
|
+
const
|
893
|
+
issues_div = md.element('issues'),
|
894
|
+
issues_header = md.element('issues-header'),
|
895
|
+
issues_list = md.element('issues-area'),
|
896
|
+
mpkeys = Object.keys(missing_params);
|
897
|
+
if(mpkeys.length) {
|
898
|
+
// When parameters are missing, report this...
|
899
|
+
issues_header.innerHTML = pluralS(mpkeys.length, 'unresolved parameter');
|
900
|
+
const html = [];
|
901
|
+
for(const k of mpkeys) {
|
902
|
+
const mb = IO_CONTEXT.bindings[k];
|
903
|
+
cnlist.length = 0;
|
904
|
+
for(const im of missing_params[k]) {
|
905
|
+
cnlist.push(safeDoubleQuotes(im.displayName));
|
906
|
+
}
|
907
|
+
html.push('<div>', mb.name_in_module,
|
908
|
+
'<span class="update-cc" title="', cnlist.sort().join('\n'),
|
909
|
+
'">(in ', pluralS(cnlist.length, 'cluster'), ')</span></div>');
|
910
|
+
}
|
911
|
+
issues_list.innerHTML = html.join('');
|
912
|
+
issues_div.style.display = 'block';
|
913
|
+
// ... and disable the OK button so that no update can be performed.
|
914
|
+
md.ok.classList.add('disab');
|
915
|
+
IO_CONTEXT = null;
|
916
|
+
} else {
|
917
|
+
// No issues report.
|
918
|
+
issues_header.innerHTML = '';
|
919
|
+
issues_list.innerHTML = '';
|
920
|
+
issues_div.style.display = 'none';
|
921
|
+
// Enable the OK button so that update can be performed.
|
922
|
+
md.ok.classList.remove('disab');
|
923
|
+
}
|
924
|
+
}
|
925
|
+
|
926
|
+
performUpdate() {
|
927
|
+
// Update all eligible previously included modules.
|
928
|
+
if(!IO_CONTEXT) {
|
929
|
+
UI.alert('Cannot update modules without context');
|
930
|
+
return;
|
931
|
+
}
|
932
|
+
const
|
933
|
+
md = this.update_modal,
|
934
|
+
mkey = md.element('module').value,
|
935
|
+
iml = this.included_modules[mkey];
|
936
|
+
MODEL.clearSelection();
|
937
|
+
// Delete obsolete items from the model.
|
938
|
+
for(const item of this.obsolete_items) {
|
939
|
+
if(item instanceof Dataset) {
|
940
|
+
delete MODEL.datasets[item.identifier];
|
941
|
+
} else if(item instanceof DatasetModifier) {
|
942
|
+
delete MODEL.equations_dataset.modifiers[UI.nameToID(item.selector)];
|
943
|
+
} else if(item instanceof Chart) {
|
944
|
+
MODEL.deleteChart(MODEL.charts.indexOf(item));
|
945
|
+
}
|
946
|
+
}
|
947
|
+
// NOTE: The included module list contains clusters.
|
948
|
+
const last = iml[iml.length - 1];
|
949
|
+
// Ensure that expressions are recompiled only after the last inclusion.
|
950
|
+
for(const c of iml) this.updateCluster(c, c === last);
|
951
|
+
// Notify modeler of the scope of the update.
|
952
|
+
let counts = `: ${pluralS(IO_CONTEXT.added_nodes.length, 'node')}, ` +
|
953
|
+
pluralS(IO_CONTEXT.added_links.length, 'link');
|
954
|
+
if(IO_CONTEXT.superseded.length > 0) {
|
955
|
+
counts += ` (superseded ${IO_CONTEXT.superseded.length})`;
|
956
|
+
console.log('SUPERSEDED:', IO_CONTEXT.superseded);
|
957
|
+
}
|
958
|
+
UI.notify(`Model updated using <tt>${IO_CONTEXT.file_name}</tt> from ` +
|
959
|
+
`<strong>${IO_CONTEXT.repo_name}</strong>${counts}`);
|
960
|
+
// Reset the IO context.
|
961
|
+
IO_CONTEXT = null;
|
962
|
+
this.update_modal.hide();
|
963
|
+
MODEL.cleanUpActors();
|
964
|
+
MODEL.focal_cluster.clearAllProcesses();
|
965
|
+
UI.drawDiagram(MODEL);
|
966
|
+
UI.updateControllerDialogs('CDEFJX');
|
967
|
+
}
|
968
|
+
|
969
|
+
updateCluster(c, last) {
|
970
|
+
// Update perviously included cluster `c`.
|
971
|
+
// Remember the XY-position of the cluster.
|
972
|
+
const
|
973
|
+
cx = c.x,
|
974
|
+
cy = c.y;
|
975
|
+
// The name of `c` will be used again as prefix.
|
976
|
+
IO_CONTEXT.prefix = c.name;
|
977
|
+
IO_CONTEXT.actor_name = UI.realActorName(c.actor.name);
|
978
|
+
// NOTE: `last` = TRUE indicates that expressions must be recompiled
|
979
|
+
// only after updating this cluster.
|
980
|
+
IO_CONTEXT.recompile = last;
|
981
|
+
// Copy the original bindings to the IO context.
|
982
|
+
const ob = c.module.bindings;
|
983
|
+
for(const k of Object.keys(ob)) {
|
984
|
+
// NOTE: Only copy bindings for parameters of the module used for updating.
|
985
|
+
if(IO_CONTEXT.bindings.hasOwnProperty(k)) {
|
986
|
+
const nb = IO_CONTEXT.bindings[k];
|
987
|
+
nb.actual_name = ob[k].actual_name;
|
988
|
+
nb.actual_id = UI.nameToID(nb.actual_name);
|
989
|
+
}
|
990
|
+
}
|
991
|
+
// Delete `c` from the model without adding its XML (UNDO to be implemented).
|
992
|
+
MODEL.deleteCluster(c, false);
|
993
|
+
// NOTE: Including may affect focal cluster, so store it...
|
994
|
+
const fc = MODEL.focal_cluster;
|
995
|
+
MODEL.initFromXML(IO_CONTEXT.xml);
|
996
|
+
// ... and restore it afterwards.
|
997
|
+
MODEL.focal_cluster = fc;
|
998
|
+
// Get the newly added cluster.
|
999
|
+
const nac = MODEL.objectByName(IO_CONTEXT.clusterName);
|
1000
|
+
if(nac instanceof Cluster) {
|
1001
|
+
// Record from which module it has been included with what bindings.
|
1002
|
+
nac.module = {name: IO_CONTEXT.file_name,
|
1003
|
+
bindings: IO_CONTEXT.copyOfBindings};
|
1004
|
+
// Give the updated cluster the same position as the original one.
|
1005
|
+
nac.x = cx;
|
1006
|
+
nac.y = cy;
|
1007
|
+
// Prepare for redraw.
|
1008
|
+
nac.clearAllProcesses();
|
1009
|
+
return true;
|
1010
|
+
}
|
1011
|
+
UI.alert('Update failed to create a cluster');
|
1012
|
+
return false;
|
1013
|
+
}
|
1014
|
+
|
1015
|
+
cancelUpdate() {
|
1016
|
+
// Clear the IO context and closes the update dialog.
|
1017
|
+
IO_CONTEXT = null;
|
1018
|
+
this.update_modal.hide();
|
1019
|
+
}
|
1020
|
+
|
1021
|
+
//
|
1022
|
+
// Methods for storing/loading a module in/from a repository
|
1023
|
+
//
|
737
1024
|
|
738
1025
|
promptForStoring() {
|
739
1026
|
if(this.repository_index >= 0) {
|
@@ -792,11 +1079,11 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
792
1079
|
}
|
793
1080
|
}
|
794
1081
|
|
795
|
-
includeModule() {
|
1082
|
+
includeModule(update=false) {
|
796
1083
|
// Include selected module into the current model.
|
797
1084
|
if(this.repository_index >= 0 && this.module_index >= 0) {
|
798
1085
|
const r = this.repositories[this.repository_index];
|
799
|
-
r.loadModule(this.module_index, true);
|
1086
|
+
r.loadModule(this.module_index, true, update);
|
800
1087
|
}
|
801
1088
|
}
|
802
1089
|
|
@@ -255,14 +255,13 @@ class UndoStack {
|
|
255
255
|
// (2) Set "selected" attribute of objects to FALSE, as the selection will
|
256
256
|
// be restored from UndoEdit.
|
257
257
|
const n = parseXML(MODEL.xml_header + `<edits>${xml}</edits>`);
|
258
|
-
if(n
|
258
|
+
if(n) {
|
259
259
|
const
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
for(
|
264
|
-
|
265
|
-
// Immediately restore "independent" entities ...
|
260
|
+
ln = [],
|
261
|
+
ppn = [],
|
262
|
+
cn = [];
|
263
|
+
for(const c of n.childNodes) {
|
264
|
+
// Immediately restore "independent" entities ...
|
266
265
|
if(c.nodeName === 'dataset') {
|
267
266
|
MODEL.addDataset(xmlDecoded(nodeContentByTag(c, 'name')), c);
|
268
267
|
} else if(c.nodeName === 'actor') {
|
@@ -280,37 +279,33 @@ class UndoStack {
|
|
280
279
|
obj.selected = false;
|
281
280
|
} else if(c.nodeName === 'chart') {
|
282
281
|
MODEL.addChart(xmlDecoded(nodeContentByTag(c, 'title')), c);
|
283
|
-
// ... but merely collect
|
282
|
+
// ... but merely collect child nodes for other entities.
|
284
283
|
} else if(c.nodeName === 'link' || c.nodeName === 'constraint') {
|
285
|
-
|
284
|
+
ln.push(c);
|
286
285
|
} else if(c.nodeName === 'product-position') {
|
287
|
-
|
286
|
+
ppn.push(c);
|
288
287
|
} else if(c.nodeName === 'cluster') {
|
289
|
-
|
288
|
+
cn.push(c);
|
290
289
|
}
|
291
290
|
}
|
292
|
-
// NOTE: Collecting the
|
291
|
+
// NOTE: Collecting the child nodes forlinks, product positions and clusters
|
293
292
|
// saves the effort to iterate over ALL childnodes again.
|
294
293
|
// First restore links and constraints.
|
295
|
-
for(const
|
296
|
-
|
297
|
-
|
298
|
-
if(
|
299
|
-
|
300
|
-
|
294
|
+
for(const c of ln) {
|
295
|
+
let name = xmlDecoded(nodeContentByTag(c, 'from-name'));
|
296
|
+
let actor = xmlDecoded(nodeContentByTag(c, 'from-owner'));
|
297
|
+
if(actor != UI.NO_ACTOR) name += ` (${actor})`;
|
298
|
+
let fn = MODEL.nodeBoxByID(UI.nameToID(name));
|
299
|
+
if(fn) {
|
300
|
+
name = xmlDecoded(nodeContentByTag(c, 'to-name'));
|
301
|
+
actor = xmlDecoded(nodeContentByTag(c, 'to-owner'));
|
301
302
|
if(actor != UI.NO_ACTOR) name += ` (${actor})`;
|
302
|
-
let
|
303
|
-
if(
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
if(tn) {
|
309
|
-
if(c.nodeName === 'link') {
|
310
|
-
MODEL.addLink(fn, tn, c).selected = false;
|
311
|
-
} else {
|
312
|
-
MODEL.addConstraint(fn, tn, c).selected = false;
|
313
|
-
}
|
303
|
+
let tn = MODEL.nodeBoxByID(UI.nameToID(name));
|
304
|
+
if(tn) {
|
305
|
+
if(c.nodeName === 'link') {
|
306
|
+
MODEL.addLink(fn, tn, c).selected = false;
|
307
|
+
} else {
|
308
|
+
MODEL.addConstraint(fn, tn, c).selected = false;
|
314
309
|
}
|
315
310
|
}
|
316
311
|
}
|
@@ -319,39 +314,33 @@ class UndoStack {
|
|
319
314
|
// NOTE: These correspond to the products that were part of the
|
320
315
|
// selection; all other product positions are restored as part of their
|
321
316
|
// containing clusters.
|
322
|
-
for(const
|
323
|
-
const
|
324
|
-
|
325
|
-
if(
|
326
|
-
|
327
|
-
|
328
|
-
if(obj) {
|
329
|
-
obj.selected = false;
|
330
|
-
MODEL.focal_cluster.addProductPosition(obj).initFromXML(c);
|
331
|
-
}
|
317
|
+
for(const c of ppn) {
|
318
|
+
const obj = MODEL.nodeBoxByID(UI.nameToID(
|
319
|
+
xmlDecoded(nodeContentByTag(c, 'product-name'))));
|
320
|
+
if(obj) {
|
321
|
+
obj.selected = false;
|
322
|
+
MODEL.focal_cluster.addProductPosition(obj).initFromXML(c);
|
332
323
|
}
|
333
324
|
}
|
334
325
|
// Lastly, restore clusters.
|
335
326
|
// NOTE: Store focal cluster, because this may change while initializing
|
336
327
|
// a cluster from XML.
|
337
328
|
const fc = MODEL.focal_cluster;
|
338
|
-
for(const
|
339
|
-
const
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
obj.selected = false;
|
344
|
-
|
329
|
+
for(const c of cn) {
|
330
|
+
const obj = MODEL.addCluster(xmlDecoded(nodeContentByTag(c, 'name')),
|
331
|
+
xmlDecoded(nodeContentByTag(c, 'owner')), c);
|
332
|
+
obj.selected = false;
|
333
|
+
/*
|
345
334
|
// TEMPORARY trace (remove when done testing)
|
346
335
|
if (MODEL.focal_cluster === fc) {
|
347
336
|
console.log('NO refocus needed');
|
348
337
|
} else {
|
349
338
|
console.log('Refocusing from ... to ... : ', MODEL.focal_cluster, fc);
|
350
339
|
}
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
340
|
+
*/
|
341
|
+
// Restore original focal cluster because addCluster may shift focus
|
342
|
+
// to a sub-cluster.
|
343
|
+
MODEL.focal_cluster = fc;
|
355
344
|
}
|
356
345
|
}
|
357
346
|
MODEL.clearSelection();
|
@@ -443,7 +432,7 @@ if (MODEL.focal_cluster === fc) {
|
|
443
432
|
// First check whether product P needs to be restored.
|
444
433
|
if(!p && ue.xml) {
|
445
434
|
const n = parseXML(MODEL.xml_header + `<edits>${ue.xml}</edits>`);
|
446
|
-
if(n && n.childNodes) {
|
435
|
+
if(n && n.childNodes.length) {
|
447
436
|
let c = n.childNodes[0];
|
448
437
|
if(c.nodeName === 'product') {
|
449
438
|
p = MODEL.addProduct(
|