From f52ee9db2aa64c507c60e704e7557585805c13b0 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Mon, 14 May 2018 16:47:52 +0200
Subject: [PATCH] Structure and selection cosntruction updates

---
 src/mol-data/int/_spec/sorted-array.spec.ts   |  11 ++
 src/mol-data/int/impl/sorted-array.ts         |  16 +++
 src/mol-data/int/sorted-array.ts              |   2 +
 src/mol-model/structure/query/generators.ts   |  52 +--------
 src/mol-model/structure/query/selection.ts    |   6 +-
 src/mol-model/structure/query/utils.ts        | 107 ------------------
 .../structure/query/utils/builders.ts         |  81 +++++++++++++
 .../structure/query/utils/structure.ts        | 103 +++++++++++++++++
 .../structure/structure/structure.ts          |  18 ++-
 9 files changed, 233 insertions(+), 163 deletions(-)
 delete mode 100644 src/mol-model/structure/query/utils.ts
 create mode 100644 src/mol-model/structure/query/utils/builders.ts
 create mode 100644 src/mol-model/structure/query/utils/structure.ts

diff --git a/src/mol-data/int/_spec/sorted-array.spec.ts b/src/mol-data/int/_spec/sorted-array.spec.ts
index db8cd15d7..2cd82af32 100644
--- a/src/mol-data/int/_spec/sorted-array.spec.ts
+++ b/src/mol-data/int/_spec/sorted-array.spec.ts
@@ -16,6 +16,11 @@ describe('sortedArray', () => {
         it(name, () => expect(a).toEqual(b));
     }
 
+    function compareArrays(a: ArrayLike<number>, b: ArrayLike<number>) {
+        expect(a.length).toBe(b.length);
+        for (let i = 0; i < a.length; i++) expect(a[i]).toBe(b[i]);
+    }
+
     const a1234 = SortedArray.ofSortedArray([1, 2, 3, 4]);
     const a2468 = SortedArray.ofSortedArray([2, 4, 6, 8]);
 
@@ -48,6 +53,12 @@ describe('sortedArray', () => {
 
     testI('findRange', SortedArray.findRange(a2468, 2, 4), Interval.ofRange(0, 1));
 
+    it('deduplicate', () => {
+        compareArrays(SortedArray.deduplicate(SortedArray.ofSortedArray([1, 1, 1, 1])), [1]);
+        compareArrays(SortedArray.deduplicate(SortedArray.ofSortedArray([1, 1, 2, 2, 3, 4])), [1, 2, 3, 4]);
+        compareArrays(SortedArray.deduplicate(SortedArray.ofSortedArray([1, 2, 3])), [1, 2, 3]);
+    });
+
     // console.log(Interval.findPredecessorIndexInInterval(Interval.ofBounds(0, 3), 2, Interval.ofBounds(0, 3)))
     // console.log(SortedArray.findPredecessorIndexInInterval(SortedArray.ofSortedArray([0, 1, 2]), 2, Interval.ofBounds(0, 3)))
 });
\ No newline at end of file
diff --git a/src/mol-data/int/impl/sorted-array.ts b/src/mol-data/int/impl/sorted-array.ts
index 11a964b17..4a5476c16 100644
--- a/src/mol-data/int/impl/sorted-array.ts
+++ b/src/mol-data/int/impl/sorted-array.ts
@@ -273,6 +273,22 @@ export function subtract(a: Nums, b: Nums) {
     return ofSortedArray(indices);
 }
 
+export function deduplicate(xs: Nums) {
+    if (xs.length < 2) return xs;
+    let count = 1;
+    for (let i = 0, _i = xs.length - 1; i < _i; i++) {
+        if (xs[i] !== xs[i + 1]) count++;
+    }
+    if (count === xs.length) return xs;
+    const ret = new Int32Array(count);
+    let o = 0;
+    for (let i = 0, _i = xs.length - 1; i < _i; i++) {
+        if (xs[i] !== xs[i + 1]) ret[o++] = xs[i];
+    }
+    ret[o] = xs[xs.length - 1];
+    return ret;
+}
+
 const _maxIntRangeRet = { startI: 0, startJ: 0, endI: 0, endJ: 0 };
 // for small sets, just gets the whole range, for large sets does a bunch of binary searches
 function getSuitableIntersectionRange(a: Nums, b: Nums) {
diff --git a/src/mol-data/int/sorted-array.ts b/src/mol-data/int/sorted-array.ts
index 2b8391cb5..928e383dd 100644
--- a/src/mol-data/int/sorted-array.ts
+++ b/src/mol-data/int/sorted-array.ts
@@ -40,6 +40,8 @@ namespace SortedArray {
     export const findPredecessorIndex: (array: SortedArray, x: number) => number = Impl.findPredecessorIndex as any;
     export const findPredecessorIndexInInterval: (array: SortedArray, x: number, bounds: Interval) => number = Impl.findPredecessorIndexInInterval as any;
     export const findRange: (array: SortedArray, min: number, max: number) => Interval = Impl.findRange as any;
+
+    export const deduplicate: (arrat: SortedArray) => SortedArray = Impl.deduplicate as any;
 }
 
 interface SortedArray extends ArrayLike<number> { '@type': 'int-sorted-array' }
diff --git a/src/mol-model/structure/query/generators.ts b/src/mol-model/structure/query/generators.ts
index 01445a1da..8bbc8a17d 100644
--- a/src/mol-model/structure/query/generators.ts
+++ b/src/mol-model/structure/query/generators.ts
@@ -7,8 +7,9 @@
 import Query from './query'
 import Selection from './selection'
 import P from './properties'
-import { Structure, Element, Unit } from '../structure'
+import { Element, Unit } from '../structure'
 import { OrderedSet, Segmentation } from 'mol-data/int'
+import { LinearGroupingBuilder } from './utils/builders';
 
 export const all: Query.Provider = async (s, ctx) => Selection.Singletons(s, s);
 
@@ -113,55 +114,6 @@ function atomGroupsSegmented({ entityTest, chainTest, residueTest, atomTest }: A
     };
 }
 
-class LinearGroupingBuilder {
-    private builders: Structure.SubsetBuilder[] = [];
-    private builderMap = new Map<string, Structure.SubsetBuilder>();
-
-    add(key: any, unit: number, element: number) {
-        let b = this.builderMap.get(key);
-        if (!b) {
-            b = this.source.subsetBuilder(true);
-            this.builders[this.builders.length] = b;
-            this.builderMap.set(key, b);
-        }
-        b.addToUnit(unit, element);
-    }
-
-    private allSingletons() {
-        for (let i = 0, _i = this.builders.length; i < _i; i++) {
-            if (this.builders[i].elementCount > 1) return false;
-        }
-        return true;
-    }
-
-    private singletonSelection(): Selection {
-        const builder = this.source.subsetBuilder(true);
-        const loc = Element.Location();
-        for (let i = 0, _i = this.builders.length; i < _i; i++) {
-            this.builders[i].setSingletonLocation(loc);
-            builder.addToUnit(loc.unit.id, loc.element);
-        }
-        return Selection.Singletons(this.source, builder.getStructure());
-    }
-
-    private fullSelection() {
-        const structures: Structure[] = new Array(this.builders.length);
-        for (let i = 0, _i = this.builders.length; i < _i; i++) {
-            structures[i] = this.builders[i].getStructure();
-        }
-        return Selection.Sequence(this.source, structures);
-    }
-
-    getSelection(): Selection {
-        const len = this.builders.length;
-        if (len === 0) return Selection.Empty(this.source);
-        if (this.allSingletons()) return this.singletonSelection();
-        return this.fullSelection();
-    }
-
-    constructor(private source: Structure) { }
-}
-
 function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, groupBy }: AtomGroupsQueryParams): Query.Provider {
     return async (structure, ctx) => {
         const { units } = structure;
diff --git a/src/mol-model/structure/query/selection.ts b/src/mol-model/structure/query/selection.ts
index ebe352b68..a1fc34f2a 100644
--- a/src/mol-model/structure/query/selection.ts
+++ b/src/mol-model/structure/query/selection.ts
@@ -6,7 +6,7 @@
 
 import { HashSet } from 'mol-data/generic'
 import { Structure } from '../structure'
-import { StructureUtils } from './utils';
+import { structureUnion } from './utils/structure';
 
 // A selection is a pair of a Structure and a sequence of unique AtomSets
 type Selection = Selection.Singletons | Selection.Sequence
@@ -31,7 +31,7 @@ namespace Selection {
     export function unionStructure(sel: Selection): Structure {
         if (isEmpty(sel)) return Structure.Empty;
         if (isSingleton(sel)) return sel.structure;
-        return StructureUtils.union(sel.source, sel.structures);
+        return structureUnion(sel.source, sel.structures);
     }
 
     export interface Builder {
@@ -42,7 +42,7 @@ namespace Selection {
     function getSelection(source: Structure, structures: Structure[], allSingletons: boolean) {
         const len = structures.length;
         if (len === 0) return Empty(source);
-        if (allSingletons) return Singletons(source, StructureUtils.union(source, structures));
+        if (allSingletons) return Singletons(source, structureUnion(source, structures));
         return Sequence(source, structures);
     }
 
diff --git a/src/mol-model/structure/query/utils.ts b/src/mol-model/structure/query/utils.ts
deleted file mode 100644
index 2981878e9..000000000
--- a/src/mol-model/structure/query/utils.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-/**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { Structure, Unit } from '../structure'
-import { SortedArray } from 'mol-data/int';
-
-namespace StructureUtils {
-    export function union(source: Structure, structures: Structure[]) {
-        if (structures.length === 0) return Structure.Empty;
-        if (structures.length === 1) return structures[0];
-
-        const unitMap = new Map<number, SortedArray>();
-        const fullUnits = new Set<number>();
-
-        for (const { units } of structures) {
-            for (let i = 0, _i = units.length; i < _i; i++) {
-                const u = units[i];
-                if (unitMap.has(u.id)) {
-                    // check if there is anything more to union in this particual unit.
-                    if (fullUnits.has(u.id)) continue;
-                    const merged = SortedArray.union(unitMap.get(u.id)!, u.elements);
-                    unitMap.set(u.id, merged);
-                    if (merged.length === source.unitMap.get(u.id).elements.length) fullUnits.add(u.id);
-                } else {
-                    unitMap.set(u.id, u.elements);
-                    if (u.elements.length === source.unitMap.get(u.id).elements.length) fullUnits.add(u.id);
-                }
-            }
-        }
-
-        const builder = source.subsetBuilder(true);
-        unitMap.forEach(buildUnion, builder);
-        return builder.getStructure();
-    }
-
-    function buildUnion(this: Structure.SubsetBuilder, elements: SortedArray, id: number) {
-        this.setUnit(id, elements);
-    }
-
-    export function areIntersecting(sA: Structure, sB: Structure): boolean {
-        if (sA === sB) return true;
-
-        let a, b;
-        if (sA.units.length < sB.units.length) { a = sA; b = sB; }
-        else { a = sB; b = sA; }
-
-        const aU = a.units, bU = b.unitMap;
-
-        for (let i = 0, _i = aU.length; i < _i; i++) {
-            const u = aU[i];
-            if (!bU.has(u.id)) continue;
-            const v = bU.get(u.id);
-            if (SortedArray.areIntersecting(u.elements, v.elements)) return true;
-        }
-
-        return false;
-    }
-
-    export function intersect(sA: Structure, sB: Structure): Structure {
-        if (sA === sB) return sA;
-        if (!areIntersecting(sA, sB)) return Structure.Empty;
-
-        let a, b;
-        if (sA.units.length < sB.units.length) { a = sA; b = sB; }
-        else { a = sB; b = sA; }
-
-        const aU = a.units, bU = b.unitMap;
-        const units: Unit[] = [];
-
-        for (let i = 0, _i = aU.length; i < _i; i++) {
-            const u = aU[i];
-            if (!bU.has(u.id)) continue;
-            const v = bU.get(u.id);
-            if (SortedArray.areIntersecting(u.elements, v.elements)) {
-                const int = SortedArray.intersect(u.elements, v.elements);
-                units[units.length] = u.getChild(int);
-            }
-        }
-
-        return Structure.create(units);
-    }
-
-    export function subtract(a: Structure, b: Structure): Structure {
-        if (a === b) return Structure.Empty;
-        if (!areIntersecting(a, b)) return a;
-
-        const aU = a.units, bU = b.unitMap;
-        const units: Unit[] = [];
-
-        for (let i = 0, _i = aU.length; i < _i; i++) {
-            const u = aU[i];
-            if (!bU.has(u.id)) continue;
-            const v = bU.get(u.id);
-            const sub = SortedArray.intersect(u.elements, v.elements);
-            if (sub.length > 0) {
-                units[units.length] = u.getChild(sub);
-            }
-        }
-
-        return Structure.create(units);
-    }
-}
-
-export { StructureUtils }
\ No newline at end of file
diff --git a/src/mol-model/structure/query/utils/builders.ts b/src/mol-model/structure/query/utils/builders.ts
new file mode 100644
index 000000000..fa9020984
--- /dev/null
+++ b/src/mol-model/structure/query/utils/builders.ts
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Element, Structure } from '../../structure';
+import Selection from '../selection';
+import { HashSet } from 'mol-data/generic';
+import { structureUnion } from './structure';
+
+export class UniqueStructuresBuilder {
+    private set = HashSet(Structure.hashCode, Structure.areEqual);
+    private structures: Structure[] = [];
+    private allSingletons = true;
+
+    add(s: Structure) {
+        if (!s.elementCount) return;
+        if (s.elementCount !== 1) this.allSingletons = false;
+        if (this.set.add(s)) {
+            this.structures[this.structures.length] = s;
+        }
+    }
+
+    getSelection() {
+        if (this.allSingletons) return Selection.Singletons(this.source, structureUnion(this.source, this.structures));
+        return Selection.Sequence(this.source, this.structures);
+    }
+
+    constructor(private source: Structure) {
+    }
+}
+
+export class LinearGroupingBuilder {
+    private builders: Structure.SubsetBuilder[] = [];
+    private builderMap = new Map<string, Structure.SubsetBuilder>();
+
+    add(key: any, unit: number, element: number) {
+        let b = this.builderMap.get(key);
+        if (!b) {
+            b = this.source.subsetBuilder(true);
+            this.builders[this.builders.length] = b;
+            this.builderMap.set(key, b);
+        }
+        b.addToUnit(unit, element);
+    }
+
+    private allSingletons() {
+        for (let i = 0, _i = this.builders.length; i < _i; i++) {
+            if (this.builders[i].elementCount > 1) return false;
+        }
+        return true;
+    }
+
+    private singletonSelection(): Selection {
+        const builder = this.source.subsetBuilder(true);
+        const loc = Element.Location();
+        for (let i = 0, _i = this.builders.length; i < _i; i++) {
+            this.builders[i].setSingletonLocation(loc);
+            builder.addToUnit(loc.unit.id, loc.element);
+        }
+        return Selection.Singletons(this.source, builder.getStructure());
+    }
+
+    private fullSelection() {
+        const structures: Structure[] = new Array(this.builders.length);
+        for (let i = 0, _i = this.builders.length; i < _i; i++) {
+            structures[i] = this.builders[i].getStructure();
+        }
+        return Selection.Sequence(this.source, structures);
+    }
+
+    getSelection(): Selection {
+        const len = this.builders.length;
+        if (len === 0) return Selection.Empty(this.source);
+        if (this.allSingletons()) return this.singletonSelection();
+        return this.fullSelection();
+    }
+
+    constructor(private source: Structure) { }
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/query/utils/structure.ts b/src/mol-model/structure/query/utils/structure.ts
new file mode 100644
index 000000000..903a5d0cb
--- /dev/null
+++ b/src/mol-model/structure/query/utils/structure.ts
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Structure, Unit } from '../../structure'
+import { SortedArray } from 'mol-data/int';
+
+export function structureUnion(source: Structure, structures: Structure[]) {
+    if (structures.length === 0) return Structure.Empty;
+    if (structures.length === 1) return structures[0];
+
+    const unitMap = new Map<number, SortedArray>();
+    const fullUnits = new Set<number>();
+
+    for (const { units } of structures) {
+        for (let i = 0, _i = units.length; i < _i; i++) {
+            const u = units[i];
+            if (unitMap.has(u.id)) {
+                // check if there is anything more to union in this particual unit.
+                if (fullUnits.has(u.id)) continue;
+                const merged = SortedArray.union(unitMap.get(u.id)!, u.elements);
+                unitMap.set(u.id, merged);
+                if (merged.length === source.unitMap.get(u.id).elements.length) fullUnits.add(u.id);
+            } else {
+                unitMap.set(u.id, u.elements);
+                if (u.elements.length === source.unitMap.get(u.id).elements.length) fullUnits.add(u.id);
+            }
+        }
+    }
+
+    const builder = source.subsetBuilder(true);
+    unitMap.forEach(buildUnion, builder);
+    return builder.getStructure();
+}
+
+function buildUnion(this: Structure.SubsetBuilder, elements: SortedArray, id: number) {
+    this.setUnit(id, elements);
+}
+
+export function structureAreIntersecting(sA: Structure, sB: Structure): boolean {
+    if (sA === sB) return true;
+
+    let a, b;
+    if (sA.units.length < sB.units.length) { a = sA; b = sB; }
+    else { a = sB; b = sA; }
+
+    const aU = a.units, bU = b.unitMap;
+
+    for (let i = 0, _i = aU.length; i < _i; i++) {
+        const u = aU[i];
+        if (!bU.has(u.id)) continue;
+        const v = bU.get(u.id);
+        if (SortedArray.areIntersecting(u.elements, v.elements)) return true;
+    }
+
+    return false;
+}
+
+export function structureIntersect(sA: Structure, sB: Structure): Structure {
+    if (sA === sB) return sA;
+    if (!structureAreIntersecting(sA, sB)) return Structure.Empty;
+
+    let a, b;
+    if (sA.units.length < sB.units.length) { a = sA; b = sB; }
+    else { a = sB; b = sA; }
+
+    const aU = a.units, bU = b.unitMap;
+    const units: Unit[] = [];
+
+    for (let i = 0, _i = aU.length; i < _i; i++) {
+        const u = aU[i];
+        if (!bU.has(u.id)) continue;
+        const v = bU.get(u.id);
+        if (SortedArray.areIntersecting(u.elements, v.elements)) {
+            const int = SortedArray.intersect(u.elements, v.elements);
+            units[units.length] = u.getChild(int);
+        }
+    }
+
+    return Structure.create(units);
+}
+
+export function structureSubtract(a: Structure, b: Structure): Structure {
+    if (a === b) return Structure.Empty;
+    if (!structureAreIntersecting(a, b)) return a;
+
+    const aU = a.units, bU = b.unitMap;
+    const units: Unit[] = [];
+
+    for (let i = 0, _i = aU.length; i < _i; i++) {
+        const u = aU[i];
+        if (!bU.has(u.id)) continue;
+        const v = bU.get(u.id);
+        const sub = SortedArray.intersect(u.elements, v.elements);
+        if (sub.length > 0) {
+            units[units.length] = u.getChild(sub);
+        }
+    }
+
+    return Structure.create(units);
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts
index 93d7d4b14..c0b39c2fa 100644
--- a/src/mol-model/structure/structure/structure.ts
+++ b/src/mol-model/structure/structure/structure.ts
@@ -181,7 +181,7 @@ namespace Structure {
             this.elementCount += elements.length;
         }
 
-        getStructure(): Structure {
+        private _getStructure(deduplicateElements: boolean): Structure {
             if (this.isEmpty) return Structure.Empty;
 
             const newUnits: Unit[] = [];
@@ -193,7 +193,15 @@ namespace Structure {
                 const id = this.ids[i];
                 const parent = this.parent.unitMap.get(id);
 
-                const unit = this.unitMap.get(id);
+                let unit: ArrayLike<number> = this.unitMap.get(id);
+                let sorted = false;
+
+                if (deduplicateElements) {
+                    if (!this.isSorted) sortArray(unit);
+                    unit = SortedArray.deduplicate(SortedArray.ofSortedArray(this.currentUnit));
+                    sorted = true;
+                }
+
                 const l = unit.length;
 
                 // if the length is the same, just copy the old unit.
@@ -203,7 +211,7 @@ namespace Structure {
                     continue;
                 }
 
-                if (!this.isSorted && l > 1) sortArray(unit);
+                if (!this.isSorted && !sorted && l > 1) sortArray(unit);
 
                 let child = parent.getChild(SortedArray.ofSortedArray(unit));
                 const pivot = symmGroups.add(child.id, child);
@@ -214,6 +222,10 @@ namespace Structure {
             return create(newUnits);
         }
 
+        getStructure(deduplicateElements = false) {
+            return this._getStructure(deduplicateElements);
+        }
+
         setSingletonLocation(location: Element.Location) {
             const id = this.ids[0];
             location.unit = this.parent.unitMap.get(id);
-- 
GitLab