-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathcut-object.html
804 lines (700 loc) · 31.3 KB
/
cut-object.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/iron-a11y-keys/iron-a11y-keys.html">
<link rel="import" href="utils.html">
<dom-module id="cut-object">
<template>
<iron-a11y-keys keys="z+only ctrl+z" on-keys-pressed="undo" target="[[keyboard_target]]"></iron-a11y-keys>
<iron-a11y-keys keys="y+only ctrl+y" on-keys-pressed="redo" target="[[keyboard_target]]"></iron-a11y-keys>
</template>
<script>
"use strict";
(function(){
var bin_count = function(group_idx, max_len){
var ret = new Uint32Array(max_len);
for(var ii=0; ii<group_idx.length; ii++)
ret[group_idx[ii]]++; // if group_idx is out of range it seems like no warning/error is raised, at least in chrome 49
return ret;
}
var arg_split = function(groups, group_idx, group_n){
// sort of like an arg sort followed by a split by value,
// groups is an array of typed arrays, and group_idx is a
// single typed_array, with each element specifiying an
// index into the the groups array. For each such element,
// we record in the typed array within groups the index of
// the element. group_n is another typed array, which is
// is of the same length as groups, it shoudld be zero-initialized
// and we use it counting up the number of entries currently
// placed into the corresponding typed array within groups.
// note that each of the typed arrays wihtin groups should have
// been sized in advance, probably using the bin_count function.
for(var ii=0; ii<group_idx.length; ii++)
groups[group_idx[ii]][group_n[group_idx[ii]]++] = ii;
};
var mask_split = function(a, mask){
// splits a into two arrays, one for 0 elements in mask and one for 1 elements
// note if mask contains other values the result will be nonsense.
if(mask.length != a.length)
throw "mask length mismatch";
var counts = bin_count(mask, 2);
var a0 = new Uint32Array(counts[0]);
var a1 = new Uint32Array(counts[1]);
var p0=0, p1=0;
for(var ii=0;ii<mask.length; ii++){
if(mask[ii])
a1[p1++] = a[ii];
else
a0[p0++] = a[ii];
}
return [a0, a1];
};
var merge_sorted = function(x){
// x is a list of arrays, each of which is internally sorted in ascending order.
// We combine them into a sorted new array and create an accompanying array saying
// for each element which of the source arrays was used.
// we fill in the result in reverse, i.e. start at n-1 and go back to 0.
let x_len = x.length;
let n = 0;
let p = new Uint32Array(x_len); // this will hold the index of the last used element in each array
for(let [ii, xx] of x.entries()){
n += xx.length;
p[ii] = xx.length;
}
let dest = new Uint32Array(n);
let src = new Uint8Array(n);
for(let ii=n-1; ii>=0; ii--){
// find the array with the next highest value
let kk_choice, v=Number.MIN_SAFE_INTEGER;
for(let kk=0; kk<x_len; kk++){
if (p[kk] && x[kk][p[kk]-1] > v) {
kk_choice = kk;
v = x[kk][p[kk]-1];
}
}
// use it
dest[ii] = v;
src[ii] = kk_choice;
p[kk_choice]--;
}
return [dest, src];
}
var merge_sorted_masked = function(x, m){
// this is the same as merge_sorted, but each array now (may) have a mask
// which is true where we want to pick and false where we should ignore entries
// we fill in the result in reverse, i.e. start at n-1 and go back to 0.
let x_len = x.length;
let n = 0;
let p = new Uint32Array(x_len); // this will hold the index of the (1+the_next_to_use) element in each array, i.e. 0=used all
for(let [ii, xx] of x.entries()){
let m_ii = m[ii];
if(m_ii){
// x[ii] is masked, so find out how many true elements there are and
// record the index_of_final_true+1 in p[ii].
p[ii] = 0;
for(let jj=0; jj<m_ii.length; jj++){
m_ii[jj] && ( n++ && (p[ii] = jj+1));
}
} else {
// no mask, so use full length of src
n += xx.length;
p[ii] = xx.length;
}
}
let dest = new Uint32Array(n);
let src = new Uint8Array(n);
for(let ii=n-1; ii>=0; ii--){
// find the array with the next highest value
let kk_choice, v=Number.MIN_SAFE_INTEGER;
for(let kk=0; kk<x_len; kk++){
if (p[kk] && x[kk][p[kk]-1] > v) {
kk_choice = kk;
v = x[kk][p[kk]-1];
}
}
// use it
dest[ii] = v;
src[ii] = kk_choice;
let m_choice = m[kk_choice];
if(m_choice){
// mask, so decrement until we are *about* to hit the next true element
while(--p[kk_choice] && !m_choice[p[kk_choice] - 1]);
} else {
// no mask, so just decrement
p[kk_choice]--;
}
}
return [dest, src];
}
var labeled_split = function(src, labels, dests_in){
// this is the inverse of merge_sorted_masked...and it's even more complicated...
//
// labels is an array of the same length as src, saying where to put
// each element. dests_in is a list of pre-existing arrays into which to
// insert the values from src, maintaining ascending-order sort in each
// dest array. One or more of dests_in may be null, in which case we
// simply accumalte the relevant values from src in a new array.
// In addition to the augmented dests arrays, this returns an equally
// long list giving a mask for each array (or null), giving true
// for elements that were originally in src.
let dest_len = dests_in.length;
// work out the length of each array in dests_out
let dests_out = [];
let masks_out = [];
let n = new Uint32Array(dest_len);
for(let ii=0; ii<src.length; ii++){
n[labels[ii]]++;
}
for(let [ii, dd] of dests_in.entries()){
n[ii] += dd ? dd.length : 0;
dests_out.push(new Uint32Array(n[ii]));
masks_out.push(dd ? new Uint8Array(n[ii]) : null);
}
// unlike merge_sorted_masked we do this in the forward dircetion
// (becuase it's complicated enough already!)
n.fill(0); // we repurpose it...
let p_in = n;
let p_out = new Uint32Array(dest_len);
for(let ii=0; ii<src.length; ii++){
let v = src[ii];
let k = labels[ii];
let dest_in = dests_in[k];
let dest_out = dests_out[k];
if(dest_in){
// merge-sort into existing dest_in
while(p_in[k] < dest_in.length && dest_in[p_in[k]]<v){
dest_out[p_out[k]++] = dest_in[p_in[k]++];
// mask is left as 0
}
masks_out[k][p_out[k]] = 1;
dest_out[p_out[k]++] = v;
} else {
// creating dest_out from scratch...
dest_out[p_out[k]++] = v;
}
}
// use up any final values in dests_in..
for(let ii=0; ii<dest_len; ii++){
let dest_in = dests_in[ii];
if(!dest_in){
continue;
}
let dest_out = dests_out[ii];
for(let p=p_in[ii]; p<dest_in.length; p++){
dest_out[p_out[ii]++] = dest_in[p];
// mask is left as 0
}
}
return [dests_out, masks_out];
}
var take_masked = function(x, m, take_true){
// returns the elements of x where m is true, unless take_true if falsy in which
// case it's the false elements of m that are taken from x.
if(take_true){
let n = 0;
for(let ii=0; ii<x.length; ii++) {
(m[ii]) && (n++);
}
let res = new Uint32Array(n);
for(let ii=0, p=0; ii<x.length; ii++) {
(m[ii]) && (res[p++] = x[ii]);
}
return res;
} else {
let n = 0;
for(let ii=0; ii<x.length; ii++) {
(!m[ii]) && (n++);
}
let res = new Uint32Array(n);
for(let ii=0, p=0; ii<x.length; ii++) {
(!m[ii]) && (res[p++] = x[ii]);
}
return res;
}
}
var arange = function(n){
var ret = new Uint32Array(n);
for(var ii=0; ii<n; ii++)
ret[ii] = ii;
return ret;
}
var CutStackState = function(that){
this.is = "stack";
this.id = null; // a kind of invalid fkey
this.undo_stack_descriptions = that.undo_stack_descriptions;
this.redo_stack_descriptions = that.redo_stack_descriptions;
this.groups = that.groups;
this.undo_stack = that.undo_stack;
this.redo_stack = that.redo_stack;
this.painter_state = that.painter_state;
return this;
}
CutStackState.prototype.get_blob_url = function(experiment_name){
// This methods is called by file-organiser when dragging starts
// build the 1d array that gives the group number for each spike...
// I'm not sure if total n_spikes is already avialble somehwere, but hey...
var n_spikes = 0;
for(let gg of this.groups){
n_spikes += gg && gg.n_spikes;
}
var the_cut = new Uint8Array(n_spikes);
for(let ii=0; ii<this.groups.length; ii++){
let gg = this.groups[ii];
if(!gg){
continue;
}
let inds = gg.akey.array;
for(let jj=0; jj<inds.length; jj++){
the_cut[inds[jj]] = ii;
}
}
// and now construct the string
let str = ['n_clusters: ' + this.groups.length,
'n_channels: 4',
'n_params: 0',
'times_used_in_Vt: 0 0 0 0'];
for(let ii=0; ii<this.groups.length; ii++){
str.push(' cluster: ' + ii + ' center: 0 0 0 0 0 0 0 0');
str.push(' min: 0 0 0 0 0 0 0 0');
str.push(' max: 0 0 0 0 0 0 0 99');
}
str.push('');
str.push('Exact_cut_for: '+ experiment_name +' spikes: ' + n_spikes);
str.push('');
str = str.join('\n') + Array.prototype.join.call(the_cut, ' ');
var b = new Blob([str], {type: 'text/plain'});
return window.URL.createObjectURL(b);
}
Polymer({
is:'cut-object',
properties: {
undo_stack_descriptions: {
type: Array,
value: function(){return [];},
notify: true
},
redo_stack_descriptions: {
type: Array,
value: function(){return [];},
notify: true
},
groups: {
type: Array,
value: function(){return [];},
notify: true
},
painter_state: {
type: Object,
notify: true,
value: function(){return {};}
},
cut_box: {
type: Object,
value: function(){return {};},
notify: true,
observer: '_use_cut_box'
},
keyboard_target: {
type: Object,
value: function(){return document.querySelector('body');}
}
}, _make_okey: function(inds, num){
return {
group_num: num,
// The indices are immutable for the lifetime of this object
akey: Utils.typed_array_manager.to_akey(inds),
// The following properties are visualized by the tile element
// and must thus be notified when changes are made. Note that
// for efficiency of notification, the rm is always changed in
// its entirerity, i.e. a new object is assigned.
n_spikes: inds.length,
tac: null,
waves: null,
rm: {
spa_max: "",
dir_max: "",
dir: null,
spa: null,
speed: null
},
// private properties for use by tac-plots module, never notified
_tac_other: null,
_tac_other_options: {},
_tac_is_on_worker: false,
// private properties for use by rm-plots module, never notified
_rm_spa_other: null,
_rm_spa_max_other: null,
_rm_dir_other: null,
_rm_dir_max_other: null,
_rm_speed_other: null,
_rm_other_options: {},
_rm_is_on_worker: false,
// property set by Polymer.Collection
_pkey: null
};
}, _use_cut_box: function(){
if (this.cut_box && this.cut_box.is === "file"){
// construct from file
let akey = this.cut_box.akey.array;
// find out how many indices for each group
let group_n = bin_count(akey, 255);
// zero-initialize an array of the required length for each group
let groups = [];
for(let ii=0; ii<group_n.length; ii++) if(group_n[ii]>0){
groups[ii] = new Uint32Array(group_n[ii]);
}
// store the indices for each group
group_n.fill(0); // we repurpose this as a counter during arg_splitting
arg_split(groups, akey, group_n);
// now we are ready to create the this.groups array of okeys
let okeys = [];
for(let [ii, gg] of groups.entries()){
if(gg){
okeys.push(this._make_okey(gg, ii));
}else{
okeys.push(null);
}
}
this.undo_stack = [{
action: 'load'
}];
this.set('groups', okeys);
this.set('undo_stack_descriptions', ['load from file']);
this.set('redo_stack_descriptions', []);
this.redo_stack = [];
this.set('painter_state', {
dest: 1,
srcs: [0]
});
} else if (this.cut_box && this.cut_box.is === "stack"){
// reinstate existing cut object
this.set('groups', this.cut_box.groups);
this.set('undo_stack_descriptions', this.cut_box.undo_stack_descriptions);
this.set('redo_stack_descriptions', this.cut_box.redo_stack_descriptions);
this.undo_stack = this.cut_box.undo_stack;
this.redo_stack = this.cut_box.redo_stack;
this.set('painter_state', this.cut_box.painter_state);
} else if(this.cut_box && this.cut_box.is === "n"){
// no cut_file, so just make a zero-group with inds 0:n
this.undo_stack = [{
action: 'create'
}];
this.set('undo_stack_descriptions', ['create blank cut']);
this.set('redo_stack_descriptions', []);
this.set('groups', [this._make_okey(arange(this.cut_box.n_spikes), 0)], 0);
this.undo_stack = [];
this.redo_stack = [];
this.set('painter_state', {
dest: 1,
srcs: [0]
});
} else {
// clear current state
this.set('groups', []);
this.set('undo_stack_descriptions', []);
this.set('redo_stack_descriptions', []);
this.undo_stack = [];
this.redo_stack = [];
this.set('painter_state', {
dest: 0,
srcs: [0]
});
}
}, _fork_if_needed: function(){
if(this.cut_box.is !== "stack"){
// tell the file-organiser to construct a new "cut box" from the preservable state of this element.
// note that the new "cut box" will be applied to this element as its cut_box, but this will not
// cause any changes as all the values will be the same.
// we could hold the reference directly, but it complciates matters when doing bulk updates of trials
let new_cut = new CutStackState(this);
this.fire('fork', new_cut);
}
}, swap_groups: function(a, b){
if(a==b) return;
this._fork_if_needed();
// make life easier, fix b>a
if(a>b){
var tmp = a;
a = b;
b = tmp;
}
if(!(this.groups[a] || this.groups[b])){
return; // at least one of the groups should actually exist
}
this._do_swap(a, b);
this.redo_stack.splice(0, this.redo_stack.length); // clear
this.undo_stack.push({
action: 'swap',
a: a,
b: b
});
this.splice('redo_stack_descriptions', 0, this.redo_stack_descriptions.length); // clear
this.push('undo_stack_descriptions', 'swap groups ' + a + " and " + b);
}, _do_swap: function(a, b){
// b must be greater than a
// b could be off beyond the end of the array as it currently stands
while(b>=this.groups.length){
this.push('groups', null);
}
let group_a = this.groups[a];
let group_b = this.groups[b];
// update group_nums in preparation for announcing splice
group_a && (group_a.group_num = b);
group_b && (group_b.group_num = a);
// TODO: check that this really is the correct way to do it
// https://github.com/Polymer/polymer/issues/3377
this.groups.splice(a, 1, group_b);
this.groups.splice(b, 1, group_a);
this.notifySplices('groups',[
{index: a, removed: [group_a], addedCount: 0, object: this.groups, type:'splice'},
{index: b-1, removed: [group_b], addedCount: 0, object: this.groups, type:'splice'},
{index: a, removed: [], addedCount: 1, object: this.groups, type:'splice'},
{index: b, removed: [], addedCount: 1, object: this.groups, type:'splice'}
]);
}, split_group: function(a, mask){
this._fork_if_needed();
var new_arrs = mask_split(this.groups[a].akey.array, mask);
// update group_nums in preparation for announcing splice
for(let ii=a+1; ii<this.groups.length; ii++){
this.groups[ii] && (this.groups[ii].group_num = ii+1);
}
this.splice('groups', a, 1, this._make_okey(new_arrs[0], a), this._make_okey(new_arrs[1], a+1));
this.redo_stack.splice(0, this.redo_stack.length); // clear
this.undo_stack.push({
action: 'split',
a: a
});
this.splice('redo_stack_descriptions', 0, this.redo_stack_descriptions.length); // clear
this.push('undo_stack_descriptions', 'split group ' + a + ' in two');
}, merge_groups: function(a, b){
// the higher-numbered group is merged into the lower number
this._fork_if_needed();
// make life easier, fix b>a
if(a>b){
var tmp = a;
a = b;
b = tmp;
}
var packed = merge_sorted([this.groups[a].akey.array,
this.groups[b].akey.array])
var inds = packed[0], mask = packed[1];
this.splice('groups', b, 1, null);
this.splice('groups', a, 1, this._make_okey(inds, a));
this.redo_stack.splice(0, this.redo_stack.length); // clear
this.undo_stack.push({
action: 'merge',
a: a,
b: b,
mask: mask
});
this.splice('redo_stack_descriptions', 0, this.redo_stack_descriptions.length); // clear
this.push('undo_stack_descriptions', 'merge groups ' + a + ' and ' + b);
}, transplant_groups: function(dest, src_masks){
let src_nums = [];
for(let num in src_masks){
src_nums.push(num);
}
if(!src_nums.length){
return;
}
this._fork_if_needed();
while(dest >= this.groups.length){
this.push('groups', null);
}
let delta = {
action: 'transplant',
dest: dest,
src_masks: src_masks
}
let valid = this._do_transplant(delta); // this applies the delta and inverts the data stored within it
if(!valid){
return;
}
this.undo_stack.push(delta);
// TODO: sort src_nums
this.push('undo_stack_descriptions', 'paint into group ' + dest + ' from group' +
(src_nums.length > 1? 's [' + src_nums.join(",") +']' : ' ' + src_nums[0]));
this.splice('redo_stack_descriptions', 0, this.redo_stack_descriptions.length); // clear
this.redo_stack.splice(0, this.redo_stack.length); // clear
}, _do_transplant: function(delta){
let dest_group = this.groups[delta.dest];
// we need to do a merge-sort on all the masked srcs and dest indicies
// we prepare two lists, one of indices and one of masks for those indices.
// we also splice out the src groups and replace with their diminished verions
let x = [(dest_group && dest_group.akey.array) || new Uint32Array(0)];
let x_mask = [null];
let src_nums = []; // used in undo delta and in undo description
for(let group_num in delta.src_masks){
group_num = parseInt(group_num);
let g = this.groups[group_num];
if(!g){
throw "transplant: src 'group " + group_num + "' doesn't exist.";
}
x.push(g.akey.array)
x_mask.push(delta.src_masks[group_num]);
let new_inds = delta.src_masks[group_num] ?
take_masked(g.akey.array, delta.src_masks[group_num], false)
: null;
this.splice('groups', group_num, 1, new_inds && new_inds.length ?
this._make_okey(new_inds, group_num) : null); // if group is empty we delete it
src_nums.push(group_num);
}
let res = merge_sorted_masked(x, x_mask); // this combines everything
if(!res[0].length){
return false; // nothing actually was transfered
}
// and now we can update the dest group
this.splice('groups', delta.dest, 1, this._make_okey(res[0], delta.dest));
delta.src = res[1]; // remember that src[ii]=0 means element ii was originally in dest group,
// and src[ii]=v means that element ii was oritinally in src_nums[v+1] group.
delta.src_nums = src_nums;
delete delta['src_masks']; // don't need this any more
return true;
}, _undo_transplant: function(delta){
let src_inds = [null];
for(let group_num of delta.src_nums){
src_inds.push(this.groups[group_num] && this.groups[group_num].akey.array);
}
let res = labeled_split(this.groups[delta.dest].akey.array, delta.src, src_inds);
this.splice('groups', delta.dest, 1, res[0][0].length ? this._make_okey(res[0][0], delta.dest) : null);
let src_masks = {}; // this will be the same as originally povided to transplant
for(let [ii, group_num] of delta.src_nums.entries()){
src_masks[group_num] = res[1][ii+1]; // +1 because the 0th entry is the dest
this.splice('groups', group_num, 1, res[0][ii+1].length ? this._make_okey(res[0][ii+1], group_num) : null)
}
delta.src_masks = src_masks;
delete delta['src']; //we don't need these any more..
delete delta['src_nums'];
}, sort: function(){
this._fork_if_needed();
// TODO: might want to accept a custom ordering rather than assuming n-order
let sorted_groups = this.groups.slice(0).sort(function(a,b){
return (a && b) ? (a.n_spikes > b.n_spikes ? -1 : 1)
: (a ? -1 : 1);
});
// invert the mapping for use with _do_reorder
let move_to = [];
for(let [ii, g] of sorted_groups.entries())if(g){
move_to[g.group_num] = ii;
}
let delta = {
action: "reorder",
move_to: move_to
}
// this will re-invert the mapping for storage
this._do_reorder(delta);
this.undo_stack.push(delta);
this.push('undo_stack_descriptions', 'sort groups by n_spikes');
this.splice('redo_stack_descriptions', 0, this.redo_stack_descriptions.length); // clear
this.redo_stack.splice(0, this.redo_stack.length); // clear
}, _do_reorder: function(delta){
let new_groups = [];
let inverse_move_to = [];
for(let [ii, group_num] of delta.move_to.entries())if(group_num > 0 || group_num === 0){
new_groups[group_num] = this.groups[ii];
inverse_move_to[group_num] = ii;
}
delta.move_to = inverse_move_to;
for(let [ii, group] of new_groups.entries()){
if(group){
group.group_num = ii; // update group_num for new ordering
}else{
new_groups[ii] = null; //enforce null rather than undefined
}
}
// splice out everything, then splice it back in the right order
this.splice.apply(this, ['groups', 0, this.groups.length].concat(new_groups));
}, undo: function(){
var delta = this.undo_stack[this.undo_stack.length-1];
var success = false;
// note that b will always be greater than a...
switch(delta.action){
case "merge":
var new_arrs = mask_split(this.groups[delta.a].akey.array, delta.mask);
this.splice('groups', delta.a, 1, this._make_okey(new_arrs[0], delta.a));
this.splice('groups', delta.b, 1, this._make_okey(new_arrs[1], delta.b));
delete delta['mask']; // not absolutely neccessary, but we don't need it now
success = true;
break;
case "split":
// we could have kept the mask originally, but we didn't
var packed = merge_sorted([this.groups[delta.a].akey.array,
this.groups[delta.a+1].akey.array])
var inds = packed[0];
// update group_nums in preparation for announcing splice
for(let ii=delta.a+1; ii<this.groups.length; ii++){
this.groups[ii] && (this.groups[ii].group_num = ii-1);
}
this.splice('groups', delta.a, 2, this._make_okey(inds, delta.a));
delta.mask = packed[1];
success = true;
break;
case "swap":
this._do_swap(delta.a, delta.b);
success = true;
break;
case "transplant":
this._undo_transplant(delta); // it's complicated so it gets its own function
success = true;
break;
case "reorder":
this._do_reorder(delta); // will invert the delta
success = true;
break;
default:
console.log("attempted undo of unknown or invalid delta");
}
if(success){
// note we hadn't yet actually removed the delta from the undo stack
this.redo_stack.push(this.undo_stack.pop())
this.push('redo_stack_descriptions', this.pop('undo_stack_descriptions'));
}
}, redo: function(){
var delta = this.redo_stack[this.redo_stack.length-1];
if(!delta)
return;
var success = false;
// note that b will always be greater than a...
switch(delta.action){
case "merge":
var packed = merge_sorted([this.groups[delta.a].akey.array,
this.groups[delta.b].akey.array]);
var inds = packed[0];
this.splice('groups', delta.b, 1, null);
this.splice('groups', delta.a, 1, this._make_okey(inds, delta.a));
delta.mask = packed[1];
success = true;
break;
case "split":
// we could have kept the mask originally, but we didn't
var new_arrs = mask_split(this.groups[delta.a].akey.array, delta.mask);
delete delta['mask'];
for(let ii=delta.a+1; ii<this.groups.length; ii++){
this.groups[ii] && (this.groups[ii].group_num = ii+1);
}
this.splice('groups', delta.a, 1, this._make_okey(new_arrs[0], delta.a), this._make_okey(new_arrs[1], delta.a+1));
success = true;
break;
case "swap":
this._do_swap(delta.a, delta.b);
success = true;
break;
case "transplant":
this._do_transplant(delta); // it's complicated so it gets its own function
success = true;
break;
case "reorder":
this._do_reorder(delta); // will invert the delta
success = true;
break;
default:
console.log("attempted redo of unknown or invalid delta");
}
if(success){
// note we hadn't yet actually removed the delta from the redo stack
this.undo_stack.push(this.redo_stack.pop())
this.push('undo_stack_descriptions', this.pop('redo_stack_descriptions'));
}
}
});
})();
</script>
</dom-module>