Pārlūkot izejas kodu

split up StructureElement

Alexander Rose 5 gadi atpakaļ
vecāks
revīzija
2376d0a9c6
76 mainītis faili ar 1031 papildinājumiem un 964 dzēšanām
  1. 1 1
      src/apps/structure-info/model.ts
  2. 2 2
      src/examples/proteopedia-wrapper/coloring.ts
  3. 1 1
      src/mol-model-formats/structure/mmcif/bonds/comp.ts
  4. 4 4
      src/mol-model-props/common/custom-element-property.ts
  5. 2 2
      src/mol-model-props/pdbe/structure-quality-report.ts
  6. 1 1
      src/mol-model-props/pdbe/themes/structure-quality-report.ts
  7. 1 1
      src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
  8. 1 1
      src/mol-model/location.ts
  9. 2 2
      src/mol-model/loci.ts
  10. 7 7
      src/mol-model/structure/export/categories/atom_site.ts
  11. 5 5
      src/mol-model/structure/export/categories/modified-residues.ts
  12. 4 4
      src/mol-model/structure/export/categories/secondary-structure.ts
  13. 7 7
      src/mol-model/structure/model/properties/custom/indexed.ts
  14. 6 6
      src/mol-model/structure/query/context.ts
  15. 2 2
      src/mol-model/structure/query/queries/filters.ts
  16. 3 3
      src/mol-model/structure/query/queries/internal.ts
  17. 1 1
      src/mol-model/structure/query/utils/builders.ts
  18. 1 795
      src/mol-model/structure/structure/element.ts
  19. 54 0
      src/mol-model/structure/structure/element/element.ts
  20. 12 0
      src/mol-model/structure/structure/element/index.ts
  21. 40 0
      src/mol-model/structure/structure/element/location.ts
  22. 422 0
      src/mol-model/structure/structure/element/loci.ts
  23. 208 0
      src/mol-model/structure/structure/element/query.ts
  24. 125 0
      src/mol-model/structure/structure/element/stats.ts
  25. 2 2
      src/mol-model/structure/structure/properties.ts
  26. 7 7
      src/mol-model/structure/structure/structure.ts
  27. 1 1
      src/mol-model/structure/structure/util/lookup3d.ts
  28. 1 1
      src/mol-model/structure/structure/util/subset-builder.ts
  29. 1 1
      src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
  30. 1 1
      src/mol-plugin/behavior/dynamic/labels.ts
  31. 1 1
      src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts
  32. 1 1
      src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
  33. 9 9
      src/mol-plugin/ui/sequence.tsx
  34. 1 1
      src/mol-plugin/ui/sequence/hetero.ts
  35. 2 2
      src/mol-plugin/ui/sequence/polymer.ts
  36. 6 6
      src/mol-plugin/util/interactivity.ts
  37. 6 6
      src/mol-plugin/util/structure-element-selection.ts
  38. 1 1
      src/mol-plugin/util/structure-labels.ts
  39. 2 2
      src/mol-repr/structure/complex-representation.ts
  40. 2 2
      src/mol-repr/structure/units-representation.ts
  41. 2 2
      src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
  42. 3 3
      src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
  43. 2 2
      src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
  44. 1 1
      src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts
  45. 2 2
      src/mol-repr/structure/visual/inter-unit-link-cylinder.ts
  46. 2 2
      src/mol-repr/structure/visual/intra-unit-link-cylinder.ts
  47. 2 2
      src/mol-repr/structure/visual/util/common.ts
  48. 5 5
      src/mol-repr/structure/visual/util/element.ts
  49. 1 1
      src/mol-repr/structure/visual/util/link.ts
  50. 2 2
      src/mol-repr/structure/visual/util/nucleotide.ts
  51. 4 4
      src/mol-repr/structure/visual/util/polymer.ts
  52. 4 4
      src/mol-repr/structure/visual/util/polymer/backbone-iterator.ts
  53. 4 4
      src/mol-repr/structure/visual/util/polymer/gap-iterator.ts
  54. 6 6
      src/mol-repr/structure/visual/util/polymer/trace-iterator.ts
  55. 1 1
      src/mol-script/runtime/query/table.ts
  56. 1 1
      src/mol-theme/color/carbohydrate-symbol.ts
  57. 2 2
      src/mol-theme/color/chain-id.ts
  58. 1 1
      src/mol-theme/color/element-index.ts
  59. 1 1
      src/mol-theme/color/element-symbol.ts
  60. 3 3
      src/mol-theme/color/entity-source.ts
  61. 1 1
      src/mol-theme/color/hydrophobicity.ts
  62. 1 1
      src/mol-theme/color/illustrative.ts
  63. 1 1
      src/mol-theme/color/model-index.ts
  64. 1 1
      src/mol-theme/color/molecule-type.ts
  65. 2 2
      src/mol-theme/color/polymer-id.ts
  66. 1 1
      src/mol-theme/color/polymer-index.ts
  67. 1 1
      src/mol-theme/color/residue-name.ts
  68. 1 1
      src/mol-theme/color/secondary-structure.ts
  69. 1 1
      src/mol-theme/color/sequence-id.ts
  70. 1 1
      src/mol-theme/color/uncertainty.ts
  71. 1 1
      src/mol-theme/color/unit-index.ts
  72. 10 10
      src/mol-theme/label.ts
  73. 1 1
      src/mol-theme/size/physical.ts
  74. 1 1
      src/mol-theme/size/uncertainty.ts
  75. 1 1
      src/perf-tests/structure.ts
  76. 1 1
      src/tests/browser/render-asa.ts

+ 1 - 1
src/apps/structure-info/model.ts

@@ -146,7 +146,7 @@ export function printRings(structure: Structure) {
 
 export function printUnits(structure: Structure) {
     console.log('\nUnits\n=============');
-    const l = StructureElement.create();
+    const l = StructureElement.Location.create();
 
     for (const unit of structure.units) {
         l.unit = unit;

+ 2 - 2
src/examples/proteopedia-wrapper/coloring.ts

@@ -54,7 +54,7 @@ export function createProteopediaCustomTheme(colors: number[]) {
         const colors = props.colors, colorCount = colors.length, defaultColor = colors[0].color;
 
         if (ctx.structure) {
-            const l = StructureElement.create()
+            const l = StructureElement.Location.create()
             const { models } = ctx.structure
             const asymIdSerialMap = new Map<string, number>()
             for (let i = 0, il = models.length; i < il; ++i) {
@@ -67,7 +67,7 @@ export function createProteopediaCustomTheme(colors: number[]) {
             }
 
             color = (location: Location): Color => {
-                if (StructureElement.isLocation(location)) {
+                if (StructureElement.Location.is(location)) {
                     const asym_id = getAsymId(location.unit);
                     const o = asymIdSerialMap.get(asym_id(location)) || 0;
                     return colors[o % colorCount].color;

+ 1 - 1
src/mol-model-formats/structure/mmcif/bonds/comp.ts

@@ -148,7 +148,7 @@ export namespace ComponentBond {
     function getUniqueResidueNames(s: Structure) {
         const prop = StructureProperties.residue.label_comp_id;
         const names = new Set<string>();
-        const loc = StructureElement.create();
+        const loc = StructureElement.Location.create();
         for (const unit of s.units) {
             if (!Unit.isAtomic(unit)) continue;
             const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);

+ 4 - 4
src/mol-model-props/common/custom-element-property.ts

@@ -64,8 +64,8 @@ namespace CustomElementProperty {
             })
         }
 
-        function getStatic(e: StructureElement) { return e.unit.model._staticPropertyData[name].get(e.element); }
-        function getDynamic(e: StructureElement) { return e.unit.model._staticPropertyData[name].get(e.element); }
+        function getStatic(e: StructureElement.Location) { return e.unit.model._staticPropertyData[name].get(e.element); }
+        function getDynamic(e: StructureElement.Location) { return e.unit.model._staticPropertyData[name].get(e.element); }
 
         const propertyProvider: CustomPropertyRegistry.ModelProvider = {
             option: [name, params.display],
@@ -86,7 +86,7 @@ namespace CustomElementProperty {
 
             if (ctx.structure && !ctx.structure.isEmpty && has(ctx.structure.models[0])) {
                 color = (location: Location) => {
-                    if (StructureElement.isLocation(location)) {
+                    if (StructureElement.Location.is(location)) {
                         const e = get(location);
                         if (typeof e !== 'undefined') return getColor(e);
                     }
@@ -118,7 +118,7 @@ namespace CustomElementProperty {
             if (loci.kind === 'element-loci') {
                 const e = loci.elements[0];
                 if (!has(e.unit.model)) return void 0;
-                return params.format!(get(StructureElement.create(e.unit, e.unit.elements[OrderedSet.getAt(e.indices, 0)])));
+                return params.format!(get(StructureElement.Location.create(e.unit, e.unit.elements[OrderedSet.getAt(e.indices, 0)])));
             }
             return void 0;
         }

+ 2 - 2
src/mol-model-props/pdbe/structure-quality-report.ts

@@ -145,7 +145,7 @@ export namespace StructureQualityReport {
     }
 
     const _emptyArray: string[] = [];
-    export function getIssues(e: StructureElement) {
+    export function getIssues(e: StructureElement.Location) {
         if (!Unit.isAtomic(e.unit)) return _emptyArray;
         const prop = StructureQualityReport.get(e.unit.model);
         if (!prop || !prop.data) return _emptyArray;
@@ -162,7 +162,7 @@ const _structure_quality_report_issues_fields = CifWriter.fields<number, ReportE
 
 interface ReportExportContext {
     models: {
-        elements: StructureElement[],
+        elements: StructureElement.Location[],
         groupId: number[]
     }[],
     info: PropertyWrapper.Info,

+ 1 - 1
src/mol-model-props/pdbe/themes/structure-quality-report.ts

@@ -34,7 +34,7 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: {
     if (ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)) {
         const getIssues = StructureQualityReport.getIssues;
         color = (location: Location) => {
-            if (StructureElement.isLocation(location)) {
+            if (StructureElement.Location.is(location)) {
                 return ValidationColors[Math.min(3, getIssues(location).length) + 1];
             }
             return ValidationColors[0];

+ 1 - 1
src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts

@@ -75,7 +75,7 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
                 legend = palette.legend
 
                 color = (location: Location): Color => {
-                    if (StructureElement.isLocation(location)) {
+                    if (StructureElement.Location.is(location)) {
                         const { assembly } = location.unit.conformation.operator
                         if (assembly && assembly.id === symmetry.assembly_id) {
                             const asymId = getAsymId(location.unit)(location)

+ 1 - 1
src/mol-model/location.ts

@@ -15,4 +15,4 @@ export function isNullLocation(x: any): x is NullLocation {
     return !!x && x.kind === 'null-location';
 }
 
-export type Location = StructureElement | Link.Location | ShapeGroup.Location | NullLocation
+export type Location = StructureElement.Location | Link.Location | ShapeGroup.Location | NullLocation

+ 2 - 2
src/mol-model/loci.ts

@@ -58,8 +58,8 @@ namespace Loci {
         if (Structure.isLoci(lociA) && Structure.isLoci(lociB)) {
             return Structure.areLociEqual(lociA, lociB)
         }
-        if (StructureElement.isLoci(lociA) && StructureElement.isLoci(lociB)) {
-            return StructureElement.areLociEqual(lociA, lociB)
+        if (StructureElement.Loci.is(lociA) && StructureElement.Loci.is(lociB)) {
+            return StructureElement.Loci.areEqual(lociA, lociB)
         }
         if (Link.isLoci(lociA) && Link.isLoci(lociB)) {
             return Link.areLociEqual(lociA, lociB)

+ 7 - 7
src/mol-model/structure/export/categories/atom_site.ts

@@ -12,7 +12,7 @@ import CifField = CifWriter.Field
 import CifCategory = CifWriter.Category
 import E = CifWriter.Encodings
 
-const atom_site_fields = CifWriter.fields<StructureElement, Structure>()
+const atom_site_fields = CifWriter.fields<StructureElement.Location, Structure>()
     .str('group_PDB', P.residue.group_PDB)
     .index('id')
     .str('type_symbol', P.atom.type_symbol as any)
@@ -70,11 +70,11 @@ function prepostfixed(prefix: string | undefined, postfix: string | undefined, n
     return name;
 }
 
-function mappedProp<K, D>(loc: (key: K, data: D) => StructureElement, prop: (e: StructureElement) => any) {
+function mappedProp<K, D>(loc: (key: K, data: D) => StructureElement.Location, prop: (e: StructureElement.Location) => any) {
     return (k: K, d: D) => prop(loc(k, d));
 }
 
-function addModelNum<K, D>(fields: CifWriter.Field.Builder<K, D>, getLocation: (key: K, data: D) => StructureElement, options?: IdFieldsOptions) {
+function addModelNum<K, D>(fields: CifWriter.Field.Builder<K, D>, getLocation: (key: K, data: D) => StructureElement.Location, options?: IdFieldsOptions) {
     if (options && options.includeModelNum) {
         fields.int('pdbx_PDB_model_num', mappedProp(getLocation, P.unit.model_num));
     }
@@ -86,7 +86,7 @@ export interface IdFieldsOptions {
     includeModelNum?: boolean
 }
 
-export function residueIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: IdFieldsOptions): CifField<K, D>[] {
+export function residueIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement.Location, options?: IdFieldsOptions): CifField<K, D>[] {
     const prefix = options && options.prefix, postfix = options && options.postfix;
     const ret = CifWriter.fields<K, D>()
         .str(prepostfixed(prefix, postfix, `label_comp_id`), mappedProp(getLocation, P.residue.label_comp_id))
@@ -111,7 +111,7 @@ export function residueIdFields<K, D>(getLocation: (key: K, data: D) => Structur
     return ret.getFields();
 }
 
-export function chainIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: IdFieldsOptions): CifField<K, D>[] {
+export function chainIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement.Location, options?: IdFieldsOptions): CifField<K, D>[] {
     const prefix = options && options.prefix, postfix = options && options.postfix;
     const ret = CifField.build<K, D>()
         .str(prepostfixed(prefix, postfix, `label_asym_id`), mappedProp(getLocation, P.chain.label_asym_id))
@@ -122,7 +122,7 @@ export function chainIdFields<K, D>(getLocation: (key: K, data: D) => StructureE
     return ret.getFields();
 }
 
-export function entityIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: IdFieldsOptions): CifField<K, D>[] {
+export function entityIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement.Location, options?: IdFieldsOptions): CifField<K, D>[] {
     const prefix = options && options.prefix, postfix = options && options.postfix;
     const ret = CifField.build<K, D>()
         .str(prepostfixed(prefix, postfix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id))
@@ -131,7 +131,7 @@ export function entityIdFields<K, D>(getLocation: (key: K, data: D) => Structure
     return ret.getFields();
 }
 
-export function atomIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement, options?: IdFieldsOptions): CifField<K, D>[] {
+export function atomIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement.Location, options?: IdFieldsOptions): CifField<K, D>[] {
     const prefix = options && options.prefix, postfix = options && options.postfix;
     const ret = CifWriter.fields<K, D>()
         .str(prepostfixed(prefix, postfix, `label_atom_id`), mappedProp(getLocation, P.atom.label_atom_id))

+ 5 - 5
src/mol-model/structure/export/categories/modified-residues.ts

@@ -13,7 +13,7 @@ import { CifExportContext } from '../mmcif';
 import CifField = CifWriter.Field
 import CifCategory = CifWriter.Category
 
-const pdbx_struct_mod_residue_fields: CifField<number, StructureElement[]>[] = [
+const pdbx_struct_mod_residue_fields: CifField<number, StructureElement.Location[]>[] = [
     CifField.index('id'),
     CifField.str(`label_comp_id`, (i, xs) => P.residue.label_comp_id(xs[i])),
     CifField.int(`label_seq_id`, (i, xs) => P.residue.label_seq_id(xs[i])),
@@ -23,11 +23,11 @@ const pdbx_struct_mod_residue_fields: CifField<number, StructureElement[]>[] = [
     CifField.str(`auth_comp_id`, (i, xs) => P.residue.auth_comp_id(xs[i])),
     CifField.int(`auth_seq_id`, (i, xs) => P.residue.auth_seq_id(xs[i])),
     CifField.str(`auth_asym_id`, (i, xs) => P.chain.auth_asym_id(xs[i])),
-    CifField.str<number, StructureElement[]>('parent_comp_id', (i, xs) => xs[i].unit.model.properties.modifiedResidues.parentId.get(P.residue.label_comp_id(xs[i]))!),
+    CifField.str<number, StructureElement.Location[]>('parent_comp_id', (i, xs) => xs[i].unit.model.properties.modifiedResidues.parentId.get(P.residue.label_comp_id(xs[i]))!),
     CifField.str('details', (i, xs) => xs[i].unit.model.properties.modifiedResidues.details.get(P.residue.label_comp_id(xs[i]))!)
 ];
 
-function getModifiedResidues({ structures }: CifExportContext): StructureElement[] {
+function getModifiedResidues({ structures }: CifExportContext): StructureElement.Location[] {
     // TODO: can different models (in the same mmCIF file) have different modified residues?
     const structure = structures[0], model = structure.model;
     const map = model.properties.modifiedResidues.parentId;
@@ -35,7 +35,7 @@ function getModifiedResidues({ structures }: CifExportContext): StructureElement
 
     const ret = [];
     const prop = P.residue.label_comp_id;
-    const loc = StructureElement.create();
+    const loc = StructureElement.Location.create();
     for (const unit of structure.units) {
         if (!Unit.isAtomic(unit) || !unit.conformation.operator.isIdentity) continue;
         const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
@@ -45,7 +45,7 @@ function getModifiedResidues({ structures }: CifExportContext): StructureElement
             loc.element = unit.elements[seg.start];
             const name = prop(loc);
             if (map.has(name)) {
-                ret[ret.length] = StructureElement.create(loc.unit, loc.element);
+                ret[ret.length] = StructureElement.Location.create(loc.unit, loc.element);
             }
         }
     }

+ 4 - 4
src/mol-model/structure/export/categories/secondary-structure.ts

@@ -62,8 +62,8 @@ const struct_sheet_range_fields: CifField[] = [
 ];
 
 interface SSElement<T extends SecondaryStructure.Element> {
-    start: StructureElement,
-    end: StructureElement,
+    start: StructureElement.Location,
+    end: StructureElement.Location,
     length: number,
     element: T
 }
@@ -100,8 +100,8 @@ function findElements<T extends SecondaryStructure.Element>(ctx: CifExportContex
                 if (startIdx !== key[current.index]) {
                     move = false;
                     ssElements[ssElements.length] = {
-                        start: StructureElement.create(unit, segs.offsets[start]),
-                        end: StructureElement.create(unit, segs.offsets[prev]),
+                        start: StructureElement.Location.create(unit, segs.offsets[start]),
+                        end: StructureElement.Location.create(unit, segs.offsets[prev]),
                         length: prev - start + 1,
                         element
                     }

+ 7 - 7
src/mol-model/structure/model/properties/custom/indexed.ts

@@ -25,7 +25,7 @@ export namespace IndexedCustomProperty {
     export type Level = 'atom' | 'residue' | 'chain' | 'entity'
 
     export interface Elements<T> {
-        elements: StructureElement[],
+        elements: StructureElement.Location[],
         property(index: number): T
     }
 
@@ -94,7 +94,7 @@ class SegmentedMappedIndexedCustomProperty<Idx extends IndexedCustomProperty.Ind
 
         const seenIndices = new Set<Idx>();
         const unitGroups = structure.unitSymmetryGroups;
-        const loci: StructureElement[] = [];
+        const loci: StructureElement.Location[] = [];
 
         const segments = this.segmentGetter(models[0]);
 
@@ -109,7 +109,7 @@ class SegmentedMappedIndexedCustomProperty<Idx extends IndexedCustomProperty.Ind
                 const seg = chains.move();
                 if (!this.has(seg.index) || seenIndices.has(seg.index)) continue;
                 seenIndices.add(seg.index);
-                loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]);
+                loci[loci.length] = StructureElement.Location.create(unit, unit.elements[seg.start]);
             }
         }
 
@@ -141,7 +141,7 @@ class ElementMappedCustomProperty<T = any> implements IndexedCustomProperty<Elem
 
         const seenIndices = new Set<ElementIndex>();
         const unitGroups = structure.unitSymmetryGroups;
-        const loci: StructureElement[] = [];
+        const loci: StructureElement.Location[] = [];
 
         for (const unitGroup of unitGroups) {
             const unit = unitGroup.units[0];
@@ -154,7 +154,7 @@ class ElementMappedCustomProperty<T = any> implements IndexedCustomProperty<Elem
                 const e = elements[i];
                 if (!this.has(e) || seenIndices.has(e)) continue;
                 seenIndices.add(elements[i]);
-                loci[loci.length] = StructureElement.create(unit, e);
+                loci[loci.length] = StructureElement.Location.create(unit, e);
             }
         }
 
@@ -186,7 +186,7 @@ class EntityMappedCustomProperty<T = any> implements IndexedCustomProperty<Entit
         const index = models[0].atomicHierarchy.index;
         const seenIndices = new Set<EntityIndex>();
         const unitGroups = structure.unitSymmetryGroups;
-        const loci: StructureElement[] = [];
+        const loci: StructureElement.Location[] = [];
 
         const segments = models[0].atomicHierarchy.chainAtomSegments;
 
@@ -202,7 +202,7 @@ class EntityMappedCustomProperty<T = any> implements IndexedCustomProperty<Entit
                 const eI = index.getEntityFromChain(seg.index);
                 if (!this.has(eI) || seenIndices.has(eI)) continue;
                 seenIndices.add(eI);
-                loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]);
+                loci[loci.length] = StructureElement.Location.create(unit, unit.elements[seg.start]);
             }
         }
 

+ 6 - 6
src/mol-model/structure/query/context.ts

@@ -11,12 +11,12 @@ import { Link } from '../structure/unit/links';
 import { LinkType } from '../model/types';
 
 export interface QueryContextView {
-    readonly element: StructureElement;
+    readonly element: StructureElement.Location;
     readonly currentStructure: Structure;
 }
 
 export class QueryContext implements QueryContextView {
-    private currentElementStack: StructureElement[] = [];
+    private currentElementStack: StructureElement.Location[] = [];
     private currentAtomicLinkStack: QueryContextLinkInfo<Unit.Atomic>[] = [];
     private currentStructureStack: Structure[] = [];
     private inputStructureStack: Structure[] = [];
@@ -27,7 +27,7 @@ export class QueryContext implements QueryContextView {
     readonly inputStructure: Structure;
 
     /** Current element */
-    readonly element: StructureElement = StructureElement.create();
+    readonly element = StructureElement.Location.create();
     currentStructure: Structure = void 0 as any;
 
     /** Current link between atoms */
@@ -38,14 +38,14 @@ export class QueryContext implements QueryContextView {
         this.element.element = e;
     }
 
-    pushCurrentElement(): StructureElement {
+    pushCurrentElement(): StructureElement.Location {
         this.currentElementStack[this.currentElementStack.length] = this.element;
-        (this.element as StructureElement) = StructureElement.create();
+        (this.element as StructureElement.Location) = StructureElement.Location.create();
         return this.element;
     }
 
     popCurrentElement() {
-        (this.element as StructureElement) = this.currentElementStack.pop()!;
+        (this.element as StructureElement.Location) = this.currentElementStack.pop()!;
     }
 
     pushCurrentLink() {

+ 2 - 2
src/mol-model/structure/query/queries/filters.ts

@@ -233,7 +233,7 @@ interface IsConnectedToCtx {
     input: Structure,
     target: Structure,
     linkTest: QueryFn<boolean>,
-    tElement: StructureElement
+    tElement: StructureElement.Location
 }
 
 function checkConnected(ctx: IsConnectedToCtx, structure: Structure) {
@@ -313,7 +313,7 @@ export function isConnectedTo({ query, target, disjunct, invert, linkTest }: IsC
             input: ctx.inputStructure,
             target: StructureSelection.unionStructure(targetSel),
             linkTest: linkTest || defaultLinkTest,
-            tElement: StructureElement.create()
+            tElement: StructureElement.Location.create()
         }
 
         const ret = StructureSelection.LinearBuilder(ctx.inputStructure);

+ 3 - 3
src/mol-model/structure/query/queries/internal.ts

@@ -20,7 +20,7 @@ export function defaultLinkTest(ctx: QueryContext) {
 export function atomicSequence(): StructureQuery {
     return ctx => {
         const { inputStructure } = ctx;
-        const l = StructureElement.create();
+        const l = StructureElement.Location.create();
 
         const units: Unit[] = [];
         for (const unit of inputStructure.units) {
@@ -48,7 +48,7 @@ export function atomicSequence(): StructureQuery {
 export function water(): StructureQuery {
     return ctx => {
         const { inputStructure } = ctx;
-        const l = StructureElement.create();
+        const l = StructureElement.Location.create();
 
         const units: Unit[] = [];
         for (const unit of inputStructure.units) {
@@ -67,7 +67,7 @@ export function water(): StructureQuery {
 export function atomicHet(): StructureQuery {
     return ctx => {
         const { inputStructure } = ctx;
-        const l = StructureElement.create();
+        const l = StructureElement.Location.create();
 
         const units: Unit[] = [];
         for (const unit of inputStructure.units) {

+ 1 - 1
src/mol-model/structure/query/utils/builders.ts

@@ -56,7 +56,7 @@ export class LinearGroupingBuilder {
 
     private singletonSelection(): StructureSelection {
         const builder = this.source.subsetBuilder(true);
-        const loc = StructureElement.create();
+        const loc = StructureElement.Location.create();
         for (let i = 0, _i = this.builders.length; i < _i; i++) {
             this.builders[i].setSingletonLocation(loc);
             builder.addToUnit(loc.unit.id, loc.element);

+ 1 - 795
src/mol-model/structure/structure/element.ts

@@ -5,800 +5,6 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { UniqueArray } from '../../../mol-data/generic';
-import { OrderedSet, SortedArray, Interval } from '../../../mol-data/int';
-import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
-import { Vec3 } from '../../../mol-math/linear-algebra';
-import { MolScriptBuilder as MS } from '../../../mol-script/language/builder';
-import { ElementIndex } from '../model';
-import { ChainIndex, ResidueIndex } from '../model/indexing';
-import Structure from './structure';
-import Unit from './unit';
-import { Boundary } from './util/boundary';
-import { StructureProperties } from '../structure';
-import { sortArray, hashFnv32a, hash2 } from '../../../mol-data/util';
-import Expression from '../../../mol-script/language/expression';
-import SortedRanges from '../../../mol-data/int/sorted-ranges';
-
-interface StructureElement<U = Unit> {
-    readonly kind: 'element-location',
-    unit: U,
-    /** Index into element (atomic/coarse) properties of unit.model */
-    element: ElementIndex
-}
-
-namespace StructureElement {
-    export function create(unit?: Unit, element?: ElementIndex): StructureElement {
-        return { kind: 'element-location', unit: unit!, element: element || (0 as ElementIndex) };
-    }
-
-    export function set(a: StructureElement, unit?: Unit, element?: ElementIndex): StructureElement {
-        if (unit) a.unit = unit
-        if (element !== undefined) a.element = element
-        return a;
-    }
-
-    export function copy(out: StructureElement, a: StructureElement): StructureElement {
-        out.unit = a.unit
-        out.element = a.element
-        return out
-    }
-
-    // TODO: when nominal types are available, make this indexed by UnitIndex
-    export type Set = SortedArray<ElementIndex>
-
-    /** Index into Unit.elements */
-    export type UnitIndex = { readonly '@type': 'structure-element-index' } & number
-
-    export interface Property<T> { (location: StructureElement): T }
-    export interface Predicate extends Property<boolean> { }
-
-    export function property<T>(p: Property<T>) { return p; }
-
-    function _wrongUnitKind(kind: string) { throw new Error(`Property only available for ${kind} models.`); }
-    export function atomicProperty<T>(p: (location: StructureElement<Unit.Atomic>) => T) {
-        return property(l => Unit.isAtomic(l.unit) ? p(l as StructureElement<Unit.Atomic>) : _wrongUnitKind('atomic'));
-    }
-
-    export function coarseProperty<T>(p: (location: StructureElement<Unit.Spheres | Unit.Gaussians>) => T) {
-        return property(l => Unit.isCoarse(l.unit) ? p(l as StructureElement<Unit.Spheres | Unit.Gaussians>) : _wrongUnitKind('coarse'));
-    }
-
-    /** Represents multiple element index locations */
-    export interface Loci {
-        readonly kind: 'element-loci',
-        readonly structure: Structure,
-        /** Access i-th element as unit.elements[indices[i]] */
-        readonly elements: ReadonlyArray<{
-            unit: Unit,
-            /**
-             * Indices into the unit.elements array.
-             * Can use OrderedSet.forEach to iterate (or OrderedSet.size + OrderedSet.getAt)
-             */
-            indices: OrderedSet<UnitIndex>
-        }>
-    }
-
-    export function Loci(structure: Structure, elements: ArrayLike<{ unit: Unit, indices: OrderedSet<UnitIndex> }>): Loci {
-        return { kind: 'element-loci', structure, elements: elements as Loci['elements'] };
-    }
-
-    export function isLoci(x: any): x is Loci {
-        return !!x && x.kind === 'element-loci';
-    }
-
-    export function areLociEqual(a: Loci, b: Loci) {
-        if (a.elements.length !== b.elements.length) return false
-        for (let i = 0, il = a.elements.length; i < il; ++i) {
-            const elementA = a.elements[i]
-            const elementB = b.elements[i]
-            if (elementA.unit.id !== elementB.unit.id) return false
-            if (!OrderedSet.areEqual(elementA.indices, elementB.indices)) return false
-        }
-        return true
-    }
-
-    export function isLocation(x: any): x is StructureElement {
-        return !!x && x.kind === 'element-location';
-    }
-
-    export function residueIndex(e: StructureElement) {
-        if (Unit.isAtomic(e.unit)) {
-            return e.unit.residueIndex[e.element];
-        } else {
-            // TODO: throw error instead?
-            return -1 as ResidueIndex;
-        }
-    }
-
-    export function chainIndex(e: StructureElement) {
-        if (Unit.isAtomic(e.unit)) {
-            return e.unit.chainIndex[e.element];
-        } else {
-            // TODO: throw error instead?
-            return -1 as ChainIndex;
-        }
-    }
-
-    export function entityIndex(l: StructureElement) {
-        return StructureProperties.entity.key(l)
-    }
-
-    export namespace Loci {
-        export function size(loci: Loci) {
-            let s = 0;
-            for (const u of loci.elements) s += OrderedSet.size(u.indices);
-            return s;
-        }
-
-        export function all(structure: Structure): Loci {
-            return Loci(structure, structure.units.map(unit => ({
-                unit,
-                indices: OrderedSet.ofBounds<UnitIndex>(0 as UnitIndex, unit.elements.length as UnitIndex)
-            })));
-        }
-
-        export function none(structure: Structure): Loci {
-            return Loci(structure, []);
-        }
-
-        export function remap(loci: Loci, structure: Structure): Loci {
-            if (structure === loci.structure) return loci
-
-            const elements: Loci['elements'][0][] = [];
-            loci.elements.forEach(e => {
-                if (!structure.unitMap.has(e.unit.id)) return
-                const unit = structure.unitMap.get(e.unit.id)
-
-                if (SortedArray.areEqual(e.unit.elements, unit.elements)) {
-                    elements.push({ unit, indices: e.indices })
-                } else {
-                    const _indices: UnitIndex[] = []
-                    const end = unit.elements.length
-                    let start = 0
-                    for (let i = 0; i < OrderedSet.size(e.indices); ++i) {
-                        const v = OrderedSet.getAt(e.indices, i)
-                        const eI = e.unit.elements[v]
-                        const uI = SortedArray.indexOfInRange(unit.elements, eI, start, end) as UnitIndex | -1
-                        if (uI !== -1) {
-                            _indices.push(uI)
-                            start = uI
-                        }
-                    }
-
-                    let indices: OrderedSet<UnitIndex>
-                    if (_indices.length > 12 && _indices[_indices.length - 1] - _indices[0] === _indices.length - 1) {
-                        indices = Interval.ofRange(_indices[0], _indices[_indices.length - 1])
-                    } else {
-                        indices = SortedArray.ofSortedArray(_indices)
-                    }
-
-                    elements.push({ unit, indices })
-                }
-            });
-
-            return Loci(structure, elements);
-        }
-
-        /** Create union of `xs` and `ys` */
-        export function union(xs: Loci, ys: Loci): Loci {
-            if (xs.elements.length > ys.elements.length) return union(ys, xs);
-            if (xs.elements.length === 0) return ys;
-
-            const map = new Map<number, OrderedSet<UnitIndex>>();
-
-            for (const e of xs.elements) map.set(e.unit.id, e.indices);
-
-            const elements: Loci['elements'][0][] = [];
-            for (const e of ys.elements) {
-                if (map.has(e.unit.id)) {
-                    elements[elements.length] = { unit: e.unit, indices: OrderedSet.union(map.get(e.unit.id)!, e.indices) };
-                    map.delete(e.unit.id)
-                } else {
-                    elements[elements.length] = e;
-                }
-            }
-
-            map.forEach((indices, id) => {
-                elements[elements.length] = { unit: xs.structure.unitMap.get(id)!, indices };
-            });
-
-            return Loci(xs.structure, elements);
-        }
-
-        /** Subtract `ys` from `xs` */
-        export function subtract(xs: Loci, ys: Loci): Loci {
-            const map = new Map<number, OrderedSet<UnitIndex>>();
-            for (const e of ys.elements) map.set(e.unit.id, e.indices);
-
-            const elements: Loci['elements'][0][] = [];
-            for (const e of xs.elements) {
-                if (map.has(e.unit.id)) {
-                    const indices = OrderedSet.subtract(e.indices, map.get(e.unit.id)!);
-                    if (OrderedSet.size(indices) === 0) continue;
-                    elements[elements.length] = { unit: e.unit, indices };
-                } else {
-                    elements[elements.length] = e;
-                }
-            }
-
-            return Loci(xs.structure, elements);
-        }
-
-        export function areIntersecting(xs: Loci, ys: Loci): boolean {
-            if (xs.elements.length > ys.elements.length) return areIntersecting(ys, xs);
-            if (xs.elements.length === 0) return ys.elements.length === 0;
-
-            const map = new Map<number, OrderedSet<UnitIndex>>();
-
-            for (const e of xs.elements) map.set(e.unit.id, e.indices);
-            for (const e of ys.elements) {
-                if (!map.has(e.unit.id)) continue;
-                if (OrderedSet.areIntersecting(map.get(e.unit.id)!, e.indices)) return true;
-            }
-
-            return false;
-        }
-
-        export function extendToWholeResidues(loci: Loci): Loci {
-            const elements: Loci['elements'][0][] = [];
-
-            for (const lociElement of loci.elements) {
-                if (lociElement.unit.kind === Unit.Kind.Atomic) {
-                    const unitElements = lociElement.unit.elements;
-                    const h = lociElement.unit.model.atomicHierarchy;
-
-                    const { index: residueIndex, offsets: residueOffsets } = h.residueAtomSegments;
-
-                    const newIndices: UnitIndex[] = [];
-                    const indices = lociElement.indices, len = OrderedSet.size(indices);
-                    let i = 0;
-                    while (i < len) {
-                        const rI = residueIndex[unitElements[OrderedSet.getAt(indices, i)]];
-                        i++;
-                        while (i < len && residueIndex[unitElements[OrderedSet.getAt(indices, i)]] === rI) {
-                            i++;
-                        }
-
-                        for (let j = residueOffsets[rI], _j = residueOffsets[rI + 1]; j < _j; j++) {
-                            const idx = OrderedSet.indexOf(unitElements, j);
-                            if (idx >= 0) newIndices[newIndices.length] = idx as UnitIndex;
-                        }
-                    }
-
-                    elements[elements.length] = { unit: lociElement.unit, indices: SortedArray.ofSortedArray(newIndices) };
-                } else {
-                    // coarse elements are already by-residue
-                    elements[elements.length] = lociElement;
-                }
-            }
-
-            return Loci(loci.structure, elements);
-        }
-
-        function getChainSegments(unit: Unit) {
-            switch (unit.kind) {
-                case Unit.Kind.Atomic: return unit.model.atomicHierarchy.chainAtomSegments
-                case Unit.Kind.Spheres: return unit.model.coarseHierarchy.spheres.chainElementSegments
-                case Unit.Kind.Gaussians: return unit.model.coarseHierarchy.gaussians.chainElementSegments
-            }
-        }
-
-        export function extendToWholeChains(loci: Loci): Loci {
-            const elements: Loci['elements'][0][] = [];
-
-            for (const lociElement of loci.elements) {
-                const _newIndices: UnitIndex[] = [];
-                const unitElements = lociElement.unit.elements;
-
-                const { index: chainIndex, offsets: chainOffsets } = getChainSegments(lociElement.unit)
-
-                const indices = lociElement.indices, len = OrderedSet.size(indices);
-                let i = 0;
-                while (i < len) {
-                    const cI = chainIndex[unitElements[OrderedSet.getAt(indices, i)]];
-                    i++;
-                    while (i < len && chainIndex[unitElements[OrderedSet.getAt(indices, i)]] === cI) {
-                        i++;
-                    }
-
-                    for (let j = chainOffsets[cI], _j = chainOffsets[cI + 1]; j < _j; j++) {
-                        const idx = OrderedSet.indexOf(unitElements, j);
-                        if (idx >= 0) _newIndices[_newIndices.length] = idx as UnitIndex;
-                    }
-                }
-
-                let newIndices: OrderedSet<UnitIndex>
-                if (_newIndices.length > 12 && _newIndices[_newIndices.length - 1] - _newIndices[0] === _newIndices.length - 1) {
-                    newIndices = Interval.ofRange(_newIndices[0], _newIndices[_newIndices.length - 1])
-                } else {
-                    newIndices = SortedArray.ofSortedArray(_newIndices)
-                }
-
-                elements[elements.length] = { unit: lociElement.unit, indices: newIndices };
-            }
-
-            return Loci(loci.structure, elements);
-        }
-
-        const boundaryHelper = new BoundaryHelper(), tempPos = Vec3.zero();
-        export function getBoundary(loci: Loci): Boundary {
-            boundaryHelper.reset(0);
-
-            for (const e of loci.elements) {
-                const { indices } = e;
-                const pos = e.unit.conformation.position, r = e.unit.conformation.r;
-                const { elements } = e.unit;
-                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
-                    const eI = elements[OrderedSet.getAt(indices, i)];
-                    pos(eI, tempPos);
-                    boundaryHelper.boundaryStep(tempPos, r(eI));
-                }
-            }
-            boundaryHelper.finishBoundaryStep();
-            for (const e of loci.elements) {
-                const { indices } = e;
-                const pos = e.unit.conformation.position, r = e.unit.conformation.r;
-                const { elements } = e.unit;
-                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
-                    const eI = elements[OrderedSet.getAt(indices, i)];
-                    pos(eI, tempPos);
-                    boundaryHelper.extendStep(tempPos, r(eI));
-                }
-            }
-
-            return { box: boundaryHelper.getBox(), sphere: boundaryHelper.getSphere() };
-        }
-
-        export function toScriptExpression(loci: Loci) {
-            if (loci.elements.length === 0) return MS.struct.generator.empty();
-
-            const models = loci.structure.models;
-            const sourceIndexMap = new Map<string, { modelLabel: string, modelIndex: number, xs: UniqueArray<number, number> }>();
-            const el = StructureElement.create(), p = StructureProperties.atom.sourceIndex;
-            for (const e of loci.elements) {
-                const { indices } = e;
-                const { elements } = e.unit;
-
-                const key = models.length === 1
-                    ? e.unit.conformation.operator.name
-                    : `${e.unit.conformation.operator.name} ${e.unit.model.label} ${e.unit.model.modelNum}`;
-
-                let sourceIndices: UniqueArray<number, number>;
-                if (sourceIndexMap.has(key)) sourceIndices = sourceIndexMap.get(key)!.xs;
-                else {
-                    sourceIndices = UniqueArray.create<number, number>();
-                    sourceIndexMap.set(key, { modelLabel: e.unit.model.label, modelIndex: e.unit.model.modelNum, xs: sourceIndices });
-                }
-
-                el.unit = e.unit;
-                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
-                    el.element = elements[OrderedSet.getAt(indices, i)];
-                    const idx = p(el);
-                    UniqueArray.add(sourceIndices, idx, idx);
-                }
-            }
-
-            const opData: OpData[] = [];
-            const keys = sourceIndexMap.keys();
-            while (true) {
-                const k = keys.next();
-                if (k.done) break;
-                const e = sourceIndexMap.get(k.value)!;
-                opData.push(getOpData(k.value, e.xs.array, models.length > 1, e.modelLabel, e.modelIndex));
-            }
-
-            const opGroups = new Map<string, OpData>();
-            for (let i = 0, il = opData.length; i < il; ++i) {
-                const d = opData[i]
-                const hash = hash2(hashFnv32a(d.atom.ranges), hashFnv32a(d.atom.set))
-                const key = `${hash}|${d.entity ? (d.entity.modelLabel + d.entity.modelIndex) : ''}`
-                if (opGroups.has(key)) {
-                    opGroups.get(key)!.chain.opName.push(...d.chain.opName)
-                } else {
-                    opGroups.set(key, d)
-                }
-            }
-
-            const opQueries: Expression[] = [];
-            opGroups.forEach(d => {
-                const { ranges, set } = d.atom
-                const { opName } = d.chain
-
-                const opProp = MS.struct.atomProperty.core.operatorName()
-                const siProp = MS.struct.atomProperty.core.sourceIndex();
-                const tests: Expression[] = [];
-
-                // TODO: add set.ofRanges constructor to MolQL???
-                if (set.length > 0) {
-                    tests[tests.length] = MS.core.set.has([MS.core.type.set(set), siProp]);
-                }
-                for (let rI = 0, _rI = ranges.length / 2; rI < _rI; rI++) {
-                    tests[tests.length] = MS.core.rel.inRange([siProp, ranges[2 * rI], ranges[2 * rI + 1]]);
-                }
-
-                if (d.entity) {
-                    const { modelLabel, modelIndex } = d.entity
-                    opQueries.push(MS.struct.generator.atomGroups({
-                        'atom-test': tests.length > 1 ? MS.core.logic.or(tests) : tests[0],
-                        'chain-test': opName.length > 1
-                            ? MS.core.set.has([MS.core.type.set(opName), opProp])
-                            : MS.core.rel.eq([opProp, opName[0]]),
-                        'entity-test': MS.core.logic.and([
-                            MS.core.rel.eq([MS.struct.atomProperty.core.modelLabel(), modelLabel]),
-                            MS.core.rel.eq([MS.struct.atomProperty.core.modelIndex(), modelIndex]),
-                        ])
-                    }))
-                } else {
-                    opQueries.push(MS.struct.generator.atomGroups({
-                        'atom-test': tests.length > 1 ? MS.core.logic.or(tests) : tests[0],
-                        'chain-test': opName.length > 1
-                            ? MS.core.set.has([MS.core.type.set(opName), opProp])
-                            : MS.core.rel.eq([opProp, opName[0]])
-                    }))
-                }
-            })
-
-            return MS.struct.modifier.union([
-                opQueries.length === 1
-                    ? opQueries[0]
-                    // Need to union before merge for fast performance
-                    : MS.struct.combinator.merge(opQueries.map(q => MS.struct.modifier.union([ q ])))
-            ]);
-        }
-
-        type OpData = {
-            atom: { set: number[], ranges: number[] },
-            chain: { opName: string[] },
-            entity?: { modelLabel: string, modelIndex: number }
-        }
-
-        function getOpData(opName: string, xs: number[], multimodel: boolean, modelLabel: string, modelIndex: number): OpData {
-            sortArray(xs);
-
-            const ranges: number[] = [];
-            const set: number[] = [];
-
-            let i = 0, len = xs.length;
-            while (i < len) {
-                const start = i;
-                i++;
-                while (i < len && xs[i - 1] + 1 === xs[i]) i++;
-                const end = i;
-                // TODO: is this a good value?
-                if (end - start > 12) {
-                    ranges[ranges.length] = xs[start];
-                    ranges[ranges.length] = xs[end - 1];
-                } else {
-                    for (let j = start; j < end; j++) {
-                        set[set.length] = xs[j];
-                    }
-                }
-            }
-
-            return multimodel
-                ? {
-                    atom: { set, ranges },
-                    chain: { opName: [ opName ] },
-                    entity: { modelLabel, modelIndex }
-                }
-                : {
-                    atom: { set, ranges },
-                    chain: { opName: [ opName ] },
-                }
-        }
-    }
-
-    //
-
-    interface QueryElement {
-        /**
-         * Array (sorted by first element in sub-array) of
-         * arrays of `Unit.id`s that share the same `Unit.invariantId`
-         */
-        groupedUnits: SortedArray<number>[],
-        set: SortedArray<UnitIndex>
-        ranges: SortedRanges<UnitIndex>
-    }
-    export interface Query {
-        /** Hash of the structure to which the query can be applied */
-        readonly hash: number
-        /** Query elements */
-        readonly elements: ReadonlyArray<Readonly<QueryElement>>
-    }
-
-    export namespace Query {
-        export const Empty: Query = { hash: -1, elements: [] }
-
-        export function fromLoci(loci: StructureElement.Loci): Query {
-            const _elements: {
-                unit: Unit
-                set: SortedArray<UnitIndex>
-                ranges: SortedRanges<UnitIndex>
-            }[] = []
-            for (const e of loci.elements) {
-                const { unit, indices } = e
-                if (OrderedSet.size(indices) === 0) continue
-
-                const ranges: UnitIndex[] = [];
-                const set: UnitIndex[] = [];
-
-                if (OrderedSet.isInterval(indices)) {
-                    if (OrderedSet.size(indices) === 1) {
-                        set.push(Interval.min(indices))
-                    } else {
-                        ranges.push(Interval.min(indices), Interval.max(indices))
-                    }
-                } else {
-                    let i = 0, len = indices.length;
-                    while (i < len) {
-                        const start = i;
-                        i++;
-                        while (i < len && indices[i - 1] + 1 === indices[i]) i++;
-                        const end = i;
-                        if (end - start > 2) {
-                            ranges.push(indices[start], indices[end - 1])
-                        } else {
-                            for (let j = start; j < end; j++) {
-                                set[set.length] = indices[j]
-                            }
-                        }
-                    }
-                }
-
-                _elements.push({
-                    unit,
-                    set: SortedArray.ofSortedArray(set),
-                    ranges: SortedRanges.ofSortedRanges(ranges)
-                })
-            }
-
-            const elementGroups = new Map<number, {
-                groupedUnits: Map<number, number[]>
-                set: SortedArray<UnitIndex>
-                ranges: SortedRanges<UnitIndex>
-            }>();
-            for (let i = 0, il = _elements.length; i < il; ++i) {
-                const e = _elements[i]
-                const key = hash2(hashFnv32a(e.ranges), hashFnv32a(e.set))
-                if (elementGroups.has(key)) {
-                    const { groupedUnits } = elementGroups.get(key)!
-                    if (groupedUnits.has(e.unit.invariantId)) {
-                        groupedUnits.get(e.unit.invariantId)!.push(e.unit.id)
-                    } else {
-                        groupedUnits.set(e.unit.invariantId, [e.unit.id])
-                    }
-                } else {
-                    const groupedUnits = new Map<number, number[]>()
-                    groupedUnits.set(e.unit.invariantId, [e.unit.id])
-                    elementGroups.set(key, { groupedUnits, set: e.set, ranges: e.ranges })
-                }
-            }
-
-            const elements: QueryElement[] = []
-            elementGroups.forEach(e => {
-                const groupedUnits: SortedArray<number>[] = []
-                e.groupedUnits.forEach(g => groupedUnits.push(SortedArray.ofUnsortedArray(g)))
-                groupedUnits.sort((a, b) => a[0] - b[0]) // sort by first unit id of each group
-                elements.push({ groupedUnits, set: e.set, ranges: e.ranges })
-            })
-
-            return { hash: loci.structure.root.hashCode, elements }
-        }
-
-        function getUnitsFromIds(unitIds: ArrayLike<number>, structure: Structure) {
-            const units: Unit[] = []
-            for (let i = 0, il = unitIds.length; i < il; ++i) {
-                const unitId = unitIds[i]
-                if (structure.unitMap.has(unitId)) units.push(structure.unitMap.get(unitId))
-            }
-            return units
-        }
-
-        export function toLoci(query: Query, parent: Structure): Loci {
-            if (query.hash !== -1 && query.hash !== parent.root.hashCode) {
-                new Error('Query not compatible with given structure')
-            }
-            const elements: Loci['elements'][0][] = []
-            for (const e of query.elements) {
-                for (const g of e.groupedUnits) {
-                    const units = getUnitsFromIds(g, parent)
-                    if (units.length === 0) continue
-
-                    let indices: OrderedSet<UnitIndex>
-                    if (e.ranges.length === 0) {
-                        indices = e.set
-                    } else if (e.set.length === 0) {
-                        if (e.ranges.length === 2) {
-                            indices = Interval.ofRange(e.ranges[0], e.ranges[1])
-                        } else {
-                            const _indices = new Int32Array(SortedRanges.size(e.ranges))
-                            SortedRanges.forEach(e.ranges, (v, i) => _indices[i] = v)
-                            indices = SortedArray.ofSortedArray(_indices)
-                        }
-                    } else {
-                        const rangesSize = SortedRanges.size(e.ranges)
-                        const _indices = new Int32Array(e.set.length + rangesSize)
-                        SortedRanges.forEach(e.ranges, (v, i) => _indices[i] = v)
-                        _indices.set(e.set, rangesSize)
-                        indices = SortedArray.ofUnsortedArray(_indices) // requires sort
-                    }
-
-                    for (const unit of units) {
-                        elements.push({ unit, indices })
-                    }
-                }
-            }
-            return Loci(parent, elements)
-        }
-
-        export function toStructure(query: Query, parent: Structure): Structure {
-            if (query.hash !== -1 && query.hash !== parent.root.hashCode) {
-                new Error('Query not compatible with given structure')
-            }
-            const units: Unit[] = []
-            for (const e of query.elements) {
-                for (const g of e.groupedUnits) {
-                    const _units = getUnitsFromIds(g, parent)
-                    if (_units.length === 0) continue
-
-                    const unit = _units[0] // the elements are grouped by unit.invariantId
-                    const rangesSize = SortedRanges.size(e.ranges)
-                    const _indices = new Int32Array(e.set.length + rangesSize)
-                    let indices: SortedArray<ElementIndex>
-                    if (e.ranges.length === 0) {
-                        for (let i = 0, il = e.set.length; i < il; ++i) {
-                            _indices[i] = unit.elements[e.set[i]]
-                        }
-                        indices = SortedArray.ofSortedArray(_indices)
-                    } else if (e.set.length === 0) {
-                        SortedRanges.forEach(e.ranges, (v, i) => _indices[i] = unit.elements[v])
-                        indices = SortedArray.ofSortedArray(_indices)
-                    } else {
-                        const rangesSize = SortedRanges.size(e.ranges)
-                        SortedRanges.forEach(e.ranges, (v, i) => _indices[i] = unit.elements[v])
-                        SortedRanges.forEach(e.set, (v, i) => _indices[i + rangesSize] = unit.elements[v])
-                        indices = SortedArray.ofUnsortedArray(_indices) // requires sort
-                    }
-
-                    for (const unit of _units) {
-                        units.push(unit.getChild(indices))
-                    }
-                }
-            }
-            return Structure.create(units, { parent })
-        }
-
-        export function areEqual(a: Query, b: Query) {
-            if (a.elements.length !== b.elements.length) return false
-            for (let i = 0, il = a.elements.length; i < il; ++i) {
-                const elementA = a.elements[i], elementB = b.elements[i]
-                if (elementA.groupedUnits.length !== elementB.groupedUnits.length) return false
-                for (let j = 0, jl = elementB.groupedUnits.length; j < jl; ++j) {
-                    if (!SortedArray.areEqual(elementA.groupedUnits[j], elementB.groupedUnits[j])) return false
-                }
-                if (!SortedArray.areEqual(elementA.set, elementB.set)) return false
-                if (!SortedRanges.areEqual(elementA.ranges, elementB.ranges)) return false
-            }
-            return true
-        }
-    }
-
-    //
-
-    export interface Stats {
-        elementCount: number
-        residueCount: number
-        unitCount: number
-
-        firstElementLoc: StructureElement
-        firstResidueLoc: StructureElement
-        firstUnitLoc: StructureElement
-    }
-
-    export namespace Stats {
-        export function create(): Stats {
-            return {
-                elementCount: 0,
-                residueCount: 0,
-                unitCount: 0,
-
-                firstElementLoc: StructureElement.create(),
-                firstResidueLoc: StructureElement.create(),
-                firstUnitLoc: StructureElement.create(),
-            }
-        }
-
-        function handleElement(stats: Stats, element: StructureElement.Loci['elements'][0]) {
-            const { indices, unit } = element
-            const { elements } = unit
-            const size = OrderedSet.size(indices)
-            if (size === 1) {
-                stats.elementCount += 1
-                if (stats.elementCount === 1) {
-                    StructureElement.set(stats.firstElementLoc, unit, elements[OrderedSet.start(indices)])
-                }
-            } else if (size === elements.length) {
-                stats.unitCount += 1
-                if (stats.unitCount === 1) {
-                    StructureElement.set(stats.firstUnitLoc, unit, elements[OrderedSet.start(indices)])
-                }
-            } else {
-                if (Unit.isAtomic(unit)) {
-                    const { index, offsets } = unit.model.atomicHierarchy.residueAtomSegments
-                    let i = 0
-                    while (i < size) {
-                        const eI = elements[OrderedSet.getAt(indices, i)]
-                        const rI = index[eI]
-                        if (offsets[rI] !== eI) {
-                            // partial residue, start missing
-                            ++i
-                            stats.elementCount += 1
-                            while (i < size && index[elements[OrderedSet.getAt(indices, i)]] === rI) {
-                                ++i
-                                stats.elementCount += 1
-                            }
-                        } else {
-                            ++i
-                            while (i < size && index[elements[OrderedSet.getAt(indices, i)]] === rI) {
-                                ++i
-                            }
-
-                            if (offsets[rI + 1] - 1 === elements[OrderedSet.getAt(indices, i - 1)]) {
-                                // full residue
-                                stats.residueCount += 1
-                                if (stats.residueCount === 1) {
-                                    StructureElement.set(stats.firstResidueLoc, unit, elements[OrderedSet.start(indices)])
-                                }
-                            } else {
-                                // partial residue, end missing
-                                stats.elementCount += offsets[rI + 1] - 1 - elements[OrderedSet.getAt(indices, i - 1)]
-                            }
-                        }
-                    }
-                } else {
-                    // TODO
-                    stats.elementCount += size
-                    if (stats.elementCount === 1) {
-                        StructureElement.set(stats.firstElementLoc, unit, elements[OrderedSet.start(indices)])
-                    }
-                }
-            }
-        }
-
-        export function ofLoci(loci: StructureElement.Loci) {
-            const stats = create()
-            if (loci.elements.length > 0) {
-                for (const e of loci.elements) handleElement(stats, e)
-            }
-            return stats
-        }
-
-        export function add(out: Stats, a: Stats, b: Stats) {
-            if (a.elementCount === 1 && b.elementCount === 0) {
-                StructureElement.copy(out.firstElementLoc, a.firstElementLoc)
-            } else if (a.elementCount === 0 && b.elementCount === 1) {
-                StructureElement.copy(out.firstElementLoc, b.firstElementLoc)
-            }
-
-            if (a.residueCount === 1 && b.residueCount === 0) {
-                StructureElement.copy(out.firstResidueLoc, a.firstResidueLoc)
-            } else if (a.residueCount === 0 && b.residueCount === 1) {
-                StructureElement.copy(out.firstResidueLoc, b.firstResidueLoc)
-            }
-
-            if (a.unitCount === 1 && b.unitCount === 0) {
-                StructureElement.copy(out.firstUnitLoc, a.firstUnitLoc)
-            } else if (a.unitCount === 0 && b.unitCount === 1) {
-                StructureElement.copy(out.firstUnitLoc, b.firstUnitLoc)
-            }
-
-            out.elementCount = a.elementCount + b.elementCount
-            out.residueCount = a.residueCount + b.residueCount
-            out.unitCount = a.unitCount + b.unitCount
-            return out
-        }
-    }
-}
+import * as StructureElement from './element/index';
 
 export default StructureElement

+ 54 - 0
src/mol-model/structure/structure/element/element.ts

@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { SortedArray } from '../../../../mol-data/int';
+import { ElementIndex, ResidueIndex, ChainIndex } from '../../model';
+import Unit from '../unit';
+import { Location } from './location';
+import StructureProperties from '../properties';
+
+// TODO: when nominal types are available, make this indexed by UnitIndex
+export type Set = SortedArray<ElementIndex>
+
+/** Index into Unit.elements */
+export type UnitIndex = { readonly '@type': 'structure-element-index' } & number
+
+export interface Property<T> { (location: Location): T }
+export interface Predicate extends Property<boolean> { }
+
+export function property<T>(p: Property<T>) { return p; }
+
+function _wrongUnitKind(kind: string) { throw new Error(`Property only available for ${kind} models.`); }
+export function atomicProperty<T>(p: (location: Location<Unit.Atomic>) => T) {
+    return property(l => Unit.isAtomic(l.unit) ? p(l as Location<Unit.Atomic>) : _wrongUnitKind('atomic'));
+}
+
+export function coarseProperty<T>(p: (location: Location<Unit.Spheres | Unit.Gaussians>) => T) {
+    return property(l => Unit.isCoarse(l.unit) ? p(l as Location<Unit.Spheres | Unit.Gaussians>) : _wrongUnitKind('coarse'));
+}
+
+export function residueIndex(e: Location) {
+    if (Unit.isAtomic(e.unit)) {
+        return e.unit.residueIndex[e.element];
+    } else {
+        // TODO: throw error instead?
+        return -1 as ResidueIndex;
+    }
+}
+
+export function chainIndex(e: Location) {
+    if (Unit.isAtomic(e.unit)) {
+        return e.unit.chainIndex[e.element];
+    } else {
+        // TODO: throw error instead?
+        return -1 as ChainIndex;
+    }
+}
+
+export function entityIndex(l: Location) {
+    return StructureProperties.entity.key(l)
+}

+ 12 - 0
src/mol-model/structure/structure/element/index.ts

@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export * from './location'
+export * from './loci'
+export * from './query'
+export * from './stats'
+export * from './element'

+ 40 - 0
src/mol-model/structure/structure/element/location.ts

@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ElementIndex } from '../../model';
+import Unit from '../unit';
+
+export { Location }
+
+interface Location<U = Unit> {
+    readonly kind: 'element-location',
+    unit: U,
+    /** Index into element (atomic/coarse) properties of unit.model */
+    element: ElementIndex
+}
+
+namespace Location {
+    export function create(unit?: Unit, element?: ElementIndex): Location {
+        return { kind: 'element-location', unit: unit!, element: element || (0 as ElementIndex) };
+    }
+
+    export function set(a: Location, unit?: Unit, element?: ElementIndex): Location {
+        if (unit) a.unit = unit
+        if (element !== undefined) a.element = element
+        return a;
+    }
+
+    export function copy(out: Location, a: Location): Location {
+        out.unit = a.unit
+        out.element = a.element
+        return out
+    }
+
+    export function is(x: any): x is Location {
+        return !!x && x.kind === 'element-location';
+    }
+}

+ 422 - 0
src/mol-model/structure/structure/element/loci.ts

@@ -0,0 +1,422 @@
+/**
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { UniqueArray } from '../../../../mol-data/generic';
+import { OrderedSet, SortedArray, Interval } from '../../../../mol-data/int';
+import { BoundaryHelper } from '../../../../mol-math/geometry/boundary-helper';
+import { Vec3 } from '../../../../mol-math/linear-algebra';
+import { MolScriptBuilder as MS } from '../../../../mol-script/language/builder';
+import Structure from '../structure';
+import Unit from '../unit';
+import { Boundary } from '../util/boundary';
+import { sortArray, hashFnv32a, hash2 } from '../../../../mol-data/util';
+import Expression from '../../../../mol-script/language/expression';
+import { ElementIndex } from '../../model';
+import { UnitIndex } from './element';
+
+/** Represents multiple element index locations */
+export interface Loci {
+    readonly kind: 'element-loci',
+    readonly structure: Structure,
+    /** Access i-th element as unit.elements[indices[i]] */
+    readonly elements: ReadonlyArray<{
+        unit: Unit,
+        /**
+         * Indices into the unit.elements array.
+         * Can use OrderedSet.forEach to iterate (or OrderedSet.size + OrderedSet.getAt)
+         */
+        indices: OrderedSet<UnitIndex>
+    }>
+}
+
+export function Loci(structure: Structure, elements: ArrayLike<{ unit: Unit, indices: OrderedSet<UnitIndex> }>): Loci {
+    return { kind: 'element-loci', structure, elements: elements as Loci['elements'] };
+}
+
+export namespace Loci {
+    export function is(x: any): x is Loci {
+        return !!x && x.kind === 'element-loci';
+    }
+
+    export function areEqual(a: Loci, b: Loci) {
+        if (a.elements.length !== b.elements.length) return false
+        for (let i = 0, il = a.elements.length; i < il; ++i) {
+            const elementA = a.elements[i]
+            const elementB = b.elements[i]
+            if (elementA.unit.id !== elementB.unit.id) return false
+            if (!OrderedSet.areEqual(elementA.indices, elementB.indices)) return false
+        }
+        return true
+    }
+
+    export function size(loci: Loci) {
+        let s = 0;
+        for (const u of loci.elements) s += OrderedSet.size(u.indices);
+        return s;
+    }
+
+    export function all(structure: Structure): Loci {
+        return Loci(structure, structure.units.map(unit => ({
+            unit,
+            indices: OrderedSet.ofBounds<UnitIndex>(0 as UnitIndex, unit.elements.length as UnitIndex)
+        })));
+    }
+
+    export function none(structure: Structure): Loci {
+        return Loci(structure, []);
+    }
+
+    export function remap(loci: Loci, structure: Structure): Loci {
+        if (structure === loci.structure) return loci
+
+        const elements: Loci['elements'][0][] = [];
+        loci.elements.forEach(e => {
+            if (!structure.unitMap.has(e.unit.id)) return
+            const unit = structure.unitMap.get(e.unit.id)
+
+            if (SortedArray.areEqual(e.unit.elements, unit.elements)) {
+                elements.push({ unit, indices: e.indices })
+            } else {
+                const _indices: UnitIndex[] = []
+                const end = unit.elements.length
+                let start = 0
+                for (let i = 0; i < OrderedSet.size(e.indices); ++i) {
+                    const v = OrderedSet.getAt(e.indices, i)
+                    const eI = e.unit.elements[v]
+                    const uI = SortedArray.indexOfInRange(unit.elements, eI, start, end) as UnitIndex | -1
+                    if (uI !== -1) {
+                        _indices.push(uI)
+                        start = uI
+                    }
+                }
+
+                let indices: OrderedSet<UnitIndex>
+                if (_indices.length > 12 && _indices[_indices.length - 1] - _indices[0] === _indices.length - 1) {
+                    indices = Interval.ofRange(_indices[0], _indices[_indices.length - 1])
+                } else {
+                    indices = SortedArray.ofSortedArray(_indices)
+                }
+
+                elements.push({ unit, indices })
+            }
+        });
+
+        return Loci(structure, elements);
+    }
+
+    /** Create union of `xs` and `ys` */
+    export function union(xs: Loci, ys: Loci): Loci {
+        if (xs.elements.length > ys.elements.length) return union(ys, xs);
+        if (xs.elements.length === 0) return ys;
+
+        const map = new Map<number, OrderedSet<UnitIndex>>();
+
+        for (const e of xs.elements) map.set(e.unit.id, e.indices);
+
+        const elements: Loci['elements'][0][] = [];
+        for (const e of ys.elements) {
+            if (map.has(e.unit.id)) {
+                elements[elements.length] = { unit: e.unit, indices: OrderedSet.union(map.get(e.unit.id)!, e.indices) };
+                map.delete(e.unit.id)
+            } else {
+                elements[elements.length] = e;
+            }
+        }
+
+        map.forEach((indices, id) => {
+            elements[elements.length] = { unit: xs.structure.unitMap.get(id)!, indices };
+        });
+
+        return Loci(xs.structure, elements);
+    }
+
+    /** Subtract `ys` from `xs` */
+    export function subtract(xs: Loci, ys: Loci): Loci {
+        const map = new Map<number, OrderedSet<UnitIndex>>();
+        for (const e of ys.elements) map.set(e.unit.id, e.indices);
+
+        const elements: Loci['elements'][0][] = [];
+        for (const e of xs.elements) {
+            if (map.has(e.unit.id)) {
+                const indices = OrderedSet.subtract(e.indices, map.get(e.unit.id)!);
+                if (OrderedSet.size(indices) === 0) continue;
+                elements[elements.length] = { unit: e.unit, indices };
+            } else {
+                elements[elements.length] = e;
+            }
+        }
+
+        return Loci(xs.structure, elements);
+    }
+
+    export function areIntersecting(xs: Loci, ys: Loci): boolean {
+        if (xs.elements.length > ys.elements.length) return areIntersecting(ys, xs);
+        if (xs.elements.length === 0) return ys.elements.length === 0;
+
+        const map = new Map<number, OrderedSet<UnitIndex>>();
+
+        for (const e of xs.elements) map.set(e.unit.id, e.indices);
+        for (const e of ys.elements) {
+            if (!map.has(e.unit.id)) continue;
+            if (OrderedSet.areIntersecting(map.get(e.unit.id)!, e.indices)) return true;
+        }
+
+        return false;
+    }
+
+    export function extendToWholeResidues(loci: Loci): Loci {
+        const elements: Loci['elements'][0][] = [];
+
+        for (const lociElement of loci.elements) {
+            if (lociElement.unit.kind === Unit.Kind.Atomic) {
+                const unitElements = lociElement.unit.elements;
+                const h = lociElement.unit.model.atomicHierarchy;
+
+                const { index: residueIndex, offsets: residueOffsets } = h.residueAtomSegments;
+
+                const newIndices: UnitIndex[] = [];
+                const indices = lociElement.indices, len = OrderedSet.size(indices);
+                let i = 0;
+                while (i < len) {
+                    const rI = residueIndex[unitElements[OrderedSet.getAt(indices, i)]];
+                    i++;
+                    while (i < len && residueIndex[unitElements[OrderedSet.getAt(indices, i)]] === rI) {
+                        i++;
+                    }
+
+                    for (let j = residueOffsets[rI], _j = residueOffsets[rI + 1]; j < _j; j++) {
+                        const idx = OrderedSet.indexOf(unitElements, j);
+                        if (idx >= 0) newIndices[newIndices.length] = idx as UnitIndex;
+                    }
+                }
+
+                elements[elements.length] = { unit: lociElement.unit, indices: SortedArray.ofSortedArray(newIndices) };
+            } else {
+                // coarse elements are already by-residue
+                elements[elements.length] = lociElement;
+            }
+        }
+
+        return Loci(loci.structure, elements);
+    }
+
+    function getChainSegments(unit: Unit) {
+        switch (unit.kind) {
+            case Unit.Kind.Atomic: return unit.model.atomicHierarchy.chainAtomSegments
+            case Unit.Kind.Spheres: return unit.model.coarseHierarchy.spheres.chainElementSegments
+            case Unit.Kind.Gaussians: return unit.model.coarseHierarchy.gaussians.chainElementSegments
+        }
+    }
+
+    export function extendToWholeChains(loci: Loci): Loci {
+        const elements: Loci['elements'][0][] = [];
+
+        for (const lociElement of loci.elements) {
+            const _newIndices: UnitIndex[] = [];
+            const unitElements = lociElement.unit.elements;
+
+            const { index: chainIndex, offsets: chainOffsets } = getChainSegments(lociElement.unit)
+
+            const indices = lociElement.indices, len = OrderedSet.size(indices);
+            let i = 0;
+            while (i < len) {
+                const cI = chainIndex[unitElements[OrderedSet.getAt(indices, i)]];
+                i++;
+                while (i < len && chainIndex[unitElements[OrderedSet.getAt(indices, i)]] === cI) {
+                    i++;
+                }
+
+                for (let j = chainOffsets[cI], _j = chainOffsets[cI + 1]; j < _j; j++) {
+                    const idx = OrderedSet.indexOf(unitElements, j);
+                    if (idx >= 0) _newIndices[_newIndices.length] = idx as UnitIndex;
+                }
+            }
+
+            let newIndices: OrderedSet<UnitIndex>
+            if (_newIndices.length > 12 && _newIndices[_newIndices.length - 1] - _newIndices[0] === _newIndices.length - 1) {
+                newIndices = Interval.ofRange(_newIndices[0], _newIndices[_newIndices.length - 1])
+            } else {
+                newIndices = SortedArray.ofSortedArray(_newIndices)
+            }
+
+            elements[elements.length] = { unit: lociElement.unit, indices: newIndices };
+        }
+
+        return Loci(loci.structure, elements);
+    }
+
+    const boundaryHelper = new BoundaryHelper(), tempPos = Vec3.zero();
+    export function getBoundary(loci: Loci): Boundary {
+        boundaryHelper.reset(0);
+
+        for (const e of loci.elements) {
+            const { indices } = e;
+            const pos = e.unit.conformation.position, r = e.unit.conformation.r;
+            const { elements } = e.unit;
+            for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
+                const eI = elements[OrderedSet.getAt(indices, i)];
+                pos(eI, tempPos);
+                boundaryHelper.boundaryStep(tempPos, r(eI));
+            }
+        }
+        boundaryHelper.finishBoundaryStep();
+        for (const e of loci.elements) {
+            const { indices } = e;
+            const pos = e.unit.conformation.position, r = e.unit.conformation.r;
+            const { elements } = e.unit;
+            for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
+                const eI = elements[OrderedSet.getAt(indices, i)];
+                pos(eI, tempPos);
+                boundaryHelper.extendStep(tempPos, r(eI));
+            }
+        }
+
+        return { box: boundaryHelper.getBox(), sphere: boundaryHelper.getSphere() };
+    }
+
+    function sourceIndex(unit: Unit, element: ElementIndex) {
+        return Unit.isAtomic(unit)
+            ? unit.model.atomicHierarchy.atoms.sourceIndex.value(element)
+            // TODO: when implemented, this should map to the source index.
+            : element
+    }
+
+    export function toScriptExpression(loci: Loci) {
+        if (loci.elements.length === 0) return MS.struct.generator.empty();
+
+        const models = loci.structure.models;
+        const sourceIndexMap = new Map<string, { modelLabel: string, modelIndex: number, xs: UniqueArray<number, number> }>();
+        for (const e of loci.elements) {
+            const { indices } = e;
+            const { elements } = e.unit;
+
+            const key = models.length === 1
+                ? e.unit.conformation.operator.name
+                : `${e.unit.conformation.operator.name} ${e.unit.model.label} ${e.unit.model.modelNum}`;
+
+            let sourceIndices: UniqueArray<number, number>;
+            if (sourceIndexMap.has(key)) sourceIndices = sourceIndexMap.get(key)!.xs;
+            else {
+                sourceIndices = UniqueArray.create<number, number>();
+                sourceIndexMap.set(key, { modelLabel: e.unit.model.label, modelIndex: e.unit.model.modelNum, xs: sourceIndices });
+            }
+
+            for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
+                const idx = sourceIndex(e.unit, elements[OrderedSet.getAt(indices, i)]);
+                UniqueArray.add(sourceIndices, idx, idx);
+            }
+        }
+
+        const opData: OpData[] = [];
+        const keys = sourceIndexMap.keys();
+        while (true) {
+            const k = keys.next();
+            if (k.done) break;
+            const e = sourceIndexMap.get(k.value)!;
+            opData.push(getOpData(k.value, e.xs.array, models.length > 1, e.modelLabel, e.modelIndex));
+        }
+
+        const opGroups = new Map<string, OpData>();
+        for (let i = 0, il = opData.length; i < il; ++i) {
+            const d = opData[i]
+            const hash = hash2(hashFnv32a(d.atom.ranges), hashFnv32a(d.atom.set))
+            const key = `${hash}|${d.entity ? (d.entity.modelLabel + d.entity.modelIndex) : ''}`
+            if (opGroups.has(key)) {
+                opGroups.get(key)!.chain.opName.push(...d.chain.opName)
+            } else {
+                opGroups.set(key, d)
+            }
+        }
+
+        const opQueries: Expression[] = [];
+        opGroups.forEach(d => {
+            const { ranges, set } = d.atom
+            const { opName } = d.chain
+
+            const opProp = MS.struct.atomProperty.core.operatorName()
+            const siProp = MS.struct.atomProperty.core.sourceIndex();
+            const tests: Expression[] = [];
+
+            // TODO: add set.ofRanges constructor to MolQL???
+            if (set.length > 0) {
+                tests[tests.length] = MS.core.set.has([MS.core.type.set(set), siProp]);
+            }
+            for (let rI = 0, _rI = ranges.length / 2; rI < _rI; rI++) {
+                tests[tests.length] = MS.core.rel.inRange([siProp, ranges[2 * rI], ranges[2 * rI + 1]]);
+            }
+
+            if (d.entity) {
+                const { modelLabel, modelIndex } = d.entity
+                opQueries.push(MS.struct.generator.atomGroups({
+                    'atom-test': tests.length > 1 ? MS.core.logic.or(tests) : tests[0],
+                    'chain-test': opName.length > 1
+                        ? MS.core.set.has([MS.core.type.set(opName), opProp])
+                        : MS.core.rel.eq([opProp, opName[0]]),
+                    'entity-test': MS.core.logic.and([
+                        MS.core.rel.eq([MS.struct.atomProperty.core.modelLabel(), modelLabel]),
+                        MS.core.rel.eq([MS.struct.atomProperty.core.modelIndex(), modelIndex]),
+                    ])
+                }))
+            } else {
+                opQueries.push(MS.struct.generator.atomGroups({
+                    'atom-test': tests.length > 1 ? MS.core.logic.or(tests) : tests[0],
+                    'chain-test': opName.length > 1
+                        ? MS.core.set.has([MS.core.type.set(opName), opProp])
+                        : MS.core.rel.eq([opProp, opName[0]])
+                }))
+            }
+        })
+
+        return MS.struct.modifier.union([
+            opQueries.length === 1
+                ? opQueries[0]
+                // Need to union before merge for fast performance
+                : MS.struct.combinator.merge(opQueries.map(q => MS.struct.modifier.union([ q ])))
+        ]);
+    }
+
+    type OpData = {
+        atom: { set: number[], ranges: number[] },
+        chain: { opName: string[] },
+        entity?: { modelLabel: string, modelIndex: number }
+    }
+
+    function getOpData(opName: string, xs: number[], multimodel: boolean, modelLabel: string, modelIndex: number): OpData {
+        sortArray(xs);
+
+        const ranges: number[] = [];
+        const set: number[] = [];
+
+        let i = 0, len = xs.length;
+        while (i < len) {
+            const start = i;
+            i++;
+            while (i < len && xs[i - 1] + 1 === xs[i]) i++;
+            const end = i;
+            // TODO: is this a good value?
+            if (end - start > 12) {
+                ranges[ranges.length] = xs[start];
+                ranges[ranges.length] = xs[end - 1];
+            } else {
+                for (let j = start; j < end; j++) {
+                    set[set.length] = xs[j];
+                }
+            }
+        }
+
+        return multimodel
+            ? {
+                atom: { set, ranges },
+                chain: { opName: [ opName ] },
+                entity: { modelLabel, modelIndex }
+            }
+            : {
+                atom: { set, ranges },
+                chain: { opName: [ opName ] },
+            }
+    }
+}

+ 208 - 0
src/mol-model/structure/structure/element/query.ts

@@ -0,0 +1,208 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { OrderedSet, SortedArray, Interval } from '../../../../mol-data/int';
+import { ElementIndex } from '../../model';
+import Structure from '../structure';
+import Unit from '../unit';
+import { hashFnv32a, hash2 } from '../../../../mol-data/util';
+import SortedRanges from '../../../../mol-data/int/sorted-ranges';
+import { UnitIndex } from './element';
+import { Loci } from './loci';
+
+interface QueryElement {
+    /**
+     * Array (sorted by first element in sub-array) of
+     * arrays of `Unit.id`s that share the same `Unit.invariantId`
+     */
+    groupedUnits: SortedArray<number>[],
+    set: SortedArray<UnitIndex>
+    ranges: SortedRanges<UnitIndex>
+}
+
+export interface Query {
+    /** Hash of the structure to which the query can be applied */
+    readonly hash: number
+    /** Query elements */
+    readonly elements: ReadonlyArray<Readonly<QueryElement>>
+}
+
+export namespace Query {
+    export const Empty: Query = { hash: -1, elements: [] }
+
+    export function fromLoci(loci: Loci): Query {
+        const _elements: {
+            unit: Unit
+            set: SortedArray<UnitIndex>
+            ranges: SortedRanges<UnitIndex>
+        }[] = []
+        for (const e of loci.elements) {
+            const { unit, indices } = e
+            if (OrderedSet.size(indices) === 0) continue
+
+            const ranges: UnitIndex[] = [];
+            const set: UnitIndex[] = [];
+
+            if (OrderedSet.isInterval(indices)) {
+                if (OrderedSet.size(indices) === 1) {
+                    set.push(Interval.min(indices))
+                } else {
+                    ranges.push(Interval.min(indices), Interval.max(indices))
+                }
+            } else {
+                let i = 0, len = indices.length;
+                while (i < len) {
+                    const start = i;
+                    i++;
+                    while (i < len && indices[i - 1] + 1 === indices[i]) i++;
+                    const end = i;
+                    if (end - start > 2) {
+                        ranges.push(indices[start], indices[end - 1])
+                    } else {
+                        for (let j = start; j < end; j++) {
+                            set[set.length] = indices[j]
+                        }
+                    }
+                }
+            }
+
+            _elements.push({
+                unit,
+                set: SortedArray.ofSortedArray(set),
+                ranges: SortedRanges.ofSortedRanges(ranges)
+            })
+        }
+
+        const elementGroups = new Map<number, {
+            groupedUnits: Map<number, number[]>
+            set: SortedArray<UnitIndex>
+            ranges: SortedRanges<UnitIndex>
+        }>();
+        for (let i = 0, il = _elements.length; i < il; ++i) {
+            const e = _elements[i]
+            const key = hash2(hashFnv32a(e.ranges), hashFnv32a(e.set))
+            if (elementGroups.has(key)) {
+                const { groupedUnits } = elementGroups.get(key)!
+                if (groupedUnits.has(e.unit.invariantId)) {
+                    groupedUnits.get(e.unit.invariantId)!.push(e.unit.id)
+                } else {
+                    groupedUnits.set(e.unit.invariantId, [e.unit.id])
+                }
+            } else {
+                const groupedUnits = new Map<number, number[]>()
+                groupedUnits.set(e.unit.invariantId, [e.unit.id])
+                elementGroups.set(key, { groupedUnits, set: e.set, ranges: e.ranges })
+            }
+        }
+
+        const elements: QueryElement[] = []
+        elementGroups.forEach(e => {
+            const groupedUnits: SortedArray<number>[] = []
+            e.groupedUnits.forEach(g => groupedUnits.push(SortedArray.ofUnsortedArray(g)))
+            groupedUnits.sort((a, b) => a[0] - b[0]) // sort by first unit id of each group
+            elements.push({ groupedUnits, set: e.set, ranges: e.ranges })
+        })
+
+        return { hash: loci.structure.root.hashCode, elements }
+    }
+
+    function getUnitsFromIds(unitIds: ArrayLike<number>, structure: Structure) {
+        const units: Unit[] = []
+        for (let i = 0, il = unitIds.length; i < il; ++i) {
+            const unitId = unitIds[i]
+            if (structure.unitMap.has(unitId)) units.push(structure.unitMap.get(unitId))
+        }
+        return units
+    }
+
+    export function toLoci(query: Query, parent: Structure): Loci {
+        if (query.hash !== -1 && query.hash !== parent.root.hashCode) {
+            new Error('Query not compatible with given structure')
+        }
+        const elements: Loci['elements'][0][] = []
+        for (const e of query.elements) {
+            for (const g of e.groupedUnits) {
+                const units = getUnitsFromIds(g, parent)
+                if (units.length === 0) continue
+
+                let indices: OrderedSet<UnitIndex>
+                if (e.ranges.length === 0) {
+                    indices = e.set
+                } else if (e.set.length === 0) {
+                    if (e.ranges.length === 2) {
+                        indices = Interval.ofRange(e.ranges[0], e.ranges[1])
+                    } else {
+                        const _indices = new Int32Array(SortedRanges.size(e.ranges))
+                        SortedRanges.forEach(e.ranges, (v, i) => _indices[i] = v)
+                        indices = SortedArray.ofSortedArray(_indices)
+                    }
+                } else {
+                    const rangesSize = SortedRanges.size(e.ranges)
+                    const _indices = new Int32Array(e.set.length + rangesSize)
+                    SortedRanges.forEach(e.ranges, (v, i) => _indices[i] = v)
+                    _indices.set(e.set, rangesSize)
+                    indices = SortedArray.ofUnsortedArray(_indices) // requires sort
+                }
+
+                for (const unit of units) {
+                    elements.push({ unit, indices })
+                }
+            }
+        }
+        return Loci(parent, elements)
+    }
+
+    export function toStructure(query: Query, parent: Structure): Structure {
+        if (query.hash !== -1 && query.hash !== parent.root.hashCode) {
+            new Error('Query not compatible with given structure')
+        }
+        const units: Unit[] = []
+        for (const e of query.elements) {
+            for (const g of e.groupedUnits) {
+                const _units = getUnitsFromIds(g, parent)
+                if (_units.length === 0) continue
+
+                const unit = _units[0] // the elements are grouped by unit.invariantId
+                const rangesSize = SortedRanges.size(e.ranges)
+                const _indices = new Int32Array(e.set.length + rangesSize)
+                let indices: SortedArray<ElementIndex>
+                if (e.ranges.length === 0) {
+                    for (let i = 0, il = e.set.length; i < il; ++i) {
+                        _indices[i] = unit.elements[e.set[i]]
+                    }
+                    indices = SortedArray.ofSortedArray(_indices)
+                } else if (e.set.length === 0) {
+                    SortedRanges.forEach(e.ranges, (v, i) => _indices[i] = unit.elements[v])
+                    indices = SortedArray.ofSortedArray(_indices)
+                } else {
+                    const rangesSize = SortedRanges.size(e.ranges)
+                    SortedRanges.forEach(e.ranges, (v, i) => _indices[i] = unit.elements[v])
+                    SortedRanges.forEach(e.set, (v, i) => _indices[i + rangesSize] = unit.elements[v])
+                    indices = SortedArray.ofUnsortedArray(_indices) // requires sort
+                }
+
+                for (const unit of _units) {
+                    units.push(unit.getChild(indices))
+                }
+            }
+        }
+        return Structure.create(units, { parent })
+    }
+
+    export function areEqual(a: Query, b: Query) {
+        if (a.elements.length !== b.elements.length) return false
+        for (let i = 0, il = a.elements.length; i < il; ++i) {
+            const elementA = a.elements[i], elementB = b.elements[i]
+            if (elementA.groupedUnits.length !== elementB.groupedUnits.length) return false
+            for (let j = 0, jl = elementB.groupedUnits.length; j < jl; ++j) {
+                if (!SortedArray.areEqual(elementA.groupedUnits[j], elementB.groupedUnits[j])) return false
+            }
+            if (!SortedArray.areEqual(elementA.set, elementB.set)) return false
+            if (!SortedRanges.areEqual(elementA.ranges, elementB.ranges)) return false
+        }
+        return true
+    }
+}

+ 125 - 0
src/mol-model/structure/structure/element/stats.ts

@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { OrderedSet } from '../../../../mol-data/int';
+import Unit from '../unit';
+import { Loci } from './loci';
+import { Location } from './location';
+
+
+export interface Stats {
+    elementCount: number
+    residueCount: number
+    unitCount: number
+
+    firstElementLoc: Location
+    firstResidueLoc: Location
+    firstUnitLoc: Location
+}
+
+export namespace Stats {
+    export function create(): Stats {
+        return {
+            elementCount: 0,
+            residueCount: 0,
+            unitCount: 0,
+
+            firstElementLoc: Location.create(),
+            firstResidueLoc: Location.create(),
+            firstUnitLoc: Location.create(),
+        }
+    }
+
+    function handleElement(stats: Stats, element: Loci['elements'][0]) {
+        const { indices, unit } = element
+        const { elements } = unit
+        const size = OrderedSet.size(indices)
+        if (size === 1) {
+            stats.elementCount += 1
+            if (stats.elementCount === 1) {
+                Location.set(stats.firstElementLoc, unit, elements[OrderedSet.start(indices)])
+            }
+        } else if (size === elements.length) {
+            stats.unitCount += 1
+            if (stats.unitCount === 1) {
+                Location.set(stats.firstUnitLoc, unit, elements[OrderedSet.start(indices)])
+            }
+        } else {
+            if (Unit.isAtomic(unit)) {
+                const { index, offsets } = unit.model.atomicHierarchy.residueAtomSegments
+                let i = 0
+                while (i < size) {
+                    const eI = elements[OrderedSet.getAt(indices, i)]
+                    const rI = index[eI]
+                    if (offsets[rI] !== eI) {
+                        // partial residue, start missing
+                        ++i
+                        stats.elementCount += 1
+                        while (i < size && index[elements[OrderedSet.getAt(indices, i)]] === rI) {
+                            ++i
+                            stats.elementCount += 1
+                        }
+                    } else {
+                        ++i
+                        while (i < size && index[elements[OrderedSet.getAt(indices, i)]] === rI) {
+                            ++i
+                        }
+
+                        if (offsets[rI + 1] - 1 === elements[OrderedSet.getAt(indices, i - 1)]) {
+                            // full residue
+                            stats.residueCount += 1
+                            if (stats.residueCount === 1) {
+                                Location.set(stats.firstResidueLoc, unit, elements[OrderedSet.start(indices)])
+                            }
+                        } else {
+                            // partial residue, end missing
+                            stats.elementCount += offsets[rI + 1] - 1 - elements[OrderedSet.getAt(indices, i - 1)]
+                        }
+                    }
+                }
+            } else {
+                // TODO
+                stats.elementCount += size
+                if (stats.elementCount === 1) {
+                    Location.set(stats.firstElementLoc, unit, elements[OrderedSet.start(indices)])
+                }
+            }
+        }
+    }
+
+    export function ofLoci(loci: Loci) {
+        const stats = create()
+        if (loci.elements.length > 0) {
+            for (const e of loci.elements) handleElement(stats, e)
+        }
+        return stats
+    }
+
+    export function add(out: Stats, a: Stats, b: Stats) {
+        if (a.elementCount === 1 && b.elementCount === 0) {
+            Location.copy(out.firstElementLoc, a.firstElementLoc)
+        } else if (a.elementCount === 0 && b.elementCount === 1) {
+            Location.copy(out.firstElementLoc, b.firstElementLoc)
+        }
+
+        if (a.residueCount === 1 && b.residueCount === 0) {
+            Location.copy(out.firstResidueLoc, a.firstResidueLoc)
+        } else if (a.residueCount === 0 && b.residueCount === 1) {
+            Location.copy(out.firstResidueLoc, b.firstResidueLoc)
+        }
+
+        if (a.unitCount === 1 && b.unitCount === 0) {
+            Location.copy(out.firstUnitLoc, a.firstUnitLoc)
+        } else if (a.unitCount === 0 && b.unitCount === 1) {
+            Location.copy(out.firstUnitLoc, b.firstUnitLoc)
+        }
+
+        out.elementCount = a.elementCount + b.elementCount
+        out.residueCount = a.residueCount + b.residueCount
+        out.unitCount = a.unitCount + b.unitCount
+        return out
+    }
+}

+ 2 - 2
src/mol-model/structure/structure/properties.ts

@@ -53,7 +53,7 @@ const atom = {
     vdw_radius: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : VdwRadius(l.unit.model.atomicHierarchy.atoms.type_symbol.value(l.element))),
 }
 
-function compId(l: StructureElement) {
+function compId(l: StructureElement.Location) {
     return !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.label_comp_id.value(l.unit.residueIndex[l.element])
 }
 
@@ -106,7 +106,7 @@ const coarse = {
     gaussian_covariance_matrix: p(l => !Unit.isGaussians(l.unit) ? notCoarse('gaussians') : l.unit.coarseConformation.covariance_matrix[l.element])
 }
 
-function eK(l: StructureElement) {
+function eK(l: StructureElement.Location) {
     switch (l.unit.kind) {
         case Unit.Kind.Atomic:
             return l.unit.model.atomicHierarchy.index.getEntityFromChain(l.unit.chainIndex[l.element])

+ 7 - 7
src/mol-model/structure/structure/structure.ts

@@ -145,7 +145,7 @@ class Structure {
     }
 
     /** Returns a new element location iterator */
-    elementLocations(): Iterator<StructureElement> {
+    elementLocations(): Iterator<StructureElement.Location> {
         return new Structure.ElementLocationIterator(this);
     }
 
@@ -256,7 +256,7 @@ class Structure {
         return this._props.representativeModel
     }
 
-    hasElement(e: StructureElement) {
+    hasElement(e: StructureElement.Location) {
         if (!this.unitMap.has(e.unit.id)) return false;
         return SortedArray.has(this.unitMap.get(e.unit.id).elements, e.element);
     }
@@ -325,7 +325,7 @@ function getModels(s: Structure) {
 function getUniqueResidueNames(s: Structure) {
     const prop = StructureProperties.residue.label_comp_id;
     const names = new Set<string>();
-    const loc = StructureElement.create();
+    const loc = StructureElement.Location.create();
     for (const unit of s.units) {
         // TODO: support coarse unit?
         if (!Unit.isAtomic(unit)) continue;
@@ -342,7 +342,7 @@ function getUniqueResidueNames(s: Structure) {
 
 function getEntityIndices(structure: Structure): ReadonlyArray<EntityIndex> {
     const { units } = structure;
-    const l = StructureElement.create();
+    const l = StructureElement.Location.create();
     const keys = UniqueArray.create<number, EntityIndex>();
 
     for (const unit of units) {
@@ -697,15 +697,15 @@ namespace Structure {
         return a.root === b.root
     }
 
-    export class ElementLocationIterator implements Iterator<StructureElement> {
-        private current = StructureElement.create();
+    export class ElementLocationIterator implements Iterator<StructureElement.Location> {
+        private current = StructureElement.Location.create();
         private unitIndex = 0;
         private elements: StructureElement.Set;
         private maxIdx = 0;
         private idx = -1;
 
         hasNext: boolean;
-        move(): StructureElement {
+        move(): StructureElement.Location {
             this.advance();
             this.current.element = this.elements[this.idx];
             return this.current;

+ 1 - 1
src/mol-model/structure/structure/util/lookup3d.ts

@@ -89,7 +89,7 @@ export class StructureLookup3D {
         const closeUnits = this.unitLookup.find(x, y, z, radius);
         if (closeUnits.count === 0) return;
 
-        const se = StructureElement.create();
+        const se = StructureElement.Location.create();
         const queryRadius = pivotR + maxRadius + radius;
 
         for (let t = 0, _t = closeUnits.count; t < _t; t++) {

+ 1 - 1
src/mol-model/structure/structure/util/subset-builder.ts

@@ -101,7 +101,7 @@ export class StructureSubsetBuilder {
         return this._getStructure(true);
     }
 
-    setSingletonLocation(location: StructureElement) {
+    setSingletonLocation(location: StructureElement.Location) {
         const id = this.ids[0];
         location.unit = this.parent.unitMap.get(id);
         location.element = this.unitMap.get(id)[0];

+ 1 - 1
src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts

@@ -71,7 +71,7 @@ function labelPDBeValidation(loci: Loci): string | undefined {
             const u = e.unit;
             if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0;
 
-            const se = StructureElement.create(u, u.elements[OrderedSet.getAt(e.indices, 0)]);
+            const se = StructureElement.Location.create(u, u.elements[OrderedSet.getAt(e.indices, 0)]);
             const issues = StructureQualityReport.getIssues(se);
             if (issues.length === 0) return 'PDBe Validation: No Issues';
             return `PDBe Validation: ${issues.join(', ')}`;

+ 1 - 1
src/mol-plugin/behavior/dynamic/labels.ts

@@ -131,7 +131,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
         }
 
         private updateLabels(p: SceneLabelsProps) {
-            const l = StructureElement.create()
+            const l = StructureElement.Location.create()
 
             const { texts, positions, sizes, depths } = this.data
             texts.length = 0

+ 1 - 1
src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts

@@ -132,7 +132,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
             }
 
             // TODO: support link loci as well?
-            if (!StructureElement.isLoci(current.loci)) return;
+            if (!StructureElement.Loci.is(current.loci)) return;
 
             const parent = this.plugin.helpers.substructureParent.get(current.loci.structure);
             if (!parent || !parent.obj) return;

+ 1 - 1
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -207,7 +207,7 @@ export namespace VolumeStreaming {
 
                 // TODO: support link loci as well?
                 // Perhaps structure loci too?
-                if (!StructureElement.isLoci(current.loci)) return;
+                if (!StructureElement.Loci.is(current.loci)) return;
 
                 const parent = this.plugin.helpers.substructureParent.get(current.loci.structure);
                 if (!parent) return;

+ 9 - 9
src/mol-plugin/ui/sequence.tsx

@@ -19,7 +19,7 @@ import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { HeteroSequenceWrapper } from './sequence/hetero';
 import { State, StateSelection } from '../../mol-state';
 
-function opKey(l: StructureElement) {
+function opKey(l: StructureElement.Location) {
     const ids = SP.unit.pdbx_struct_oper_list_ids(l)
     const ncs = SP.unit.struct_ncs_oper_id(l)
     const hkl = SP.unit.hkl(l)
@@ -29,9 +29,9 @@ function opKey(l: StructureElement) {
 
 function getSequenceWrapper(state: SequenceViewState, structureSelection: StructureElementSelectionManager): SequenceWrapper.Any | undefined {
     const { structure, entityId, invariantUnitId, operatorKey } = state
-    const l = StructureElement.create()
+    const l = StructureElement.Location.create()
     for (const unit of structure.units) {
-        StructureElement.set(l, unit, unit.elements[0])
+        StructureElement.Location.set(l, unit, unit.elements[0])
         if (SP.entity.id(l) !== entityId) continue
         if (unit.invariantId !== invariantUnitId) continue
         if (opKey(l) !== operatorKey) continue
@@ -45,11 +45,11 @@ function getSequenceWrapper(state: SequenceViewState, structureSelection: Struct
 
 function getEntityOptions(structure: Structure) {
     const options: [string, string][] = []
-    const l = StructureElement.create()
+    const l = StructureElement.Location.create()
     const seen = new Set<string>()
 
     for (const unit of structure.units) {
-        StructureElement.set(l, unit, unit.elements[0])
+        StructureElement.Location.set(l, unit, unit.elements[0])
         const id = SP.entity.id(l)
         if (seen.has(id)) continue
 
@@ -64,12 +64,12 @@ function getEntityOptions(structure: Structure) {
 
 function getUnitOptions(structure: Structure, entityId: string) {
     const options: [number, string][] = []
-    const l = StructureElement.create()
+    const l = StructureElement.Location.create()
     const seen = new Set<number>()
     const water = new Map<string, number>()
 
     for (const unit of structure.units) {
-        StructureElement.set(l, unit, unit.elements[0])
+        StructureElement.Location.set(l, unit, unit.elements[0])
         if (SP.entity.id(l) !== entityId) continue
 
         const id = unit.invariantId
@@ -100,11 +100,11 @@ function getUnitOptions(structure: Structure, entityId: string) {
 
 function getOperatorOptions(structure: Structure, entityId: string, invariantUnitId: number) {
     const options: [string, string][] = []
-    const l = StructureElement.create()
+    const l = StructureElement.Location.create()
     const seen = new Set<string>()
 
     for (const unit of structure.units) {
-        StructureElement.set(l, unit, unit.elements[0])
+        StructureElement.Location.set(l, unit, unit.elements[0])
         if (SP.entity.id(l) !== entityId) continue
         if (unit.invariantId !== invariantUnitId) continue
 

+ 1 - 1
src/mol-plugin/ui/sequence/hetero.ts

@@ -25,7 +25,7 @@ export class HeteroSequenceWrapper extends SequenceWrapper<StructureUnit> {
     eachResidue(loci: Loci, apply: (set: OrderedSet) => boolean) {
         let changed = false
         const { structure, unit } = this.data
-        if (StructureElement.isLoci(loci)) {
+        if (StructureElement.Loci.is(loci)) {
             if (!Structure.areRootsEquivalent(loci.structure, structure)) return false
             loci = StructureElement.Loci.remap(loci, structure)
 

+ 2 - 2
src/mol-plugin/ui/sequence/polymer.ts

@@ -36,7 +36,7 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> {
     eachResidue(loci: Loci, apply: (set: OrderedSet) => boolean) {
         let changed = false
         const { structure, unit } = this.data
-        if (StructureElement.isLoci(loci)) {
+        if (StructureElement.Loci.is(loci)) {
             if (!Structure.areRootsEquivalent(loci.structure, structure)) return false
             loci = StructureElement.Loci.remap(loci, structure)
 
@@ -62,7 +62,7 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> {
     }
 
     constructor(data: StructureUnit) {
-        const l = StructureElement.create(data.unit, data.unit.elements[0])
+        const l = StructureElement.Location.create(data.unit, data.unit.elements[0])
         const sequence = data.unit.model.sequence.byEntityKey[SP.entity.key(l)].sequence
         const length = sequence.sequence.length
         const markerArray = new Uint8Array(length)

+ 6 - 6
src/mol-plugin/util/interactivity.ts

@@ -54,9 +54,9 @@ namespace Interactivity {
 
     const Granularity = {
         'element': (loci: ModelLoci) => loci,
-        'residue': (loci: ModelLoci) => SE.isLoci(loci) ? SE.Loci.extendToWholeResidues(loci) : loci,
-        'chain': (loci: ModelLoci) => SE.isLoci(loci) ? SE.Loci.extendToWholeChains(loci) : loci,
-        'structure': (loci: ModelLoci) => SE.isLoci(loci) ? Structure.Loci(loci.structure) : loci
+        'residue': (loci: ModelLoci) => SE.Loci.is(loci) ? SE.Loci.extendToWholeResidues(loci) : loci,
+        'chain': (loci: ModelLoci) => SE.Loci.is(loci) ? SE.Loci.extendToWholeChains(loci) : loci,
+        'structure': (loci: ModelLoci) => SE.Loci.is(loci) ? Structure.Loci(loci.structure) : loci
     }
     type Granularity = keyof typeof Granularity
     const GranularityOptions = Object.keys(Granularity).map(n => [n, capitalize(n)]) as [Granularity, string][]
@@ -103,7 +103,7 @@ namespace Interactivity {
                 // convert to StructureElement.Loci of root structure
                 loci = Structure.toStructureElementLoci(Structure.Loci(loci.structure.root))
             }
-            if (StructureElement.isLoci(loci) && loci.structure.parent) {
+            if (StructureElement.Loci.is(loci) && loci.structure.parent) {
                 // ensure the root structure is used
                 loci = StructureElement.Loci.remap(loci, loci.structure.parent)
             }
@@ -129,7 +129,7 @@ namespace Interactivity {
             const { current, modifiers } = e
 
             const normalized: Loci<ModelLoci> = this.normalizedLoci(current)
-            if (StructureElement.isLoci(normalized.loci)) {
+            if (StructureElement.Loci.is(normalized.loci)) {
                 let loci: StructureElement.Loci = normalized.loci;
                 if (modifiers && modifiers.shift) {
                     loci = this.sel.tryGetRange(loci) || loci;
@@ -175,7 +175,7 @@ namespace Interactivity {
                     const sels = this.sel.clear();
                     for (const s of sels) this.mark({ loci: s }, MarkerAction.Deselect);
                 }
-            } else if (StructureElement.isLoci(normalized.loci)) {
+            } else if (StructureElement.Loci.is(normalized.loci)) {
                 if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
                     // select only the current element on Ctrl + Right-Click
                     const old = this.sel.get(normalized.loci.structure);

+ 6 - 6
src/mol-plugin/util/structure-element-selection.ts

@@ -52,7 +52,7 @@ class StructureElementSelectionManager {
     }
 
     add(loci: Loci): Loci {
-        if (StructureElement.isLoci(loci)) {
+        if (StructureElement.Loci.is(loci)) {
             const entry = this.getEntry(loci.structure);
             if (entry) {
                 entry.selection = StructureElement.Loci.union(entry.selection, loci);
@@ -64,7 +64,7 @@ class StructureElementSelectionManager {
     }
 
     remove(loci: Loci): Loci {
-        if (StructureElement.isLoci(loci)) {
+        if (StructureElement.Loci.is(loci)) {
             const entry = this.getEntry(loci.structure);
             if (entry) {
                 entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
@@ -76,7 +76,7 @@ class StructureElementSelectionManager {
     }
 
     set(loci: Loci): Loci {
-        if (StructureElement.isLoci(loci)) {
+        if (StructureElement.Loci.is(loci)) {
             const entry = this.getEntry(loci.structure);
             if (entry) {
                 entry.selection = loci;
@@ -108,7 +108,7 @@ class StructureElementSelectionManager {
     }
 
     has(loci: Loci) {
-        if (StructureElement.isLoci(loci)) {
+        if (StructureElement.Loci.is(loci)) {
             const entry = this.getEntry(loci.structure);
             if (entry) {
                 return StructureElement.Loci.areIntersecting(loci, entry.selection);
@@ -118,7 +118,7 @@ class StructureElementSelectionManager {
     }
 
     tryGetRange(loci: Loci): StructureElement.Loci | undefined {
-        if (!StructureElement.isLoci(loci)) return;
+        if (!StructureElement.Loci.is(loci)) return;
         if (loci.elements.length !== 1) return;
         const entry = this.getEntry(loci.structure);
         if (!entry) return;
@@ -139,7 +139,7 @@ class StructureElementSelectionManager {
     private prevHighlight: StructureElement.Loci | undefined = void 0;
 
     accumulateInteractiveHighlight(loci: Loci) {
-        if (StructureElement.isLoci(loci)) {
+        if (StructureElement.Loci.is(loci)) {
             if (this.prevHighlight) {
                 this.prevHighlight = StructureElement.Loci.union(this.prevHighlight, loci);
             } else {

+ 1 - 1
src/mol-plugin/util/structure-labels.ts

@@ -75,7 +75,7 @@ function getLabelDataStatic(structure: Structure, text: string, size: number, po
 function getLabelDataComputed(structure: Structure, level: 'elements' | 'residues'): LabelsData {
     const data: LabelsData = { texts: [], positions: [], sizes: [], depths: [] };
 
-    const l = StructureElement.create();
+    const l = StructureElement.Location.create();
     const { units } = structure;
 
     const { label_atom_id } = StructureProperties.atom;

+ 2 - 2
src/mol-repr/structure/complex-representation.ts

@@ -56,9 +56,9 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
 
     function mark(loci: Loci, action: MarkerAction) {
         if (!_structure) return false
-        if (!StructureElement.isLoci(loci) && !Link.isLoci(loci)) return false
+        if (!StructureElement.Loci.is(loci) && !Link.isLoci(loci)) return false
         if (!Structure.areRootsEquivalent(loci.structure, _structure)) return false
-        if (StructureElement.isLoci(loci)) {
+        if (StructureElement.Loci.is(loci)) {
             loci = StructureElement.Loci.remap(loci, _structure)
         } else if (Link.isLoci(loci)) {
             loci = Link.remapLoci(loci, _structure)

+ 2 - 2
src/mol-repr/structure/units-representation.ts

@@ -165,9 +165,9 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R
     function mark(loci: Loci, action: MarkerAction) {
         let changed = false
         if (!_structure) return false
-        if (!StructureElement.isLoci(loci) && !Link.isLoci(loci)) return false
+        if (!StructureElement.Loci.is(loci) && !Link.isLoci(loci)) return false
         if (!Structure.areRootsEquivalent(loci.structure, _structure)) return false
-        if (StructureElement.isLoci(loci)) {
+        if (StructureElement.Loci.is(loci)) {
             loci = StructureElement.Loci.remap(loci, _structure)
             if (loci.elements.length === 0) return false
         } else if (Link.isLoci(loci)) {

+ 2 - 2
src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts

@@ -25,7 +25,7 @@ function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure: Struc
     const { links, elements } = structure.carbohydrates
     const { linkSizeFactor } = props
 
-    const location = StructureElement.create()
+    const location = StructureElement.Location.create()
 
     const builderProps = {
         linkCount: links.length,
@@ -128,7 +128,7 @@ function eachCarbohydrateLink(loci: Loci, structure: Structure, apply: (interval
                 if (apply(Interval.ofSingleton(idx))) changed = true
             }
         }
-    } else if (StructureElement.isLoci(loci)) {
+    } else if (StructureElement.Loci.is(loci)) {
         if (!Structure.areEquivalent(loci.structure, structure)) return false
         // TODO mark link only when both of the link elements are in a StructureElement.Loci
         const { getElementIndex, getLinkIndices, elements } = structure.carbohydrates

+ 3 - 3
src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts

@@ -51,7 +51,7 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure,
 
     const carbohydrates = structure.carbohydrates
     const n = carbohydrates.elements.length
-    const l = StructureElement.create()
+    const l = StructureElement.Location.create()
 
     for (let i = 0; i < n; ++i) {
         const c = carbohydrates.elements[i];
@@ -168,7 +168,7 @@ function CarbohydrateElementIterator(structure: Structure): LocationIterator {
     const carbElements = structure.carbohydrates.elements
     const groupCount = carbElements.length * 2
     const instanceCount = 1
-    const location = StructureElement.create()
+    const location = StructureElement.Location.create()
     function getLocation (groupIndex: number, instanceIndex: number) {
         const carb = carbElements[Math.floor(groupIndex / 2)]
         location.unit = carb.unit
@@ -195,7 +195,7 @@ function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: num
 function eachCarbohydrate(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     const { getElementIndex, getAnomericCarbons } = structure.carbohydrates
     let changed = false
-    if (!StructureElement.isLoci(loci)) return false
+    if (!StructureElement.Loci.is(loci)) return false
     if (!Structure.areEquivalent(loci.structure, structure)) return false
     for (const e of loci.elements) {
         // TODO make more efficient by handling/grouping `e.indices` by residue index

+ 2 - 2
src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts

@@ -25,7 +25,7 @@ function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, structur
     const { terminalLinks, elements } = structure.carbohydrates
     const { linkSizeFactor } = props
 
-    const location = StructureElement.create()
+    const location = StructureElement.Location.create()
 
     const builderProps = {
         linkCount: terminalLinks.length,
@@ -141,7 +141,7 @@ function eachTerminalLink(loci: Loci, structure: Structure, apply: (interval: In
                 if (apply(Interval.ofSingleton(idx))) changed = true
             }
         }
-    } else if (StructureElement.isLoci(loci)) {
+    } else if (StructureElement.Loci.is(loci)) {
         if (!Structure.areEquivalent(loci.structure, structure)) return false
         // TODO mark link only when both of the link elements are in a StructureElement.Loci
         const { getElementIndex, getTerminalLinkIndices, elements } = structure.carbohydrates

+ 1 - 1
src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts

@@ -26,7 +26,7 @@ function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Str
     if (!crossLinks.count) return Mesh.createEmpty(mesh)
     const { sizeFactor } = props
 
-    const location = StructureElement.create()
+    const location = StructureElement.Location.create()
 
     const builderProps = {
         linkCount: crossLinks.count,

+ 2 - 2
src/mol-repr/structure/visual/inter-unit-link-cylinder.ts

@@ -26,7 +26,7 @@ function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: Structur
 
     if (!bondCount) return Mesh.createEmpty(mesh)
 
-    const location = StructureElement.create()
+    const location = StructureElement.Location.create()
 
     const builderProps = {
         linkCount: bondCount,
@@ -112,7 +112,7 @@ function eachLink(loci: Loci, structure: Structure, apply: (interval: Interval)
                 if (apply(Interval.ofSingleton(idx))) changed = true
             }
         }
-    } else if (StructureElement.isLoci(loci)) {
+    } else if (StructureElement.Loci.is(loci)) {
         if (!Structure.areEquivalent(loci.structure, structure)) return false
         // TODO mark link only when both of the link elements are in a StructureElement.Loci
         for (const e of loci.elements) {

+ 2 - 2
src/mol-repr/structure/visual/intra-unit-link-cylinder.ts

@@ -23,7 +23,7 @@ import { isHydrogen } from './util/common';
 function createIntraUnitLinkCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitLinkParams>, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
 
-    const location = StructureElement.create(unit)
+    const location = StructureElement.Location.create(unit)
 
     const elements = unit.elements;
     const links = unit.links
@@ -140,7 +140,7 @@ function eachLink(loci: Loci, structureGroup: StructureGroup, apply: (interval:
                 }
             }
         }
-    } else if (StructureElement.isLoci(loci)) {
+    } else if (StructureElement.Loci.is(loci)) {
         const { structure, group } = structureGroup
         if (!Structure.areEquivalent(loci.structure, structure)) return false
         const unit = group.units[0]

+ 2 - 2
src/mol-repr/structure/visual/util/common.ts

@@ -127,7 +127,7 @@ export function getUnitConformationAndRadius(unit: Unit, ignoreHydrogens = false
         id
     }
 
-    const l = StructureElement.create(unit)
+    const l = StructureElement.Location.create(unit)
     const sizeTheme = PhysicalSizeTheme({}, {})
     const radius = (index: number) => {
         l.element = index as ElementIndex
@@ -138,7 +138,7 @@ export function getUnitConformationAndRadius(unit: Unit, ignoreHydrogens = false
 }
 
 export function getStructureConformationAndRadius(structure: Structure, ignoreHydrogens = false) {
-    const l = StructureElement.create()
+    const l = StructureElement.Location.create()
     const sizeTheme = PhysicalSizeTheme({}, {})
 
     let xs: ArrayLike<number>

+ 5 - 5
src/mol-repr/structure/visual/util/element.ts

@@ -37,7 +37,7 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
 
     const v = Vec3.zero()
     const pos = unit.conformation.invariantPosition
-    const l = StructureElement.create()
+    const l = StructureElement.Location.create()
     l.unit = unit
 
     for (let i = 0; i < elementCount; i++) {
@@ -78,7 +78,7 @@ export function createElementSphereImpostor(ctx: VisualContext, unit: Unit, stru
 
 export function eachElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
-    if (!StructureElement.isLoci(loci)) return false
+    if (!StructureElement.Loci.is(loci)) return false
     const { structure, group } = structureGroup
     if (!Structure.areEquivalent(loci.structure, structure)) return false
     const elementCount = group.elements.length
@@ -115,7 +115,7 @@ export function getElementLoci(pickingId: PickingId, structureGroup: StructureGr
 
 export function eachSerialElement(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     let changed = false
-    if (!StructureElement.isLoci(loci)) return false
+    if (!StructureElement.Loci.is(loci)) return false
     if (!Structure.areEquivalent(loci.structure, structure)) return false
     const { unitElementCount } = structure.serialMapping
     for (const e of loci.elements) {
@@ -155,7 +155,7 @@ export namespace ElementIterator {
     export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
         const groupCount = group.elements.length
         const instanceCount = group.units.length
-        const location = StructureElement.create()
+        const location = StructureElement.Location.create()
         const getLocation = (groupIndex: number, instanceIndex: number) => {
             const unit = group.units[instanceIndex]
             location.unit = unit
@@ -170,7 +170,7 @@ export namespace ElementIterator {
         const groupCount = elementCount
         const instanceCount = 1
         const { unitIndices, elementIndices } = structure.serialMapping
-        const location = StructureElement.create()
+        const location = StructureElement.Location.create()
         const getLocation = (groupIndex: number) => {
             location.unit = units[unitIndices[groupIndex]]
             location.element = elementIndices[groupIndex]

+ 1 - 1
src/mol-repr/structure/visual/util/link.ts

@@ -130,7 +130,7 @@ export namespace LinkIterator {
         const unit = group.units[0]
         const groupCount = Unit.isAtomic(unit) ? unit.links.edgeCount * 2 : 0
         const instanceCount = group.units.length
-        const location = StructureElement.create()
+        const location = StructureElement.Location.create()
         const getLocation = (groupIndex: number, instanceIndex: number) => {
             const unit = group.units[instanceIndex]
             location.unit = unit

+ 2 - 2
src/mol-repr/structure/visual/util/nucleotide.ts

@@ -18,7 +18,7 @@ export namespace NucleotideLocationIterator {
         const nucleotideElementIndices = Unit.isAtomic(u) ? u.nucleotideElements : []
         const groupCount = nucleotideElementIndices.length
         const instanceCount = group.units.length
-        const location = StructureElement.create()
+        const location = StructureElement.Location.create()
         const getLocation = (groupIndex: number, instanceIndex: number) => {
             const unit = group.units[instanceIndex]
             location.unit = unit
@@ -47,7 +47,7 @@ export function getNucleotideElementLoci(pickingId: PickingId, structureGroup: S
  */
 export function eachNucleotideElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
-    if (!StructureElement.isLoci(loci)) return false
+    if (!StructureElement.Loci.is(loci)) return false
     const { structure, group } = structureGroup
     if (!Structure.areEquivalent(loci.structure, structure)) return false
     const unit = group.units[0]

+ 4 - 4
src/mol-repr/structure/visual/util/polymer.ts

@@ -39,7 +39,7 @@ export namespace PolymerLocationIterator {
         const polymerElements = group.units[0].polymerElements
         const groupCount = polymerElements.length
         const instanceCount = group.units.length
-        const location = StructureElement.create()
+        const location = StructureElement.Location.create()
         const getLocation = (groupIndex: number, instanceIndex: number) => {
             const unit = group.units[instanceIndex]
             location.unit = unit
@@ -55,7 +55,7 @@ export namespace PolymerGapLocationIterator {
         const gapElements = group.units[0].gapElements
         const groupCount = gapElements.length
         const instanceCount = group.units.length
-        const location = StructureElement.create()
+        const location = StructureElement.Location.create()
         const getLocation = (groupIndex: number, instanceIndex: number) => {
             const unit = group.units[instanceIndex]
             location.unit = unit
@@ -93,7 +93,7 @@ export function getPolymerElementLoci(pickingId: PickingId, structureGroup: Stru
  */
 export function eachPolymerElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
-    if (!StructureElement.isLoci(loci)) return false
+    if (!StructureElement.Loci.is(loci)) return false
     const { structure, group } = structureGroup
     if (!Structure.areEquivalent(loci.structure, structure)) return false
     const { polymerElements, model, elements } = group.units[0]
@@ -175,7 +175,7 @@ export function eachPolymerGapElement(loci: Loci, structureGroup: StructureGroup
                 }
             }
         }
-    } else if (StructureElement.isLoci(loci)) {
+    } else if (StructureElement.Loci.is(loci)) {
         const { structure, group } = structureGroup
         if (!Structure.areRootsEquivalent(loci.structure, structure)) return false
         loci = StructureElement.Loci.remap(loci, structure)

+ 4 - 4
src/mol-repr/structure/visual/util/polymer/backbone-iterator.ts

@@ -21,14 +21,14 @@ export function PolymerBackboneIterator(unit: Unit): Iterator<PolymerBackbonePai
 }
 
 interface PolymerBackbonePair {
-    centerA: StructureElement
-    centerB: StructureElement
+    centerA: StructureElement.Location
+    centerB: StructureElement.Location
 }
 
 function createPolymerBackbonePair (unit: Unit) {
     return {
-        centerA: StructureElement.create(unit),
-        centerB: StructureElement.create(unit),
+        centerA: StructureElement.Location.create(unit),
+        centerB: StructureElement.Location.create(unit),
     }
 }
 

+ 4 - 4
src/mol-repr/structure/visual/util/polymer/gap-iterator.ts

@@ -20,14 +20,14 @@ export function PolymerGapIterator(unit: Unit): Iterator<PolymerGapPair> {
 }
 
 interface PolymerGapPair {
-    centerA: StructureElement
-    centerB: StructureElement
+    centerA: StructureElement.Location
+    centerB: StructureElement.Location
 }
 
 function createPolymerGapPair (unit: Unit) {
     return {
-        centerA: StructureElement.create(unit),
-        centerB: StructureElement.create(unit),
+        centerA: StructureElement.Location.create(unit),
+        centerB: StructureElement.Location.create(unit),
     }
 }
 

+ 6 - 6
src/mol-repr/structure/visual/util/polymer/trace-iterator.ts

@@ -30,9 +30,9 @@ export function PolymerTraceIterator(unit: Unit, structure: Structure): Iterator
 }
 
 interface PolymerTraceElement {
-    center: StructureElement
-    centerPrev: StructureElement
-    centerNext: StructureElement
+    center: StructureElement.Location
+    centerPrev: StructureElement.Location
+    centerNext: StructureElement.Location
     first: boolean, last: boolean
     secStrucFirst: boolean, secStrucLast: boolean
     secStrucType: SecondaryStructureType
@@ -48,9 +48,9 @@ const SecStrucTypeNA = SecondaryStructureType.create(SecondaryStructureType.Flag
 
 function createPolymerTraceElement (unit: Unit): PolymerTraceElement {
     return {
-        center: StructureElement.create(unit),
-        centerPrev: StructureElement.create(unit),
-        centerNext: StructureElement.create(unit),
+        center: StructureElement.Location.create(unit),
+        centerPrev: StructureElement.Location.create(unit),
+        centerNext: StructureElement.Location.create(unit),
         first: false, last: false,
         secStrucFirst: false, secStrucLast: false,
         secStrucType: SecStrucTypeNA,

+ 1 - 1
src/mol-script/runtime/query/table.ts

@@ -322,7 +322,7 @@ const symbols = [
     D(MolScript.structureQuery.linkProperty.flags, (ctx, xs) => ctx.atomicLink.type),
 ];
 
-function atomProp(p: (e: StructureElement) => any): (ctx: QueryContext, _: any) => any {
+function atomProp(p: (e: StructureElement.Location) => any): (ctx: QueryContext, _: any) => any {
     return (ctx, _) => p(ctx.element);
 }
 

+ 1 - 1
src/mol-theme/color/carbohydrate-symbol.ts

@@ -42,7 +42,7 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Va
             if (isSecondary) {
                 return SaccharideColors.Secondary
             } else {
-                if (StructureElement.isLocation(location)) {
+                if (StructureElement.Location.is(location)) {
                     return getColor(location.unit, location.element)
                 } else if (Link.isLocation(location)) {
                     return getColor(location.aUnit, location.aUnit.elements[location.aIndex])

+ 2 - 2
src/mol-theme/color/chain-id.ts

@@ -77,7 +77,7 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: PD.Values<ChainI
     let legend: ScaleLegend | TableLegend | undefined
 
     if (ctx.structure) {
-        const l = StructureElement.create()
+        const l = StructureElement.Location.create()
         const asymIdSerialMap = getAsymIdSerialMap(ctx.structure.root)
 
         const palette = getPalette(asymIdSerialMap.size, props)
@@ -85,7 +85,7 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: PD.Values<ChainI
 
         color = (location: Location): Color => {
             let serial: number | undefined = undefined
-            if (StructureElement.isLocation(location)) {
+            if (StructureElement.Location.is(location)) {
                 const asym_id = getAsymId(location.unit)
                 serial = asymIdSerialMap.get(asym_id(location))
             } else if (Link.isLocation(location)) {

+ 1 - 1
src/mol-theme/color/element-index.ts

@@ -47,7 +47,7 @@ export function ElementIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<E
         legend = palette.legend
 
         color = (location: Location): Color => {
-            if (StructureElement.isLocation(location)) {
+            if (StructureElement.Location.is(location)) {
                 const unitIndex = unitIdIndex.get(location.unit.id)!
                 const unitElementIndex = OrderedSet.findPredecessorIndex(units[unitIndex].elements, location.element)
                 return palette.color(cummulativeElementCount.get(unitIndex)! + unitElementIndex)

+ 1 - 1
src/mol-theme/color/element-symbol.ts

@@ -41,7 +41,7 @@ export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values<
     const colorMap = getAdjustedColorMap(ElementSymbolColors, props.saturation, props.lightness)
 
     function color(location: Location): Color {
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             if (Unit.isAtomic(location.unit)) {
                 const { type_symbol } = location.unit.model.atomicHierarchy.atoms
                 return elementSymbolColor(colorMap, type_symbol.value(location.element))

+ 3 - 3
src/mol-theme/color/entity-source.ts

@@ -105,14 +105,14 @@ export function EntitySourceColorTheme(ctx: ThemeDataContext, props: PD.Values<E
     let legend: ScaleLegend | TableLegend | undefined
 
     if (ctx.structure) {
-        const l = StructureElement.create()
+        const l = StructureElement.Location.create()
         const { models } = ctx.structure.root
         const { seqToSrcByModelEntity, srcKeySerialMap } = getMaps(models)
 
         const palette = getPalette(srcKeySerialMap.size + 1, props)
         legend = palette.legend
 
-        const getSrcColor = (location: StructureElement) => {
+        const getSrcColor = (location: StructureElement.Location) => {
             const modelIndex = models.indexOf(location.unit.model)
             const entityId = StructureProperties.entity.id(location)
             const mK = modelEntityKey(modelIndex, entityId)
@@ -126,7 +126,7 @@ export function EntitySourceColorTheme(ctx: ThemeDataContext, props: PD.Values<E
         }
 
         color = (location: Location): Color => {
-            if (StructureElement.isLocation(location)) {
+            if (StructureElement.Location.is(location)) {
                 return getSrcColor(location)
             } else if (Link.isLocation(location)) {
                 l.unit = location.aUnit

+ 1 - 1
src/mol-theme/color/hydrophobicity.ts

@@ -72,7 +72,7 @@ export function HydrophobicityColorTheme(ctx: ThemeDataContext, props: PD.Values
 
     function color(location: Location): Color {
         let compId: string | undefined
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             if (Unit.isAtomic(location.unit)) {
                 compId = getAtomicCompId(location.unit, location.element)
             } else {

+ 1 - 1
src/mol-theme/color/illustrative.ts

@@ -47,7 +47,7 @@ export function IllustrativeColorTheme(ctx: ThemeDataContext, props: PD.Values<I
     const colorMap = getAdjustedColorMap(ElementSymbolColors, 0, 0.7)
 
     function color(location: Location): Color {
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             if (Unit.isAtomic(location.unit)) {
                 const moleculeType = location.unit.model.atomicHierarchy.derived.residue.moleculeType[location.unit.residueIndex[location.element]]
                 const typeSymbol = location.unit.model.atomicHierarchy.atoms.type_symbol.value(location.element)

+ 1 - 1
src/mol-theme/color/model-index.ts

@@ -39,7 +39,7 @@ export function ModelIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<Mod
         }
 
         color = (location: Location): Color => {
-            if (StructureElement.isLocation(location)) {
+            if (StructureElement.Location.is(location)) {
                 return modelColor.get(location.unit.model.id)!
             } else if (Link.isLocation(location)) {
                 return modelColor.get(location.aUnit.model.id)!

+ 1 - 1
src/mol-theme/color/molecule-type.ts

@@ -56,7 +56,7 @@ export function MoleculeTypeColorTheme(ctx: ThemeDataContext, props: PD.Values<M
     const colorMap = getAdjustedColorMap(MoleculeTypeColors, props.saturation, props.lightness)
 
     function color(location: Location): Color {
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             return moleculeTypeColor(colorMap, location.unit, location.element)
         } else if (Link.isLocation(location)) {
             return moleculeTypeColor(colorMap, location.aUnit, location.aUnit.elements[location.aIndex])

+ 2 - 2
src/mol-theme/color/polymer-id.ts

@@ -86,7 +86,7 @@ export function PolymerIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Poly
     let legend: ScaleLegend | TableLegend | undefined
 
     if (ctx.structure) {
-        const l = StructureElement.create()
+        const l = StructureElement.Location.create()
         const polymerAsymIdSerialMap = getPolymerAsymIdSerialMap(ctx.structure.root)
 
         const palette = getPalette(polymerAsymIdSerialMap.size, props)
@@ -94,7 +94,7 @@ export function PolymerIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Poly
 
         color = (location: Location): Color => {
             let serial: number | undefined = undefined
-            if (StructureElement.isLocation(location)) {
+            if (StructureElement.Location.is(location)) {
                 const asym_id = getAsymId(location.unit)
                 serial = polymerAsymIdSerialMap.get(asym_id(location))
             } else if (Link.isLocation(location)) {

+ 1 - 1
src/mol-theme/color/polymer-index.ts

@@ -61,7 +61,7 @@ export function PolymerIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<P
 
         color = (location: Location): Color => {
             let color: Color | undefined
-            if (StructureElement.isLocation(location)) {
+            if (StructureElement.Location.is(location)) {
                 color = unitIdColor.get(location.unit.id)
             } else if (Link.isLocation(location)) {
                 color = unitIdColor.get(location.aUnit.id)

+ 1 - 1
src/mol-theme/color/residue-name.ts

@@ -102,7 +102,7 @@ export function ResidueNameColorTheme(ctx: ThemeDataContext, props: PD.Values<Re
     const colorMap = getAdjustedColorMap(ResidueNameColors, props.saturation, props.lightness)
 
     function color(location: Location): Color {
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             if (Unit.isAtomic(location.unit)) {
                 const compId = getAtomicCompId(location.unit, location.element)
                 return residueNameColor(colorMap, compId)

+ 1 - 1
src/mol-theme/color/secondary-structure.ts

@@ -92,7 +92,7 @@ export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: PD.Va
     const colorMap = getAdjustedColorMap(SecondaryStructureColors, props.saturation, props.lightness)
 
     function color(location: Location): Color {
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             return secondaryStructureColor(colorMap, location.unit, location.element, computedSecondaryStructure)
         } else if (Link.isLocation(location)) {
             return secondaryStructureColor(colorMap, location.aUnit, location.aUnit.elements[location.aIndex], computedSecondaryStructure)

+ 1 - 1
src/mol-theme/color/sequence-id.ts

@@ -71,7 +71,7 @@ export function SequenceIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Seq
         maxLabel: 'End',
     })
     const color = (location: Location): Color => {
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             const { unit, element } = location
             const seq_id = getSeqId(unit, element)
             if (seq_id > 0) {

+ 1 - 1
src/mol-theme/color/uncertainty.ts

@@ -44,7 +44,7 @@ export function UncertaintyColorTheme(ctx: ThemeDataContext, props: PD.Values<Un
     // TODO calc domain based on data, set min/max as 10/90 percentile to be robust against outliers
 
     function color(location: Location): Color {
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             return scale.color(getUncertainty(location.unit, location.element))
         } else if (Link.isLocation(location)) {
             return scale.color(getUncertainty(location.aUnit, location.aUnit.elements[location.aIndex]))

+ 1 - 1
src/mol-theme/color/unit-index.ts

@@ -46,7 +46,7 @@ export function UnitIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<Unit
         }
 
         color = (location: Location): Color => {
-            if (StructureElement.isLocation(location)) {
+            if (StructureElement.Location.is(location)) {
                 return unitIdColor.get(location.unit.id)!
             } else if (Link.isLocation(location)) {
                 return unitIdColor.get(location.aUnit.id)!

+ 10 - 10
src/mol-theme/label.ts

@@ -10,10 +10,10 @@ import { Loci } from '../mol-model/loci';
 import { OrderedSet } from '../mol-data/int';
 
 // for `labelFirst`, don't create right away to avoid problems with circular dependencies/imports
-let elementLocA: StructureElement
-let elementLocB: StructureElement
+let elementLocA: StructureElement.Location
+let elementLocB: StructureElement.Location
 
-function setElementLocation(loc: StructureElement, unit: Unit, index: StructureElement.UnitIndex) {
+function setElementLocation(loc: StructureElement.Location, unit: Unit, index: StructureElement.UnitIndex) {
     loc.unit = unit
     loc.element = unit.elements[index]
 }
@@ -75,8 +75,8 @@ export function structureElementStatsLabel(stats: StructureElement.Stats, counts
 }
 
 export function linkLabel(link: Link.Location) {
-    if (!elementLocA) elementLocA = StructureElement.create()
-    if (!elementLocB) elementLocB = StructureElement.create()
+    if (!elementLocA) elementLocA = StructureElement.Location.create()
+    if (!elementLocB) elementLocB = StructureElement.Location.create()
     setElementLocation(elementLocA, link.aUnit, link.aIndex)
     setElementLocation(elementLocB, link.bUnit, link.bIndex)
     return `${elementLabel(elementLocA)} - ${elementLabel(elementLocB)}`
@@ -84,15 +84,15 @@ export function linkLabel(link: Link.Location) {
 
 export type LabelGranularity = 'element' | 'residue' | 'chain' | 'structure'
 
-export function elementLabel(location: StructureElement, granularity: LabelGranularity = 'element') {
+export function elementLabel(location: StructureElement.Location, granularity: LabelGranularity = 'element') {
     const model = location.unit.model.entry
     const instance = location.unit.conformation.operator.name
     const label = [model, instance]
 
     if (Unit.isAtomic(location.unit)) {
-        label.push(atomicElementLabel(location as StructureElement<Unit.Atomic>, granularity))
+        label.push(atomicElementLabel(location as StructureElement.Location<Unit.Atomic>, granularity))
     } else if (Unit.isCoarse(location.unit)) {
-        label.push(coarseElementLabel(location as StructureElement<Unit.Spheres | Unit.Gaussians>, granularity))
+        label.push(coarseElementLabel(location as StructureElement.Location<Unit.Spheres | Unit.Gaussians>, granularity))
     } else {
         label.push('Unknown')
     }
@@ -100,7 +100,7 @@ export function elementLabel(location: StructureElement, granularity: LabelGranu
     return label.join(' | ')
 }
 
-export function atomicElementLabel(location: StructureElement<Unit.Atomic>, granularity: LabelGranularity) {
+export function atomicElementLabel(location: StructureElement.Location<Unit.Atomic>, granularity: LabelGranularity) {
     const label_asym_id = Props.chain.label_asym_id(location)
     const auth_asym_id = Props.chain.auth_asym_id(location)
     const seq_id = location.unit.model.atomicHierarchy.residues.auth_seq_id.isDefined ? Props.residue.auth_seq_id(location) : Props.residue.label_seq_id(location)
@@ -122,7 +122,7 @@ export function atomicElementLabel(location: StructureElement<Unit.Atomic>, gran
     return label.reverse().join(' | ')
 }
 
-export function coarseElementLabel(location: StructureElement<Unit.Spheres | Unit.Gaussians>, granularity: LabelGranularity) {
+export function coarseElementLabel(location: StructureElement.Location<Unit.Spheres | Unit.Gaussians>, granularity: LabelGranularity) {
     // TODO handle granularity
     const asym_id = Props.coarse.asym_id(location)
     const seq_id_begin = Props.coarse.seq_id_begin(location)

+ 1 - 1
src/mol-theme/size/physical.ts

@@ -37,7 +37,7 @@ export function getPhysicalRadius(unit: Unit, element: ElementIndex): number {
 export function PhysicalSizeTheme(ctx: ThemeDataContext, props: PD.Values<PhysicalSizeThemeParams>): SizeTheme<PhysicalSizeThemeParams> {
     function size(location: Location): number {
         let size: number
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             size = getPhysicalRadius(location.unit, location.element)
         } else if (Link.isLocation(location)) {
             size = getPhysicalRadius(location.aUnit, location.aUnit.elements[location.aIndex])

+ 1 - 1
src/mol-theme/size/uncertainty.ts

@@ -35,7 +35,7 @@ export function getUncertainty(unit: Unit, element: ElementIndex, props: PD.Valu
 export function UncertaintySizeTheme(ctx: ThemeDataContext, props: PD.Values<UncertaintySizeThemeParams>): SizeTheme<UncertaintySizeThemeParams> {
     function size(location: Location): number {
         let size = props.baseSize
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             size += getUncertainty(location.unit, location.element, props)
         } else if (Link.isLocation(location)) {
             size += getUncertainty(location.aUnit, location.aUnit.elements[location.aIndex], props)

+ 1 - 1
src/perf-tests/structure.ts

@@ -117,7 +117,7 @@ export namespace PropertyAccess {
     }
 
     function sumProperty(structure: Structure, p: StructureElement.Property<number>) {
-        const l = StructureElement.create();
+        const l = StructureElement.Location.create();
         let s = 0;
 
         for (const unit of structure.units) {

+ 1 - 1
src/tests/browser/render-asa.ts

@@ -117,7 +117,7 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
         domain: [0.0, 1.0]
     })
     color = (location: Location): Color => {
-        if (StructureElement.isLocation(location)) {
+        if (StructureElement.Location.is(location)) {
             if (Unit.isAtomic(location.unit)) {
                 const value = accessibleSurfaceArea.relativeAccessibleSurfaceArea![location.unit.residueIndex[location.element]];
                 return value !== AccessibleSurfaceArea.VdWLookup[0] /* signals missing value */ ? scale.color(value) : DefaultColor;