Kaynağa Gözat

Issue #805: tmdet-extension added, hacked loadStructureFromUrl

cycle20 1 yıl önce
ebeveyn
işleme
2f5be57139

+ 1 - 0
package.json

@@ -32,6 +32,7 @@
     "main": "build/src/index.js",
     "files": [
         "build/dist/",
+        "build/src/tmdet-extension/",
         "build/src/viewer/"
     ],
     "author": "RCSB PDB and Mol* Contributors",

+ 417 - 0
src/tmdet-extension/LICENSE

@@ -0,0 +1,417 @@
+Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+
+Authors:
+
+* Gabor Tusnady (tusnady.gabor at ttk.hu)
+* Csongor Gerdan (gerdan.csongor at ttk.hu)
+
+=======================================================================
+
+Attribution-NonCommercial 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+     Considerations for licensors: Our public licenses are
+     intended for use by those authorized to give the public
+     permission to use material in ways otherwise restricted by
+     copyright and certain other rights. Our licenses are
+     irrevocable. Licensors should read and understand the terms
+     and conditions of the license they choose before applying it.
+     Licensors should also secure all rights necessary before
+     applying our licenses so that the public can reuse the
+     material as expected. Licensors should clearly mark any
+     material not subject to the license. This includes other CC-
+     licensed material, or material used under an exception or
+     limitation to copyright. More considerations for licensors:
+    wiki.creativecommons.org/Considerations_for_licensors
+
+     Considerations for the public: By using one of our public
+     licenses, a licensor grants the public permission to use the
+     licensed material under specified terms and conditions. If
+     the licensor's permission is not necessary for any reason--for
+     example, because of any applicable exception or limitation to
+     copyright--then that use is not regulated by the license. Our
+     licenses grant only permissions under copyright and certain
+     other rights that a licensor has authority to grant. Use of
+     the licensed material may still be restricted for other
+     reasons, including because others have copyright or other
+     rights in the material. A licensor may make special requests,
+     such as asking that all changes be marked or described.
+     Although not required by our licenses, you are encouraged to
+     respect those requests where reasonable. More considerations
+     for the public:
+    wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution-NonCommercial 4.0 International Public
+License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-NonCommercial 4.0 International Public License ("Public
+License"). To the extent this Public License may be interpreted as a
+contract, You are granted the Licensed Rights in consideration of Your
+acceptance of these terms and conditions, and the Licensor grants You
+such rights in consideration of benefits the Licensor receives from
+making the Licensed Material available under these terms and
+conditions.
+
+
+Section 1 -- Definitions.
+
+  a. Adapted Material means material subject to Copyright and Similar
+     Rights that is derived from or based upon the Licensed Material
+     and in which the Licensed Material is translated, altered,
+     arranged, transformed, or otherwise modified in a manner requiring
+     permission under the Copyright and Similar Rights held by the
+     Licensor. For purposes of this Public License, where the Licensed
+     Material is a musical work, performance, or sound recording,
+     Adapted Material is always produced where the Licensed Material is
+     synched in timed relation with a moving image.
+
+  b. Adapter's License means the license You apply to Your Copyright
+     and Similar Rights in Your contributions to Adapted Material in
+     accordance with the terms and conditions of this Public License.
+
+  c. Copyright and Similar Rights means copyright and/or similar rights
+     closely related to copyright including, without limitation,
+     performance, broadcast, sound recording, and Sui Generis Database
+     Rights, without regard to how the rights are labeled or
+     categorized. For purposes of this Public License, the rights
+     specified in Section 2(b)(1)-(2) are not Copyright and Similar
+     Rights.
+  d. Effective Technological Measures means those measures that, in the
+     absence of proper authority, may not be circumvented under laws
+     fulfilling obligations under Article 11 of the WIPO Copyright
+     Treaty adopted on December 20, 1996, and/or similar international
+     agreements.
+
+  e. Exceptions and Limitations means fair use, fair dealing, and/or
+     any other exception or limitation to Copyright and Similar Rights
+     that applies to Your use of the Licensed Material.
+
+  f. Licensed Material means the artistic or literary work, database,
+     or other material to which the Licensor applied this Public
+     License.
+
+  g. Licensed Rights means the rights granted to You subject to the
+     terms and conditions of this Public License, which are limited to
+     all Copyright and Similar Rights that apply to Your use of the
+     Licensed Material and that the Licensor has authority to license.
+
+  h. Licensor means the individual(s) or entity(ies) granting rights
+     under this Public License.
+
+  i. NonCommercial means not primarily intended for or directed towards
+     commercial advantage or monetary compensation. For purposes of
+     this Public License, the exchange of the Licensed Material for
+     other material subject to Copyright and Similar Rights by digital
+     file-sharing or similar means is NonCommercial provided there is
+     no payment of monetary compensation in connection with the
+     exchange.
+
+  j. Share means to provide material to the public by any means or
+     process that requires permission under the Licensed Rights, such
+     as reproduction, public display, public performance, distribution,
+     dissemination, communication, or importation, and to make material
+     available to the public including in ways that members of the
+     public may access the material from a place and at a time
+     individually chosen by them.
+
+  k. Sui Generis Database Rights means rights other than copyright
+     resulting from Directive 96/9/EC of the European Parliament and of
+     the Council of 11 March 1996 on the legal protection of databases,
+     as amended and/or succeeded, as well as other essentially
+     equivalent rights anywhere in the world.
+
+  l. You means the individual or entity exercising the Licensed Rights
+     under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+  a. License grant.
+
+       1. Subject to the terms and conditions of this Public License,
+          the Licensor hereby grants You a worldwide, royalty-free,
+          non-sublicensable, non-exclusive, irrevocable license to
+          exercise the Licensed Rights in the Licensed Material to:
+
+            a. reproduce and Share the Licensed Material, in whole or
+               in part, for NonCommercial purposes only; and
+
+            b. produce, reproduce, and Share Adapted Material for
+               NonCommercial purposes only.
+
+       2. Exceptions and Limitations. For the avoidance of doubt, where
+          Exceptions and Limitations apply to Your use, this Public
+          License does not apply, and You do not need to comply with
+          its terms and conditions.
+
+       3. Term. The term of this Public License is specified in Section
+          6(a).
+
+       4. Media and formats; technical modifications allowed. The
+          Licensor authorizes You to exercise the Licensed Rights in
+          all media and formats whether now known or hereafter created,
+          and to make technical modifications necessary to do so. The
+          Licensor waives and/or agrees not to assert any right or
+          authority to forbid You from making technical modifications
+          necessary to exercise the Licensed Rights, including
+          technical modifications necessary to circumvent Effective
+          Technological Measures. For purposes of this Public License,
+          simply making modifications authorized by this Section 2(a)
+          (4) never produces Adapted Material.
+
+       5. Downstream recipients.
+
+            a. Offer from the Licensor -- Licensed Material. Every
+               recipient of the Licensed Material automatically
+               receives an offer from the Licensor to exercise the
+               Licensed Rights under the terms and conditions of this
+               Public License.
+
+            b. No downstream restrictions. You may not offer or impose
+               any additional or different terms or conditions on, or
+               apply any Effective Technological Measures to, the
+               Licensed Material if doing so restricts exercise of the
+               Licensed Rights by any recipient of the Licensed
+               Material.
+
+       6. No endorsement. Nothing in this Public License constitutes or
+          may be construed as permission to assert or imply that You
+          are, or that Your use of the Licensed Material is, connected
+          with, or sponsored, endorsed, or granted official status by,
+          the Licensor or others designated to receive attribution as
+          provided in Section 3(a)(1)(A)(i).
+
+  b. Other rights.
+
+       1. Moral rights, such as the right of integrity, are not
+          licensed under this Public License, nor are publicity,
+          privacy, and/or other similar personality rights; however, to
+          the extent possible, the Licensor waives and/or agrees not to
+          assert any such rights held by the Licensor to the limited
+          extent necessary to allow You to exercise the Licensed
+          Rights, but not otherwise.
+
+       2. Patent and trademark rights are not licensed under this
+          Public License.
+
+       3. To the extent possible, the Licensor waives any right to
+          collect royalties from You for the exercise of the Licensed
+          Rights, whether directly or through a collecting society
+          under any voluntary or waivable statutory or compulsory
+          licensing scheme. In all other cases the Licensor expressly
+          reserves any right to collect such royalties, including when
+          the Licensed Material is used other than for NonCommercial
+          purposes.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+  a. Attribution.
+
+       1. If You Share the Licensed Material (including in modified
+          form), You must:
+
+            a. retain the following if it is supplied by the Licensor
+               with the Licensed Material:
+
+                 i. identification of the creator(s) of the Licensed
+                    Material and any others designated to receive
+                    attribution, in any reasonable manner requested by
+                    the Licensor (including by pseudonym if
+                    designated);
+
+                ii. a copyright notice;
+
+               iii. a notice that refers to this Public License;
+
+                iv. a notice that refers to the disclaimer of
+                    warranties;
+
+                 v. a URI or hyperlink to the Licensed Material to the
+                    extent reasonably practicable;
+
+            b. indicate if You modified the Licensed Material and
+               retain an indication of any previous modifications; and
+
+            c. indicate the Licensed Material is licensed under this
+               Public License, and include the text of, or the URI or
+               hyperlink to, this Public License.
+
+       2. You may satisfy the conditions in Section 3(a)(1) in any
+          reasonable manner based on the medium, means, and context in
+          which You Share the Licensed Material. For example, it may be
+          reasonable to satisfy the conditions by providing a URI or
+          hyperlink to a resource that includes the required
+          information.
+
+       3. If requested by the Licensor, You must remove any of the
+          information required by Section 3(a)(1)(A) to the extent
+          reasonably practicable.
+
+       4. If You Share Adapted Material You produce, the Adapter's
+          License You apply must not prevent recipients of the Adapted
+          Material from complying with this Public License.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+  a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+     to extract, reuse, reproduce, and Share all or a substantial
+     portion of the contents of the database for NonCommercial purposes
+     only;
+
+  b. if You include all or a substantial portion of the database
+     contents in a database in which You have Sui Generis Database
+     Rights, then the database in which You have Sui Generis Database
+     Rights (but not its individual contents) is Adapted Material; and
+
+  c. You must comply with the conditions in Section 3(a) if You Share
+     all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+  c. The disclaimer of warranties and limitation of liability provided
+     above shall be interpreted in a manner that, to the extent
+     possible, most closely approximates an absolute disclaimer and
+     waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+  a. This Public License applies for the term of the Copyright and
+     Similar Rights licensed here. However, if You fail to comply with
+     this Public License, then Your rights under this Public License
+     terminate automatically.
+
+  b. Where Your right to use the Licensed Material has terminated under
+     Section 6(a), it reinstates:
+
+       1. automatically as of the date the violation is cured, provided
+          it is cured within 30 days of Your discovery of the
+          violation; or
+
+       2. upon express reinstatement by the Licensor.
+
+     For the avoidance of doubt, this Section 6(b) does not affect any
+     right the Licensor may have to seek remedies for Your violations
+     of this Public License.
+
+  c. For the avoidance of doubt, the Licensor may also offer the
+     Licensed Material under separate terms or conditions or stop
+     distributing the Licensed Material at any time; however, doing so
+     will not terminate this Public License.
+
+  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+     License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+  a. The Licensor shall not be bound by any additional or different
+     terms or conditions communicated by You unless expressly agreed.
+
+  b. Any arrangements, understandings, or agreements regarding the
+     Licensed Material not stated herein are separate from and
+     independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+  a. For the avoidance of doubt, this Public License does not, and
+     shall not be interpreted to, reduce, limit, restrict, or impose
+     conditions on any use of the Licensed Material that could lawfully
+     be made without permission under this Public License.
+
+  b. To the extent possible, if any provision of this Public License is
+     deemed unenforceable, it shall be automatically reformed to the
+     minimum extent necessary to make it enforceable. If the provision
+     cannot be reformed, it shall be severed from this Public License
+     without affecting the enforceability of the remaining terms and
+     conditions.
+
+  c. No term or condition of this Public License will be waived and no
+     failure to comply consented to unless expressly agreed to by the
+     Licensor.
+
+  d. Nothing in this Public License constitutes or may be interpreted
+     as a limitation upon, or waiver of, any privileges and immunities
+     that apply to the Licensor or You, including from the legal
+     processes of any jurisdiction or authority.
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
+

+ 55 - 0
src/tmdet-extension/README.md

@@ -0,0 +1,55 @@
+# TMDET Extension
+
+TmMol* is a Mol* extension to visualize transmembrane regions and topology
+according to data of http://pdbtm.enzim.hu/ served by PDBe.
+
+
+# Usage
+
+`loadWithUNITMPMembraneRepresentation` call on an initialized
+`PluginUIContext` object downloads structure and annotation data
+from the given URLs.
+
+For example:
+
+```
+<script type="text/javascript" src="./tm_molstar.js"></script>
+<script type="text/javascript">
+    // init viewer
+    var viewer = new tm_molstar.Viewer('app', {
+        layoutShowControls: false,
+        layoutIsExpanded: false,
+        viewportShowExpand: true,
+        collapseLeftPanel: true
+    });
+
+    var pdbId = '1a0s';
+
+    tm_molstar.loadWithUNITMPMembraneRepresentation(viewer.plugin, {
+
+        // URL to get mmCIF/PDBx data
+        structureUrl: `https://cs.litemol.org/${pdbId}/full`,
+
+        // URL to load PDBTM information according to FunPDBe Data Exchange Format
+        regionDescriptorUrl: `http://localhost:8000/tmdet-data/${pdbId}.json`,
+    });
+...
+
+```
+
+Please see the `index.html` and `index.ts` files of the demo
+application in the `src/apps/tm-viwer` folder and example data
+files in `data/tmdet-example-annotations` folder for more details.
+
+# Authors
+
+* Gabor Tusnady (tusnady.gabor at ttk.hu), Protein Bioinformatics Research Group, RCNS.
+* Csongor Gerdan (gerdan.csongor at ttk.hu), Protein Bioinformatics Research Group, RCNS.
+
+# License
+
+Creative Commons Attribution-NonCommercial 4.0 International Public
+License
+
+Please see the LICENSE file for more details or visit
+https://creativecommons.org/licenses/by-nc/4.0/.

+ 41 - 0
src/tmdet-extension/algorithm.ts

@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2018-2020 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>
+ */
+/**
+ * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+ *
+ * Licensed under CC BY-NC 4.0, see LICENSE file for more info.
+ *
+ * @author Gabor Tusnady <tusnady.gabor@ttk.hu>
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+
+import { Vec3 } from 'molstar/lib/mol-math/linear-algebra';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import 'molstar/lib/mol-util/polyfill';
+
+export const TMDETParams = {
+    numberOfSpherePoints: PD.Numeric(140, { min: 35, max: 700, step: 1 }, { description: 'Number of spheres/directions to test for membrane placement. Original value is 350.' }),
+    stepSize: PD.Numeric(1, { min: 0.25, max: 4, step: 0.25 }, { description: 'Thickness of membrane slices that will be tested' }),
+    minThickness: PD.Numeric(20, { min: 10, max: 30, step: 1}, { description: 'Minimum membrane thickness used during refinement' }),
+    maxThickness: PD.Numeric(40, { min: 30, max: 50, step: 1}, { description: 'Maximum membrane thickness used during refinement' }),
+    adjust: PD.Numeric(14, { min: 0, max: 30, step: 1 }, { description: 'Minimum length of membrane-spanning regions (original values: 14 for alpha-helices and 5 for beta sheets). Set to 0 to not optimize membrane thickness.' }),
+};
+export type TMDETParams = typeof TMDETParams
+export type TMDETProps = PD.Values<TMDETParams>
+
+const v3dot = Vec3.dot;
+
+export function isInMembranePlane(testPoint: Vec3, normalVector: Vec3, planePoint1: Vec3, planePoint2: Vec3): boolean {
+    const d1 = -v3dot(normalVector, planePoint1);
+    const d2 = -v3dot(normalVector, planePoint2);
+    return _isInMembranePlane(testPoint, normalVector, Math.min(d1, d2), Math.max(d1, d2));
+}
+
+function _isInMembranePlane(testPoint: Vec3, normalVector: Vec3, min: number, max: number): boolean {
+    const d = -v3dot(normalVector, testPoint);
+    return d > min && d < max;
+}

+ 346 - 0
src/tmdet-extension/behavior.ts

@@ -0,0 +1,346 @@
+/**
+ * Copyright (c) 2018-2020 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>
+ */
+/**
+ * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+ *
+ * Licensed under CC BY-NC 4.0, see LICENSE file for more info.
+ *
+ * @author Gabor Tusnady <tusnady.gabor@ttk.hu>
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from 'molstar/lib/mol-plugin-state/builder/structure/representation-preset';
+import { StateObject, StateObjectRef, StateObjectCell, StateTransformer, StateTransform } from 'molstar/lib/mol-state';
+import { Task } from 'molstar/lib/mol-task';
+import { PluginBehavior } from 'molstar/lib/mol-plugin/behavior';
+import { PluginStateObject, PluginStateTransform } from 'molstar/lib/mol-plugin-state/objects';
+import { PluginContext } from 'molstar/lib/mol-plugin/context';
+import { DefaultQueryRuntimeTable } from 'molstar/lib/mol-script/runtime/query/compiler';
+import { StructureSelectionQuery, StructureSelectionCategory } from 'molstar/lib/mol-plugin-state/helpers/structure-selection-query';
+import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
+import { GenericRepresentationRef } from 'molstar/lib/mol-plugin-state/manager/structure/hierarchy-state';
+import { PluginUIContext } from 'molstar/lib/mol-plugin-ui/context';
+// import { Gunzip } from "zlibt2";
+
+
+// TMDET imports
+import { MembraneOrientationRepresentationProvider, MembraneOrientationParams, MembraneOrientationRepresentation } from './representation';
+import { MembraneOrientationProvider, MembraneOrientation, TmDetDescriptorCache } from './prop';
+import { applyTransformations, createMembraneOrientation } from './transformation';
+import { ComponentsType, PDBTMDescriptor, PMS } from './types';
+import { registerTmDetSymmetry } from './symmetry';
+import { TmDetLabelProvider } from './labeling';
+import { TmDetColorThemeProvider, updateSiteColors } from './tmdet-color-theme';
+//import { loadInitialSnapshot, rotateCamera, storeCameraSnapshot } from './camera';
+import { DebugUtil } from './debug-utils';
+
+const Tag = MembraneOrientation.Tag;
+const TMDET_MEMBRANE_ORIENTATION = 'TMDET Membrane Orientation';
+
+export const TMDETMembraneOrientation = PluginBehavior.create<{ autoAttach: boolean }>({
+    name: 'tmdet-membrane-orientation-prop',
+    category: 'custom-props',
+    display: {
+        name: TMDET_MEMBRANE_ORIENTATION,
+        description: 'Data calculated with TMDET algorithm.'
+    },
+    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
+        private provider = MembraneOrientationProvider
+
+        register(): void {
+            console.log('TMDET REGISER');
+            DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);
+
+            this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach);
+
+            this.ctx.representation.structure.registry.add(MembraneOrientationRepresentationProvider);
+            this.ctx.query.structure.registry.add(isTransmembrane);
+
+            this.ctx.representation.structure.themes.colorThemeRegistry.add(TmDetColorThemeProvider);
+            this.ctx.managers.lociLabels.addProvider(TmDetLabelProvider);
+
+            this.ctx.genericRepresentationControls.set(Tag.Representation, selection => {
+                const refs: GenericRepresentationRef[] = [];
+                selection.structures.forEach(structure => {
+                    const memRepr = structure.genericRepresentations?.filter(r => r.cell.transform.transformer.id === MembraneOrientation3D.id)[0];
+                    if (memRepr) refs.push(memRepr);
+                });
+                return [refs, 'Membrane Orientation'];
+            });
+            this.ctx.builders.structure.representation.registerPreset(MembraneOrientationPreset);
+        }
+
+        update(p: { autoAttach: boolean }) {
+            let updated = this.params.autoAttach !== p.autoAttach;
+            this.params.autoAttach = p.autoAttach;
+            this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
+            return updated;
+        }
+
+        unregister() {
+            DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor);
+
+            this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
+
+            this.ctx.representation.structure.registry.remove(MembraneOrientationRepresentationProvider);
+            this.ctx.query.structure.registry.remove(isTransmembrane);
+
+            this.ctx.genericRepresentationControls.delete(Tag.Representation);
+            this.ctx.builders.structure.representation.unregisterPreset(MembraneOrientationPreset);
+
+            this.ctx.representation.structure.themes.colorThemeRegistry.remove(TmDetColorThemeProvider);
+            this.ctx.managers.lociLabels.removeProvider(TmDetLabelProvider);
+        }
+    },
+    params: () => ({
+        autoAttach: PD.Boolean(false)
+    })
+});
+
+//
+
+export const isTransmembrane = StructureSelectionQuery('Residues Embedded in Membrane', MS.struct.modifier.union([
+    MS.struct.modifier.wholeResidues([
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                'atom-test': MembraneOrientation.symbols.isTransmembrane.symbol(),
+            })
+        ])
+    ])
+]), {
+    description: 'Select residues that are embedded between the membrane layers.',
+    category: StructureSelectionCategory.Residue,
+    ensureCustomProperties: (ctx, structure) => {
+        return MembraneOrientationProvider.attach(ctx, structure);
+    }
+});
+
+
+
+
+//
+// //////////////////////////// TMDET VIEWER FUNCTIONS
+//
+
+
+
+
+export let membraneOrientation: MembraneOrientation;
+
+export async function loadWithUNITMPMembraneRepresentation(plugin: PluginUIContext, params: any) {
+    //storeCameraSnapshot(plugin); // store if it is not stored yet
+
+    //loadInitialSnapshot(plugin); // load if there is a stored one
+    setTimeout(() => { plugin.clear(); }, 100); // clear scene after some delay
+
+    updateSiteColors(params.side1);
+
+    setTimeout(() => { (async () => {
+        const pdbtmDescriptor: PDBTMDescriptor = await downloadRegionDescriptor(plugin, params);
+        pdbtmDescriptor.side1 = params.side1;
+        TmDetDescriptorCache.add(pdbtmDescriptor);
+
+        membraneOrientation = createMembraneOrientation(pdbtmDescriptor);
+
+        // load structure
+        await loadStructure(plugin, params, pdbtmDescriptor);
+        // cartoon, colors etc.
+        await createStructureRepresentation(plugin, pdbtmDescriptor);
+
+        //
+        // It also resets the camera because the membranes render 1st and the structure might not be fully visible
+        //
+        //rotateCamera(plugin);
+    })(); }, 500);
+}
+
+async function downloadRegionDescriptor(plugin: PluginUIContext, params: any): Promise<any> {
+    // run a fetch task
+    const downloadResult: string = await plugin.runTask(plugin.fetch({ url: params.regionDescriptorUrl })) as string;
+    const pdbtmDescriptor: any = JSON.parse(downloadResult);
+    return pdbtmDescriptor;
+}
+
+async function createStructureRepresentation(plugin: PluginUIContext, pdbtmDescriptor: any) {
+    // get the first structure of the first model
+    const structure: StateObjectRef<PMS> = plugin.managers.structure.hierarchy.current.models[0].structures[0].cell;
+    const components = await createStructureComponents(plugin, structure);
+
+    await applyTransformations(plugin, pdbtmDescriptor);
+
+    await buildStructureRepresentation(plugin, pdbtmDescriptor, components);
+}
+
+async function createStructureComponents(plugin: PluginUIContext, structure: StateObjectCell<PMS, StateTransform<StateTransformer<StateObject<any, StateObject.Type<any>>, StateObject<any, StateObject.Type<any>>, any>>>) {
+    return {
+        polymer: await plugin.builders.structure.tryCreateComponentStatic(structure, 'polymer'),
+        ligand: await plugin.builders.structure.tryCreateComponentStatic(structure, 'ligand'),
+        water: await plugin.builders.structure.tryCreateComponentStatic(structure, 'water'),
+    };
+}
+
+async function buildStructureRepresentation(plugin: PluginUIContext, pdbtmDescriptor: PDBTMDescriptor, components: ComponentsType) {
+    const builder = plugin.builders.structure.representation;
+    const update = plugin.build();
+    if (components.polymer) {
+        builder.buildRepresentation(update, components.polymer, {
+                type: 'cartoon',
+                color: TmDetColorThemeProvider.name as any, colorParams: { pdbtmDescriptor }
+            },
+            { tag: 'polymer' }
+        );
+    }
+    if (components.ligand)
+        builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick' }, { tag: 'ligand' });
+    if (components.water)
+        builder.buildRepresentation(update, components.water, { type: 'ball-and-stick', typeParams: { alpha: 0.6 } }, { tag: 'water' });
+    await update.commit();
+}
+
+async function loadStructure(ctx: PluginUIContext, params: any, pdbtmDescriptor: PDBTMDescriptor): Promise<void> {
+
+    // replace original symmetry format function
+    registerTmDetSymmetry(pdbtmDescriptor);
+
+    let format: "mmcif"|"pdb" = "mmcif";
+    // check url to determine file format and compression
+    const match: string[] = params.structureUrl.match(new RegExp('/(pdb|cif)(.gz)?$/g/'));
+    DebugUtil.log(`format: ${params.format}`);
+    if ((params.format != 'mmcif' || params.format != 'pdb') && match != null) {
+        if (match[0].startsWith('cif')) {
+            format = 'mmcif';
+        }
+        if (match[0].startsWith('pdb')) {
+            format = 'pdb';
+        }
+    } else if (params.format) {
+        format = params.format;
+    }
+
+    const builders = ctx.builders;
+    const data = await downloadData(ctx, params, pdbtmDescriptor);
+
+    const trajectory = await builders.structure.parseTrajectory(data, format);
+
+
+    // create membrane representation
+    await builders.structure.hierarchy.applyPreset(
+        trajectory, 'default', { representationPreset: 'tmdet-preset-membrane-orientation' as any });
+}
+
+
+async function downloadData(ctx: PluginUIContext, params: any, pdbtmDescriptor: PDBTMDescriptor) {
+
+    // let gzipped: boolean = false;
+    // if (params.compression || params.structureUrl.endsWith('gz')) {
+    //     gzipped = true;
+    // }
+
+    const builders = ctx.builders;
+    let downloadResult = await ctx.runTask(ctx.fetch({ url: params.structureUrl, type: "string" }));
+    DebugUtil.log('First 50 chars of input data', downloadResult.slice(0, 50));
+    // TODO: temporary solution
+    // TODO: downloadResult = downloadResult.replace(/HETATM.+\n/mg, ''); // remove all hetatom stuffs
+    // TODO: const uncompressed: string = await ungzip(downloadResult); // it does not work right now
+    // console.log(uncompressed.slice(0, 100));
+
+    return await builders.data.rawData({
+        data: downloadResult,
+        label: `${pdbtmDescriptor.pdb_id}`,
+    }); // , { state: { isGhost: true } });
+
+}
+
+// async function ungzip(data: Uint8Array) {
+//     // TODO: it does not work :(
+//     return new Gunzip(data).decompress();
+// }
+
+//
+// //////////////////////////// END OF TMDET VIEWER SECTION
+//
+
+
+
+
+type MembraneOrientation3DType = typeof MembraneOrientation3D
+const MembraneOrientation3D = PluginStateTransform.BuiltIn({
+    name: Tag.Representation,
+    display: {
+        name: TMDET_MEMBRANE_ORIENTATION,
+        description: 'Membrane Orientation planes and rims. Data calculated with TMDET algorithm.'
+    },
+    from: PluginStateObject.Molecule.Structure,
+    to: PluginStateObject.Shape.Representation3D,
+    params: () => {
+        return {
+            ...MembraneOrientationParams,
+        };
+    }
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create(TMDET_MEMBRANE_ORIENTATION, async ctx => {
+            await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
+            const repr = MembraneOrientationRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => MembraneOrientationParams);
+            await repr.createOrUpdate(params, a.data).runInContext(ctx);
+            return new PluginStateObject.Shape.Representation3D({ repr, sourceData: a.data }, { label: TMDET_MEMBRANE_ORIENTATION });
+        });
+    },
+    update({ a, b, newParams }, plugin: PluginContext) {
+        return Task.create(TMDET_MEMBRANE_ORIENTATION, async ctx => {
+            await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
+            const props = { ...b.data.repr.props, ...newParams };
+            await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
+            b.data.sourceData = a.data;
+            return StateTransformer.UpdateResult.Updated;
+        });
+    },
+    isApplicable(a) {
+        return MembraneOrientationProvider.isApplicable(a.data);
+    }
+});
+
+export const MembraneOrientationPreset = StructureRepresentationPresetProvider({
+    id: 'tmdet-preset-membrane-orientation',
+    display: {
+        name: TMDET_MEMBRANE_ORIENTATION, group: 'Annotation',
+        description: 'Shows orientation of membrane layers. Data calculated with TMDET algorithm.' // TODO add ' or obtained via RCSB PDB'
+    },
+    isApplicable(a) {
+        return MembraneOrientationProvider.isApplicable(a.data);
+    },
+    params: () => StructureRepresentationPresetProvider.CommonParams,
+    async apply(ref, params, plugin) {
+        const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
+        const structure  = structureCell?.obj?.data;
+        if (!structureCell || !structure) return {};
+
+        if (!MembraneOrientationProvider.get(structure).value) {
+            await plugin.runTask(Task.create(TMDET_MEMBRANE_ORIENTATION, async runtime => {
+                await MembraneOrientationProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure);
+            }));
+        }
+
+        const membraneOrientation = await tryCreateMembraneOrientation(plugin, structureCell);
+        const colorTheme =  TmDetColorThemeProvider.name as any;
+        const preset = await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin);
+
+        return { components: preset.components, representations: { ...preset.representations, membraneOrientation } };
+    }
+});
+
+export function tryCreateMembraneOrientation(plugin: PluginContext, structure: StateObjectRef<PMS>, params?: StateTransformer.Params<MembraneOrientation3DType>, initialState?: Partial<StateTransform.State>) {
+    const state = plugin.state.data;
+    const membraneOrientation = state.build().to(structure)
+        .applyOrUpdateTagged(Tag.Representation, MembraneOrientation3D, params, { state: initialState });
+    return membraneOrientation.commit({ revertOnError: true });
+}

+ 67 - 0
src/tmdet-extension/camera.ts

@@ -0,0 +1,67 @@
+/**
+ * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+ *
+ * Licensed under CC BY-NC 4.0, see LICENSE file for more info.
+ *
+ * @author Gabor Tusnady <tusnady.gabor@ttk.hu>
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+
+import { PluginUIContext } from 'molstar/lib/mol-plugin-ui/context';
+import { Quat, Vec3 } from 'molstar/lib/mol-math/linear-algebra';
+import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
+import { Camera } from 'molstar/lib/mol-canvas3d/camera';
+import { DebugUtil } from './debug-utils';
+
+
+let initialSnapshot: Camera.Snapshot;
+
+export function storeCameraSnapshot(plugin: PluginUIContext): void {
+    if (!initialSnapshot) {
+        initialSnapshot = plugin.canvas3d!.camera.getSnapshot();
+        DebugUtil.log('initialSnapshot stored:', initialSnapshot);
+    }
+}
+
+export function loadInitialSnapshot(plugin: PluginUIContext): void {
+    if (!initialSnapshot) {
+        DebugUtil.log('initialSnapshot is undefined');
+    } else {
+        DebugUtil.log('Loading initial snapshot:', initialSnapshot);
+        PluginCommands.Camera.Reset(plugin, { snapshot: initialSnapshot });
+    }
+}
+
+export async function rotateCamera(plugin: PluginUIContext) {
+    function rot90q(v: Vec3, axis: Vec3 = Vec3.create(1, 0, 0)): Vec3 {
+        const q = Quat.setAxisAngle(Quat(), axis, -Math.PI/2);
+        return Vec3.transformQuat(Vec3(), v, q);
+    }
+    function sub(v: Vec3, u: Vec3): Vec3 {
+        return Vec3.sub(Vec3(), v, u);
+    }
+    function add(v: Vec3, u: Vec3): Vec3 {
+        return Vec3.add(Vec3(), v, u);
+    }
+
+    if (!plugin.canvas3d) {
+        return;
+    }
+
+    const cam = plugin.canvas3d!.camera;
+    const snapshot = cam.getSnapshot();
+    const newSnapshot = {
+        ...snapshot,
+        // target + rotateBy90(postition - target)
+        position: add(snapshot.target, rot90q(sub(snapshot.position, snapshot.target))),
+        target: snapshot.target,
+        up: Vec3.negUnitZ
+    };
+    const duration = 100;
+    PluginCommands.Camera.Reset(plugin, { snapshot: newSnapshot, durationMs: duration }).then(() => {
+        setTimeout(()=> {
+            requestAnimationFrame(() => plugin.canvas3d?.requestCameraReset());
+        }, duration + 50); // The 2nd reset needs some delay
+    });
+
+}

+ 177 - 0
src/tmdet-extension/debug-utils.ts

@@ -0,0 +1,177 @@
+/**
+ * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+ *
+ * Licensed under CC BY-NC 4.0, see LICENSE file for more info.
+ *
+ * @author Gabor Tusnady <tusnady.gabor@ttk.hu>
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+
+import { mmCIF_Database } from "molstar/lib/mol-io/reader/cif/schema/mmcif";
+import { Mat4, Vec3 } from "molstar/lib/mol-math/linear-algebra";
+import { MmcifFormat } from "molstar/lib/mol-model-formats/structure/mmcif";
+import { Model } from "molstar/lib/mol-model/structure";
+import { AtomicConformation } from "molstar/lib/mol-model/structure/model/properties/atomic";
+import { createStructureRepresentationParams } from "molstar/lib/mol-plugin-state/helpers/structure-representation-params";
+import { StateTransforms } from "molstar/lib/mol-plugin-state/transforms";
+import { PluginUIContext } from "molstar/lib/mol-plugin-ui/context";
+import { Expression } from "molstar/lib/mol-script/language/expression";
+import { Color } from "molstar/lib/mol-util/color";
+import { rotateCamera as RC } from "./camera";
+import { TmDetDescriptorCache } from "./prop";
+import { getChainExpression as GCE, getCurrentHierarchy as GCH, transformationForStateTransform, transformWholeModel } from "./transformation";
+import { PDBTMTransformationMatrix } from "./types";
+
+export namespace DebugUtil {
+    let plugin: PluginUIContext;
+    let logEnabled = false;
+
+    export function init(ctx: PluginUIContext) {
+        plugin = ctx;
+    }
+
+    //
+    // logging
+    //
+    export function enableLog() {
+        logEnabled = true;
+    }
+
+    export function disableLog() {
+        logEnabled = false;
+    }
+
+    export function log(...args: any[]) {
+        if (logEnabled) {
+            console.log(...args);
+        }
+    }
+
+    //
+    //
+    // lin.alg.
+    //
+
+    export function transformVector(v: Vec3, matrix: Mat4): Vec3 {
+        const result = Vec3.transformMat4(Vec3(), v, matrix);
+        log("transformVector: Input v & matrix:", v, Mat4.makeTable(matrix));
+        log("transformVector: Result vector:", result);
+        return result;
+    }
+
+    export function descriptorMxToMat4(matrix: PDBTMTransformationMatrix): Mat4 {
+        log("descriptorMxToMat4: Input:", matrix);
+        const result = transformationForStateTransform(matrix);
+        log("descriptorMxToMat4: Result:", result);
+        return result;
+    }
+
+
+    //
+    // structure utils
+    //
+
+    export const getChainExpression = GCE;
+
+    export function getCellOfFirstStructure() {
+        return getCell();
+    }
+
+    export function getCell(structureIndex: number = 0, modelIndex: number = 0) {
+        return getCurrentHierarchy().models[structureIndex].structures[modelIndex].cell;
+    }
+
+    export function getCurrentHierarchy() {
+        return GCH(plugin);
+    }
+
+    export function getModelOfFirstStructure() {
+        return getCellOfFirstStructure().obj?.data.model;
+    }
+
+    // in case of mmCIF format
+    export function dumpAtomProperties(authAtomId: number) {
+        const model: Model|undefined = getModelOfFirstStructure();
+        log("First model:", model);
+        if(!model) {
+            return;
+        }
+
+        const rowCount = model.atomicHierarchy.atoms._rowCount;
+        const conformation: AtomicConformation = model.atomicConformation;
+        const db: mmCIF_Database = (model.sourceData as MmcifFormat).data.db;
+        const atom_site = db.atom_site;
+        for (let index = 0; index < rowCount; index++) {
+            if (conformation.atomId.value(index) == authAtomId) {
+                log("Model Atom Conformation:", {
+                    atomIdParameter: authAtomId,
+                    atomId: conformation.atomId.value(index),
+                    coords: [
+                        conformation.x[index],
+                        conformation.y[index],
+                        conformation.z[index]
+                    ],
+                    xyzDefined: conformation.xyzDefined
+                });
+
+                log("Atom Source Data (mmCIF database):", {
+                    authId: atom_site.auth_atom_id.value(index),
+                    compId: atom_site.auth_comp_id.value(index),
+                    authAsymId: atom_site.auth_asym_id.value(index),
+                    authSeqId: atom_site.auth_seq_id.value(index),
+                    coords: [
+                        atom_site.Cartn_x.value(index),
+                        atom_site.Cartn_y.value(index),
+                        atom_site.Cartn_z.value(index)
+                    ]
+                });
+            }
+        }
+    }
+
+
+    export function getCachesOfEachStructure() {
+        let sIndex = 0;
+        getCurrentHierarchy().structures.forEach(struct => {
+            let cIndex = 0;
+            struct.components.forEach(component => {
+                log(`struct: ${sIndex}; component: ${cIndex}`, component.cell.cache);
+                cIndex++;
+            });
+            sIndex++;
+        });
+    }
+
+    export function color(expression: Expression, color: number[], structureIndex: number = 0, modelIndex: number = 0) {
+        const structure = getCell(structureIndex, modelIndex);
+        const stateBuilder = plugin.build().to(structure);
+        stateBuilder.apply(
+            StateTransforms.Model.StructureSelectionFromExpression,
+            { expression: expression }
+        )
+        .apply(
+            StateTransforms.Representation.StructureRepresentation3D,
+            createStructureRepresentationParams(plugin, structure.obj?.data, {
+                type: 'ball-and-stick',
+                color: 'uniform', colorParams: { value: Color.fromArray(color, 0) }
+            })
+        );
+        stateBuilder.commit();
+    }
+
+    export function transform(entityId: string) {
+        const tmx = TmDetDescriptorCache.get(entityId)?.additional_entry_annotations.membrane.transformation_matrix;
+        transformWholeModel(plugin, tmx!);
+    }
+
+    //
+    // Camera
+    //
+    export function rotateCamera() {
+        RC(plugin);
+    }
+
+    export function requestCameraReset() {
+        requestAnimationFrame(() => plugin.canvas3d?.requestCameraReset({ durationMs: 1000 }));
+    }
+}

+ 98 - 0
src/tmdet-extension/labeling.ts

@@ -0,0 +1,98 @@
+/**
+ * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+ *
+ * Licensed under CC BY-NC 4.0, see LICENSE file for more info.
+ *
+ * @author Gabor Tusnady <tusnady.gabor@ttk.hu>
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+
+import { Loci } from 'molstar/lib/mol-model/loci';
+import { StructureElement } from 'molstar/lib/mol-model/structure';
+import { LociLabel, LociLabelProvider } from 'molstar/lib/mol-plugin-state/manager/loci-label';
+import { TmDetChainListCache, TmDetDescriptorCache } from './prop';
+import { createResidueListsPerChain, getChainAndResidueIds } from './tmdet-color-theme';
+import { ChainList, getResidue, ResidueItem } from './types';
+
+const siteLabels = [
+    "Side1",
+    "Side2",
+    "TM alpha",
+    "TM beta",
+    "TM re-entrant loop",
+    "Interfacial Helix",
+    "Unknown localization",
+    "Membrane Inside"
+];
+const DefaultResidueLabel = 6; // Unknown localization
+
+
+export const TmDetLabelProvider: LociLabelProvider = {
+    label: (loci: Loci): LociLabel => {
+        let labelText = siteLabels[DefaultResidueLabel];
+
+        if (loci.kind == 'element-loci') {
+            const unit = loci.elements[0].unit;
+            const pdbId = unit.model.entryId;
+
+            const descriptor = TmDetDescriptorCache.get(pdbId);
+            if (!descriptor) {
+                return labelText;
+            }
+
+            let chainList =  TmDetChainListCache.get(pdbId);
+            if (!chainList) {
+                const tmType = descriptor.additional_entry_annotations.tm_type;
+                chainList = createResidueListsPerChain(descriptor.chains, descriptor.side1, tmType);
+                TmDetChainListCache.set(pdbId, chainList);
+            }
+
+            const location = StructureElement.Loci.getFirstLocation(loci);
+            const { chainId, residueId } = getChainAndResidueIds(location!);
+            const residue = getResidue(chainList, chainId!, residueId!);
+            if (residue) {
+                labelText = siteLabels[residue?.siteId];
+                let regionText = getRegionText(chainList, chainId!, residue);
+                labelText = `${labelText}: ${regionText}`
+            }
+
+        }
+        return labelText;
+    }
+}
+
+function getRegionText(chainList: ChainList, chainId: string, residue: ResidueItem): string {
+    let value = "Unknown region range";
+
+    const chain = chainList.filter((chain) => chain.chainId === chainId)[0];
+    if (chain) {
+        // find start of region
+        const residues = chain.residues;
+        const  authId = parseInt(residue.authId!)
+        let previous = residues[residues.length-1];
+        for (let i = residues.length-1; i > 0; i--) {
+            const current = residues[i];
+            const currentId = parseInt(current.authId!);
+            // cancel loop when siteId changes
+            if (currentId < authId && current.siteId != residue.siteId) {
+                break;
+            }
+            previous = current;
+        }
+        value = `[ ${previous.authId}`;
+
+        // find end of region
+        previous = residues[0];
+        for (let current of residues) {
+            const currentId = parseInt(current.authId!);
+            // cancel loop when siteId changes
+            if (authId < currentId && current.siteId !== residue.siteId) {
+                break;
+            }
+            previous = current;
+        }
+        value = `${value}-${previous.authId} ]`;
+    }
+
+    return value;
+}

+ 139 - 0
src/tmdet-extension/prop.ts

@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2018-2020 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>
+ */
+/**
+ * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+ *
+ * Licensed under CC BY-NC 4.0, see LICENSE file for more info.
+ *
+ * @author Gabor Tusnady <tusnady.gabor@ttk.hu>
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { Structure, StructureProperties, Unit } from 'molstar/lib/mol-model/structure';
+import { CustomPropertyDescriptor } from 'molstar/lib/mol-model/custom-property';
+import { isInMembranePlane, TMDETParams } from './algorithm';
+import { CustomStructureProperty } from 'molstar/lib/mol-model-props/common/custom-structure-property';
+import { CustomProperty } from 'molstar/lib/mol-model-props/common/custom-property';
+import { Vec3 } from 'molstar/lib/mol-math/linear-algebra';
+import { QuerySymbolRuntime } from 'molstar/lib/mol-script/runtime/query/base';
+import { CustomPropSymbol } from 'molstar/lib/mol-script/language/symbol';
+import { Type } from 'molstar/lib/mol-script/language/type';
+import { membraneOrientation } from './behavior';
+import { ChainList, PDBTMDescriptor } from './types';
+
+export const MembraneOrientationParams = {
+    ...TMDETParams
+};
+export type MembraneOrientationParams = typeof MembraneOrientationParams
+export type MembraneOrientationProps = PD.Values<MembraneOrientationParams>
+
+export { MembraneOrientation };
+
+/**
+ * Simple storage to made PDBTM descriptor available globally.
+ *
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+class DescriptorCache {
+    private map: Map<string, PDBTMDescriptor>;
+
+    constructor() {
+        this.map = new Map<string, PDBTMDescriptor>();
+    }
+
+    add(descriptor: PDBTMDescriptor) {
+        const key = descriptor.pdb_id.toLowerCase();
+        if (!this.map.has(key)) {
+            this.map.set(key, descriptor);
+        }
+    }
+
+    get(key: string) {
+        key = key.toLowerCase();
+        return this.map.get(key);
+    }
+
+}
+export const TmDetDescriptorCache = new DescriptorCache();
+
+class ChainListCache {
+    private map: Map<string, ChainList>;
+
+    constructor() {
+        this.map = new Map<string, ChainList>();
+    }
+
+    set(key: string, chains: ChainList) {
+        key = key.toLowerCase();
+        if (!this.map.has(key)) {
+            this.map.set(key, chains);
+        }
+    }
+
+    get(key: string) {
+        key = key.toLowerCase();
+        return this.map.get(key);
+    }
+}
+export const TmDetChainListCache = new ChainListCache();
+
+interface MembraneOrientation {
+    // point in membrane boundary
+    readonly planePoint1: Vec3,
+    // point in opposite side of membrane boundary
+    readonly planePoint2: Vec3,
+    // normal vector of membrane layer
+    readonly normalVector: Vec3,
+    // the radius of the membrane layer
+    readonly radius: number,
+    readonly centroid: Vec3
+}
+
+namespace MembraneOrientation {
+    export enum Tag {
+        Representation = 'tmdet-membrane-orientation-3d'
+    }
+
+    const pos = Vec3();
+    export const symbols = {
+        isTransmembrane: QuerySymbolRuntime.Dynamic(CustomPropSymbol('computed', 'membrane-orientation.is-transmembrane', Type.Bool),
+            ctx => {
+                const { unit, structure } = ctx.element;
+                const { x, y, z } = StructureProperties.atom;
+                if (!Unit.isAtomic(unit)) return 0;
+                const membraneOrientation = MembraneOrientationProvider.get(structure).value;
+                if (!membraneOrientation) return 0;
+                Vec3.set(pos, x(ctx.element), y(ctx.element), z(ctx.element));
+                const { normalVector, planePoint1, planePoint2 } = membraneOrientation;
+                return isInMembranePlane(pos, normalVector, planePoint1, planePoint2);
+            })
+    };
+}
+
+export const MembraneOrientationProvider: CustomStructureProperty.Provider<MembraneOrientationParams, MembraneOrientation> = CustomStructureProperty.createProvider({
+    label: 'TMDET Membrane Orientation Provider',
+    descriptor: CustomPropertyDescriptor({
+        name: 'tmdet_computed_membrane_orientation',
+        symbols: MembraneOrientation.symbols,
+        // TODO `cifExport`
+    }),
+    type: 'root',
+    defaultParams: MembraneOrientationParams,
+//    getParams: (data: Structure) => MembraneOrientationParams,
+    getParams: function(data: Structure) {
+        //DebugUtil.log('getParams:: DEBUG', MembraneOrientationParams);
+        return MembraneOrientationParams;
+    },
+    isApplicable: (data: Structure) => true,
+    obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<MembraneOrientationProps>) => {
+        //DebugUtil.log('obtain:: DEBUG', data.customPropertyDescriptors);
+        let result = membraneOrientation;
+        //DebugUtil.log("RESULT of 'obtain:'", result);
+        return { value: result };
+    }
+});

+ 150 - 0
src/tmdet-extension/representation.ts

@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2018-2020 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 { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { Vec3, Mat4 } from 'molstar/lib/mol-math/linear-algebra';
+import { Representation, RepresentationContext, RepresentationParamsGetter } from 'molstar/lib/mol-repr/representation';
+import { Structure } from 'molstar/lib/mol-model/structure';
+import { StructureRepresentationProvider, StructureRepresentation, StructureRepresentationStateBuilder } from 'molstar/lib/mol-repr/structure/representation';
+import { MembraneOrientation } from './prop';
+import { ThemeRegistryContext } from 'molstar/lib/mol-theme/theme';
+import { ShapeRepresentation } from 'molstar/lib/mol-repr/shape/representation';
+import { Shape } from 'molstar/lib/mol-model/shape';
+import { RuntimeContext } from 'molstar/lib/mol-task';
+import { Lines } from 'molstar/lib/mol-geo/geometry/lines/lines';
+import { Mesh } from 'molstar/lib/mol-geo/geometry/mesh/mesh';
+import { LinesBuilder } from 'molstar/lib/mol-geo/geometry/lines/lines-builder';
+import { Circle } from 'molstar/lib/mol-geo/primitive/circle';
+import { transformPrimitive } from 'molstar/lib/mol-geo/primitive/primitive';
+import { MeshBuilder } from 'molstar/lib/mol-geo/geometry/mesh/mesh-builder';
+import { MembraneOrientationProvider } from './prop';
+import { MarkerActions } from 'molstar/lib/mol-util/marker-action';
+import { lociLabel } from 'molstar/lib/mol-theme/label';
+import { ColorNames } from 'molstar/lib/mol-util/color/names';
+import { CustomProperty } from 'molstar/lib/mol-model-props/common/custom-property';
+
+const SharedParams = {
+    color: PD.Color(ColorNames.lightgrey),
+    radiusFactor: PD.Numeric(1.2, { min: 0.1, max: 3.0, step: 0.01 }, { description: 'Scale the radius of the membrane layer' })
+};
+
+const BilayerPlanesParams = {
+    ...Mesh.Params,
+    ...SharedParams,
+    sectorOpacity: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
+};
+export type BilayerPlanesParams = typeof BilayerPlanesParams
+export type BilayerPlanesProps = PD.Values<BilayerPlanesParams>
+
+const BilayerRimsParams = {
+    ...Lines.Params,
+    ...SharedParams,
+    lineSizeAttenuation: PD.Boolean(true),
+    linesSize: PD.Numeric(0.3, { min: 0.01, max: 50, step: 0.01 }),
+    dashedLines: PD.Boolean(true),
+};
+export type BilayerRimsParams = typeof BilayerRimsParams
+export type BilayerRimsProps = PD.Values<BilayerRimsParams>
+
+const MembraneOrientationVisuals = {
+    'bilayer-planes': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneOrientation, BilayerPlanesParams>) => ShapeRepresentation(getBilayerPlanes, Mesh.Utils, { modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }), modifyProps: p => ({ ...p, alpha: p.sectorOpacity, ignoreLight: true, doubleSided: false }) }),
+    'bilayer-rims': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneOrientation, BilayerRimsParams>) => ShapeRepresentation(getBilayerRims, Lines.Utils, { modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }) })
+};
+
+export const MembraneOrientationParams = {
+    ...BilayerPlanesParams,
+    ...BilayerRimsParams,
+    visuals: PD.MultiSelect(['bilayer-planes', 'bilayer-rims'], PD.objectToOptions(MembraneOrientationVisuals)),
+};
+export type MembraneOrientationParams = typeof MembraneOrientationParams
+export type MembraneOrientationProps = PD.Values<MembraneOrientationParams>
+
+export function getMembraneOrientationParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(MembraneOrientationParams);
+}
+
+export type MembraneOrientationRepresentation = StructureRepresentation<MembraneOrientationParams>
+export function MembraneOrientationRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MembraneOrientationParams>): MembraneOrientationRepresentation {
+    return Representation.createMulti('Membrane Orientation', ctx, getParams, StructureRepresentationStateBuilder, MembraneOrientationVisuals as unknown as Representation.Def<Structure, MembraneOrientationParams>);
+}
+
+export const MembraneOrientationRepresentationProvider = StructureRepresentationProvider({
+    name: 'tmdet-membrane-orientation',
+    label: 'Membrane Orientation',
+    description: 'Displays a grid of points representing membrane layers.',
+    factory: MembraneOrientationRepresentation,
+    getParams: getMembraneOrientationParams,
+    defaultValues: PD.getDefaultValues(MembraneOrientationParams),
+    defaultColorTheme: { name: 'shape-group' },
+    defaultSizeTheme: { name: 'shape-group' },
+    isApplicable: (structure: Structure) => structure.elementCount > 0,
+    ensureCustomProperties: {
+        attach: (ctx: CustomProperty.Context, structure: Structure) => MembraneOrientationProvider.attach(ctx, structure, void 0, true),
+        detach: (data) => MembraneOrientationProvider.ref(data, false)
+    }
+});
+
+function membraneLabel(data: Structure) {
+    return `${lociLabel(Structure.Loci(data))} | Membrane Orientation`;
+}
+
+function getBilayerRims(ctx: RuntimeContext, data: Structure, props: BilayerRimsProps, shape?: Shape<Lines>): Shape<Lines> {
+    const { planePoint1: p1, planePoint2: p2, centroid, radius } = MembraneOrientationProvider.get(data).value!;
+    const scaledRadius = props.radiusFactor * radius;
+    const builder = LinesBuilder.create(128, 64, shape?.geometry);
+    getLayerCircle(builder, p1, centroid, scaledRadius, props);
+    getLayerCircle(builder, p2, centroid, scaledRadius, props);
+    return Shape.create('Bilayer rims', data, builder.getLines(), () => props.color, () => props.linesSize, () => membraneLabel(data));
+}
+
+function getLayerCircle(builder: LinesBuilder, p: Vec3, centroid: Vec3, radius: number, props: BilayerRimsProps, shape?: Shape<Lines>) {
+    const circle = getCircle(p, centroid, radius);
+    const { indices, vertices } = circle;
+    for (let j = 0, jl = indices.length; j < jl; j += 3) {
+        if (props.dashedLines && j % 2 === 1) continue; // draw every other segment to get dashes
+        const start = indices[j] * 3;
+        const end = indices[j + 1] * 3;
+        const startX = vertices[start];
+        const startY = vertices[start + 1];
+        const startZ = vertices[start + 2];
+        const endX = vertices[end];
+        const endY = vertices[end + 1];
+        const endZ = vertices[end + 2];
+        builder.add(startX, startY, startZ, endX, endY, endZ, 0);
+    }
+}
+
+const tmpMat = Mat4();
+const tmpV = Vec3();
+function getCircle(p: Vec3, centroid: Vec3, radius: number) {
+    if (Vec3.dot(Vec3.unitY, Vec3.sub(tmpV, p, centroid)) === 0) {
+        Mat4.targetTo(tmpMat, p, centroid, Vec3.unitY);
+    } else {
+        Mat4.targetTo(tmpMat, p, centroid, Vec3.unitX);
+    }
+    Mat4.setTranslation(tmpMat, p);
+    Mat4.mul(tmpMat, tmpMat, Mat4.rotX90);
+
+    const circle = Circle({ radius, segments: 64 });
+    return transformPrimitive(circle, tmpMat);
+}
+
+function getBilayerPlanes(ctx: RuntimeContext, data: Structure, props: BilayerPlanesProps, shape?: Shape<Mesh>): Shape<Mesh> {
+    const { planePoint1: p1, planePoint2: p2, centroid, radius } = MembraneOrientationProvider.get(data).value!;
+    const state = MeshBuilder.createState(128, 64, shape && shape.geometry);
+    const scaledRadius = props.radiusFactor * radius;
+    getLayerPlane(state, p1, centroid, scaledRadius);
+    getLayerPlane(state, p2, centroid, scaledRadius);
+    return Shape.create('Bilayer planes', data, MeshBuilder.getMesh(state), () => props.color, () => 1, () => membraneLabel(data));
+}
+
+function getLayerPlane(state: MeshBuilder.State, p: Vec3, centroid: Vec3, radius: number) {
+    const circle = getCircle(p, centroid, radius);
+    state.currentGroup = 0;
+    MeshBuilder.addPrimitive(state, Mat4.id, circle);
+    MeshBuilder.addPrimitiveFlipped(state, Mat4.id, circle);
+}

+ 158 - 0
src/tmdet-extension/symmetry.ts

@@ -0,0 +1,158 @@
+/**
+ * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+ *
+ * Licensed under CC BY-NC 4.0, see LICENSE file for more info.
+ *
+ * @author Gabor Tusnady <tusnady.gabor@ttk.hu>
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+
+import { MmcifFormat } from 'molstar/lib/mol-model-formats/structure/mmcif';
+import { Column, Table } from 'molstar/lib/mol-data/db';
+import { mmCIF_Schema } from 'molstar/lib/mol-io/reader/cif/schema/mmcif';
+import { Model } from 'molstar/lib/mol-model/structure';
+import { ModelSymmetry } from 'molstar/lib/mol-model-formats/structure/property/symmetry';
+import { PDBTMDescriptor } from './types';
+import { DebugUtil } from './debug-utils';
+
+
+export function registerTmDetSymmetry(pdbtmDescriptor: PDBTMDescriptor) {
+    ModelSymmetry.Provider.formatRegistry.remove('mmCIF');
+    const excludedChains = constructChainListFromOperations(pdbtmDescriptor);
+    ModelSymmetry.Provider.formatRegistry.add('mmCIF',  function(model: Model) {
+        return tmDetSymmetryFromMmCif(model, excludedChains);
+    });
+}
+
+function constructChainListFromOperations(pdbtmDescriptor: PDBTMDescriptor): string[] {
+    const excludedChains: string[] = [];
+    // add chain deletes
+    const biomatrix = pdbtmDescriptor.additional_entry_annotations.biomatrix;
+    if (biomatrix?.chain_deletes) {
+        biomatrix.chain_deletes.forEach(
+            chainId => excludedChains.push(chainId)
+        );
+    }
+    // exclude result of transformations
+    if (biomatrix?.matrix_list) {
+        biomatrix.matrix_list.forEach(
+            matrix => matrix.apply_to_chain_list.forEach(
+                applyItem => excludedChains.push(applyItem.new_chain_id)
+            )
+        );
+    }
+
+    return excludedChains;
+}
+
+function tmDetSymmetryFromMmCif(model: Model, excludedChains: string[]) {
+    if (!MmcifFormat.is(model.sourceData)) return;
+
+    let data = model.sourceData.data.db;
+    excludedChains = union(
+        excludedChains,
+        Array.from(data.pdbx_nonpoly_scheme.asym_id.toArray())
+    );
+
+    const updated_pdbx_struct_assembly_gen = createPdbxStructAssemblyGen(
+        data.pdbx_struct_assembly_gen,
+        excludedChains
+    );
+    DebugUtil.log('Non-poly entities:', Table.formatToString(data.pdbx_entity_nonpoly));
+    DebugUtil.log('Non-poly chains:', data.pdbx_nonpoly_scheme.asym_id.toArray());
+
+    const only_identity_operation = createPdbxStructOperList(data.pdbx_struct_oper_list);
+
+    return ModelSymmetry.fromData({
+        symmetry: data.symmetry,
+        cell: data.cell,
+        struct_ncs_oper: data.struct_ncs_oper,
+        atom_sites: data.atom_sites,
+        pdbx_struct_assembly: data.pdbx_struct_assembly,
+        pdbx_struct_assembly_gen: updated_pdbx_struct_assembly_gen,
+        pdbx_struct_oper_list: only_identity_operation
+    });
+}
+
+function createPdbxStructAssemblyGen(pdbx_struct_assembly_gen: Table<mmCIF_Schema['pdbx_struct_assembly_gen']>,
+    excludedChains: string[]): Table<mmCIF_Schema['pdbx_struct_assembly_gen']> {
+
+    const asym_id_list_column = createAsymIdColumn(
+        pdbx_struct_assembly_gen, excludedChains
+    );
+
+    // create table with new column
+    let updated_pdbx_struct_assembly_gen = Table.ofColumns(
+        pdbx_struct_assembly_gen._schema,
+        {
+            assembly_id: Column.ofStringArray([ '1' ]),
+            asym_id_list: asym_id_list_column,
+            //oper_expression: data.pdbx_struct_assembly_gen.oper_expression
+            // NOTE: we expect here pdbx_struct_assembly_gen has only one row
+            oper_expression: Column.ofStringArray([ '1' ])
+        }
+    );
+    DebugUtil.log('Orig. assembly_gen', Table.formatToString(pdbx_struct_assembly_gen));
+    DebugUtil.log('Updated assembly_gen', Table.formatToString(updated_pdbx_struct_assembly_gen));
+
+    return updated_pdbx_struct_assembly_gen;
+}
+
+function createPdbxStructOperList(pdbx_struct_oper_list: Table<mmCIF_Schema['pdbx_struct_oper_list']>):
+    Table<mmCIF_Schema['pdbx_struct_oper_list']> {
+
+    let updated_pdbx_struct_oper_list = Table.ofColumns(
+        pdbx_struct_oper_list._schema,
+        {
+            id: Column.ofStringArray([ '1' ]),
+            type: Column.ofArray({
+                array: [ 'identity operation' ],
+                schema: pdbx_struct_oper_list.type.schema
+            }),
+            name: Column.ofStringArray([ '1_555' ]),
+            symmetry_operation: Column.ofStringArray([ 'x,y,z' ]),
+            matrix:  Column.ofArray({
+                array: [[ 1, 0, 0, 0, 1, 0, 0, 0, 1 ]],
+                schema: pdbx_struct_oper_list.matrix.schema
+            }),
+            vector: Column.ofArray({
+                array: [ [ 0, 0, 0 ] ],
+                schema: pdbx_struct_oper_list.vector.schema
+            })
+        }
+    );
+
+    DebugUtil.log('Orig. pdbx_struct_oper_list', Table.formatToString(pdbx_struct_oper_list));
+    DebugUtil.log('Updated pdbx_struct_oper_list', Table.formatToString(updated_pdbx_struct_oper_list));
+
+    return updated_pdbx_struct_oper_list;
+}
+
+function createAsymIdColumn(pdbx_struct_assembly_gen: Table<mmCIF_Schema['pdbx_struct_assembly_gen']>,
+    excludedChains: string[]) {
+
+    let asym_id_list: string[] = [];
+    for (let i = 0; i < pdbx_struct_assembly_gen._rowCount; i++) {
+        const currentAsymIdList = pdbx_struct_assembly_gen.asym_id_list.value(i);
+        asym_id_list = asym_id_list.concat(currentAsymIdList);
+    }
+    asym_id_list = minus(asym_id_list, excludedChains);
+    DebugUtil.log('Excluded chains:', excludedChains);
+    DebugUtil.log('Included chains:', asym_id_list);
+
+    return Column.ofStringListArray([ asym_id_list ]);
+}
+
+// difference of two string arrays (interpreted as sets)
+function minus(a: string[], b: string[]): string[] {
+    const b_set = new Set(b);
+    const difference = a.filter(x => !b_set.has(x));
+    return Array.from(new Set(difference).values());
+}
+
+// union of two string arrays (interpreted as sets)
+function union(a: string[], b: string[]): string[] {
+    const a_set = new Set(a);
+    b.forEach(item => a_set.add(item));
+    return Array.from(a_set.values());
+}

+ 258 - 0
src/tmdet-extension/tmdet-color-theme.ts

@@ -0,0 +1,258 @@
+/**
+ * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+ *
+ * Licensed under CC BY-NC 4.0, see LICENSE file for more info.
+ *
+ * @author Gabor Tusnady <tusnady.gabor@ttk.hu>
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+
+import { StructureElement, Unit } from 'molstar/lib/mol-model/structure';
+import { ColorTheme } from 'molstar/lib/mol-theme/color';
+import { ThemeDataContext } from 'molstar/lib/mol-theme/theme';
+import { Color } from 'molstar/lib/mol-util/color';
+import { ColorNames } from 'molstar/lib/mol-util/color/names';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { Location } from 'molstar/lib/mol-model/location';
+import { ChainList, getResidue, PDBTMChain } from './types';
+import { TmDetChainListCache, TmDetDescriptorCache } from './prop';
+
+
+export type TmDetColorThemeParams = {
+    pdbtmDescriptor: any
+};
+
+export function TmDetColorTheme(
+    ctx: ThemeDataContext,
+    props: PD.Values<TmDetColorThemeParams>
+): ColorTheme<TmDetColorThemeParams> {
+    let descriptorChains: PDBTMChain[] = [];
+    let pdbtmDescriptor = props.pdbtmDescriptor;
+    const pdbId = ctx.structure?.model.entryId;
+
+    // If it is not given as parameter,
+    // but there is one in the cache for this structure.
+    if (!pdbtmDescriptor) {
+        if (pdbId) {
+            pdbtmDescriptor = TmDetDescriptorCache.get(pdbId);
+        }
+    }
+    if (pdbtmDescriptor) {
+        descriptorChains = pdbtmDescriptor.chains;
+    }
+
+    const tmType = pdbtmDescriptor.additional_entry_annotations.tm_type;
+    const chainList = createResidueListsPerChain(descriptorChains, pdbtmDescriptor.side1, tmType);
+    if (pdbId) {
+        TmDetChainListCache.set(pdbId, chainList);
+    }
+
+    return {
+        factory: TmDetColorTheme,
+        granularity: 'group', //'residue' as any,
+        color: (location: Location) => getColor(location, chainList),
+        props: props,
+        description: 'TMDet...',
+    };
+}
+
+const DefaultResidueColor = ColorNames.lightgrey;
+
+enum SiteIndexes {
+    Side1 = 0,
+    Side2 = 1,
+    TmAlpha = 2,
+    TmBeta = 3,
+    TmReentrantLoop = 4,
+    InterfacialHelix = 5,
+    UnknownLocalization = 6,
+    MembraneInside = 7,
+    Periplasm = 8
+};
+
+// Old default values - it is overwritten by ult_* CSS classes
+// See below updateSiteColors().
+const siteColors = [
+    Color.fromArray([255,100,100], 0), // Side1
+    Color.fromArray([100,100,255], 0), // Side2
+    Color.fromArray([255,255,  0], 0), // TM alpha
+    Color.fromArray([255,255,  0], 0), // TM beta
+    Color.fromArray([255,127,  0], 0), // TM re-entrant loop
+    Color.fromArray([0,255,    0], 0), // Interfacial Helix
+    Color.fromArray([196,196,196], 0), // Unknow localization
+    Color.fromArray([0,255,    0], 0), // Membrane Inside
+    Color.fromArray([255, 0, 255], 0)  // Periplasm
+];
+
+const siteCssNames = [
+    "ult_side1",
+    "ult_side2",
+    "ult_alpha",
+    "ult_beta",
+    "ult_reentrant",
+    "ult_ifh",
+    "ult_unknown",
+    "ult_membins",
+    "ult_periplasm"
+];
+
+const regionColorMapFromCss = new Map();
+
+// set default values
+siteCssNames.forEach((className, index) => {
+    regionColorMapFromCss.set(className, siteColors[index]);
+});
+
+function getColor(location: Location, chains: ChainList): Color {
+    let color = DefaultResidueColor;
+
+    // TODO: How to solve cases when chain operation occurs?
+    if (StructureElement.Location.is(location) && Unit.isAtomic(location.unit)) {
+
+        const { chainId, residueId } = getChainAndResidueIds(location);
+        color = residueColor(chains, chainId!, residueId!);
+    }
+    return color;
+}
+
+export function getChainAndResidueIds(location: Location) {
+    let chainId = undefined;
+    let residueId = undefined;
+
+    if (StructureElement.Location.is(location) && Unit.isAtomic(location.unit)) {
+        const atomicHierarchy = location.unit.model.atomicHierarchy;
+        const residueIdx = StructureElement.residueIndex(location);
+        const chainIdx = StructureElement.Location.chainIndex(location);
+
+        residueId = atomicHierarchy.residues.label_seq_id.value(residueIdx).toString();
+        chainId = atomicHierarchy.chains.label_asym_id.value(chainIdx).toString();
+    }
+    return {
+        chainId: chainId,
+        residueId: residueId
+    };
+}
+
+export function createResidueListsPerChain(chains: PDBTMChain[], side1: string|null, tmType: string) {
+
+    const chainList: ChainList = [];
+    const hasBeta = chains.some(chain => chain.additional_chain_annotations.type == "beta");
+
+    chains.forEach((chain: PDBTMChain) => {
+        const chainType = chain.additional_chain_annotations.type;
+        chainList.push({ chainId: chain.chain_label, type: chainType, residues: [] });
+        let annotationErrorLogged = false;
+        chain.residues.forEach((residue) => {
+            let siteId = residue.site_data![0].site_id_ref - 1;
+            let siteColorId = siteId;
+            if (tmType == "Tm_Alpha" && hasBeta && side1 == "Periplasm") {
+                if (siteId == SiteIndexes.Side1) {
+                    siteColorId = SiteIndexes.Periplasm;
+                } else if (siteId == SiteIndexes.Side2) {
+                    if (chainType == "non_tm" || chainType == "alpha") {
+                        siteColorId = SiteIndexes.Side1;
+                    } else if (chainType == "beta") {
+                        siteColorId = SiteIndexes.Side2;
+                    }
+                }
+            } else if (chainType == "beta") {
+                if (side1 == "Periplasm" && siteColorId == SiteIndexes.Side1
+                    || side1 == "Outside" && siteColorId == SiteIndexes.Side2) {
+                    siteColorId = SiteIndexes.Periplasm;
+                } else if (!annotationErrorLogged && side1 == "Inside" && siteColorId == SiteIndexes.Side1) {
+                    console.error(`Annotation error: beta chain has inside region ${chain.chain_label}:${residue.pdb_res_label}`);
+                    annotationErrorLogged = true;
+                }
+            }
+
+            chainList[chainList.length - 1].residues.push({
+                authId: residue.pdb_res_label,
+                siteId: siteId,
+                siteColorId: siteColorId
+            });
+        });
+    });
+
+    return chainList;
+}
+
+function residueColor(chains: ChainList, chainId: string, residueId: string): Color {
+    let color = DefaultResidueColor;
+
+    const residue = getResidue(chains, chainId, residueId);
+    if (residue) {
+        color = siteColors[residue.siteColorId];
+    }
+
+    return color;
+}
+
+// Provider
+
+export const TmDetColorThemeProvider: ColorTheme.Provider<TmDetColorThemeParams, 'tmdet-custom-color-theme'> = {
+    name: 'tmdet-custom-color-theme',
+    label: 'TMDet Topology Theme',
+    category: 'TMDet',
+    factory: TmDetColorTheme,
+    getParams: () => ({ pdbtmDescriptor: { isOptional: true } }),
+    defaultValues: { pdbtmDescriptor: undefined },
+    isApplicable: () => true,
+};
+
+
+
+// Colors from CSS rules
+
+function loadRegionColorsFromStyleSheets(prefix: string = 'ult_'): void {
+    const sheets: CSSStyleSheet[] = Array.from(document.styleSheets);
+    sheets.forEach((sheet: CSSStyleSheet) => {
+        const rules: CSSRule[] = Array.from(sheet.cssRules);
+        rules.forEach((rule: CSSRule) => fetchRule(rule, prefix));
+    });
+}
+
+export function updateSiteColors(side1: "Inside"|"Outside"|null): void {
+    if (!side1) {
+        return;
+    }
+    loadRegionColorsFromStyleSheets();
+    if (regionColorMapFromCss.size == 0) {
+        console.warn('Cannot read any region-related color rules');
+    }
+
+    siteCssNames.forEach((ultClassName, index) => {
+        const color = regionColorMapFromCss.get(ultClassName);
+        if (color != null) {
+            siteColors[index] = color;
+        }
+    });
+
+    const inside = regionColorMapFromCss.get("ult_inside");
+    const outside = regionColorMapFromCss.get("ult_outside");
+    if (side1 == "Inside") {
+        siteColors[SiteIndexes.Side1] = inside;
+        siteColors[SiteIndexes.Side2] = outside;
+    } else if (side1 == "Outside") {
+        siteColors[SiteIndexes.Side1] = outside;
+        siteColors[SiteIndexes.Side2] = inside;
+    }
+}
+
+function fetchRule(rule: CSSRule, prefix: string) {
+    let styleRule = rule as CSSStyleRule;
+    if (styleRule.selectorText?.startsWith('.' + prefix)) {
+        const value = styleRule.style.getPropertyValue('fill');
+        const color: Color = getStyleColor(value);
+        const key = styleRule.selectorText.slice(1);
+        regionColorMapFromCss.set(key, color);
+    }
+}
+
+function getStyleColor(cssColorText: string): Color {
+    const values = cssColorText?.match(/\d+/g);
+    let intValues = values?.map(value => parseInt(value));
+    if (!intValues) {
+        intValues = [ 0, 0, 0 ];
+    }
+    return Color.fromArray(intValues, 0);
+}

+ 182 - 0
src/tmdet-extension/transformation.ts

@@ -0,0 +1,182 @@
+/**
+ * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+ *
+ * Licensed under CC BY-NC 4.0, see LICENSE file for more info.
+ *
+ * @author Gabor Tusnady <tusnady.gabor@ttk.hu>
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+
+import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
+import { PluginUIContext } from 'molstar/lib/mol-plugin-ui/context';
+import { Mat4, Vec3 } from 'molstar/lib/mol-math/linear-algebra';
+import { PDBTMDescriptor, PDBTMTransformationMatrix, PMS } from './types';
+import { createStructureRepresentationParams } from 'molstar/lib/mol-plugin-state/helpers/structure-representation-params';
+import { StateTransforms } from 'molstar/lib/mol-plugin-state/transforms';
+import { StateObjectRef, StateBuilder } from 'molstar/lib/mol-state';
+import { Expression } from 'molstar/lib/mol-script/language/expression';
+import { MembraneOrientation } from './prop';
+import { TmDetColorThemeProvider } from './tmdet-color-theme';
+import { DebugUtil } from './debug-utils';
+
+
+export function applyTransformations(plugin: PluginUIContext, pdbtmDescriptor: PDBTMDescriptor) {
+    const annotations = pdbtmDescriptor.additional_entry_annotations;
+
+    const membraneTransformation = transformationForStateTransform(pdbtmDescriptor.additional_entry_annotations.membrane.transformation_matrix);
+
+    if (annotations?.biomatrix?.matrix_list) {
+        annotations.biomatrix.matrix_list.forEach(function(mx) {
+            mx.apply_to_chain_list.forEach(function(chainPair) {
+                let id  = chainPair.chain_id;
+                let newId = chainPair.new_chain_id;
+                if (annotations.biomatrix.chain_deletes?.includes(newId)) {
+                    DebugUtil.log(`${id} -> ${newId} transformation skipped due to delete rule`);
+                    return;
+                }
+                const mtx = transformationForStateTransform(mx.transformation_matrix);
+                const composedTransformation = Mat4.mul(Mat4(), membraneTransformation, mtx);
+                chainTransformation(plugin, composedTransformation, id, newId);
+                // await plugin.runTask(Task.create(`TMDET: Transform '${id}' into '${newId}'`, async () => {
+                //     chainTransformation(plugin, mx.transformation_matrix, id, newId);
+                // }));
+            });
+        });
+    }
+
+    // WARNING: the components cannot be accessed here (created by chainTransformations in the above loop)
+    //          Maybe due to some kind of synchronization behavior.
+    //          plugin.runTask with "await" also cannot synchronize here.
+    //
+    // So this function call transforms only already existing components due to a side effect of another issue.
+    transformWholeModel(plugin, pdbtmDescriptor.additional_entry_annotations.membrane.transformation_matrix);
+    // await plugin.runTask(Task.create(`TMDET: Apply membrane transformation`, async () => {
+    //     transformWholeModel(plugin, pdbtmDescriptor.additional_entry_annotations.membrane.transformation_matrix);
+    // }));
+
+}
+
+export function transformWholeModel(plugin: PluginUIContext, membraneMatrix: PDBTMTransformationMatrix) {
+    //
+    // membrane transformation
+    //
+    const membraneTransformation = transformationForStateTransform(membraneMatrix);
+
+    // transform each component
+    DebugUtil.log("STRUCTURES", getCurrentHierarchy(plugin).structures);
+    getCurrentHierarchy(plugin).structures[0].components.forEach(component => {
+        const structure: StateObjectRef<PMS> = component.cell;
+        const update: StateBuilder.To<any, any> = plugin.build().to(structure);
+
+        const label = component.cell.obj!.label.toUpperCase();
+        DebugUtil.log("memb.transform of", label, component.cell.obj?.id);
+        DebugUtil.log(`${label} component.cell.obj:`, component.cell.obj);
+        // DebugUtil.log(`${label} component:`, component);
+
+        update
+            .apply(StateTransforms.Model.TransformStructureConformation, {
+                "transform": { name: "matrix", params: { data: membraneTransformation, transpose: false } }
+            });
+        update.commit();
+    });
+}
+
+/**
+ * Perform transformation on a chain.
+ *
+ * @param plugin UI context
+ * @param transformationMatrix 4x4 matrix describes transformation and translation
+ * @param chainId Id of chain to be selected for transformation
+ */
+export function chainTransformation(plugin: PluginUIContext, transformationMatrix: Mat4, chainId: string, newId: string): void {
+    const query: Expression = getChainExpression(chainId);
+
+//    const transformation = transformationForStateTransform(transformationMatrix);
+    const transformation = transformationMatrix;
+    const structure: StateObjectRef<PMS> = plugin.managers.structure.hierarchy.current.models[0].structures[0].cell;
+    const update: StateBuilder.To<any, any> = plugin.build().to(structure);
+
+    update
+        .apply(
+            StateTransforms.Model.StructureSelectionFromExpression,
+            { label: newId, expression: query }
+        )
+        .apply(StateTransforms.Model.TransformStructureConformation, {
+            "transform": { name: "matrix", params: { data: transformation, transpose: false } }
+        })
+        .apply(
+            StateTransforms.Representation.StructureRepresentation3D,
+            createStructureRepresentationParams(plugin, structure.obj?.data, {
+                type: 'cartoon',
+                color: TmDetColorThemeProvider.name as any //, colorParams: { pdbtmDescriptor }
+            })
+        );
+    update.commit();
+    DebugUtil.log(`${chainId}->${newId} DONE`);
+}
+
+// function vadd(a: Vec3, b: Vec3): Vec3 {
+//     return Vec3.add(Vec3.zero(), a, b);
+// }
+
+function vneg(u: Vec3): Vec3 {
+    return Vec3.negate(Vec3.zero(), u);
+}
+
+export function createMembraneOrientation(pdbtmDescriptor: PDBTMDescriptor): MembraneOrientation {
+    const membrane = pdbtmDescriptor.additional_entry_annotations.membrane;
+
+    const membraneNormal: Vec3 = Vec3.fromObj(membrane.normal);
+    const result: MembraneOrientation = {
+        planePoint1: membraneNormal,
+        planePoint2: vneg(membraneNormal),
+        centroid: Vec3.zero(),
+        normalVector: membraneNormal,
+
+        // (NOTE: the TMDET extension calculates and sets it during applying preset)
+        radius: membrane.radius
+    };
+
+    return result;
+}
+
+export function transformationForStateTransform(tmatrix: PDBTMTransformationMatrix): Mat4 {
+    // matrix expected in column-major order
+    const mx: Mat4 = Mat4.fromArray(Mat4.zero(),
+        [
+            tmatrix.rowx.x, tmatrix.rowy.x, tmatrix.rowz.x, 0,
+            tmatrix.rowx.y, tmatrix.rowy.y, tmatrix.rowz.y, 0,
+            tmatrix.rowx.z, tmatrix.rowy.z, tmatrix.rowz.z, 0,
+                         0,              0,              0, 1
+        ], 0
+    );
+    Mat4.setTranslation(mx, Vec3.create(
+        tmatrix.rowx.t,
+        tmatrix.rowy.t,
+        tmatrix.rowz.t
+    ));
+    // TODO: DebugUtil.log('MAT4\n', Mat4.makeTable(mx));
+    return mx;
+}
+
+export function getAtomGroupExpression(chainId: string, auth_array: number[]): Expression {
+    // TODO DebugUtil.log('auth_array:', auth_array);
+    const query: Expression =
+        MS.struct.generator.atomGroups({
+            'residue-test': MS.core.set.has([MS.set( ...auth_array ), MS.ammp('auth_seq_id')]),
+            'chain-test': MS.core.rel.eq([chainId, MS.ammp('label_asym_id')])
+        });
+    return query;
+}
+
+export function getChainExpression(chainId: string): Expression {
+    const query: Expression =
+        MS.struct.generator.atomGroups({
+            'chain-test': MS.core.rel.eq([chainId, MS.ammp('label_asym_id')])
+        });
+    return query;
+}
+
+export function getCurrentHierarchy(plugin: PluginUIContext) {
+    return plugin.managers.structure.hierarchy.current;
+}

+ 100 - 0
src/tmdet-extension/types.ts

@@ -0,0 +1,100 @@
+/**
+ * Copyright (C) 2022, Protein Bioinformatics Research Group, RCNS
+ *
+ * Licensed under CC BY-NC 4.0, see LICENSE file for more info.
+ *
+ * @author Gabor Tusnady <tusnady.gabor@ttk.hu>
+ * @author Csongor Gerdan <gerdan.csongor@ttk.hu>
+ */
+
+import { PluginStateObject } from "molstar/lib/mol-plugin-state/objects";
+import { StateObject, StateTransformer } from 'molstar/lib/mol-state';
+import { StateObjectSelector } from "molstar/lib/mol-state/object";
+
+export type PMS = PluginStateObject.Molecule.Structure;
+
+export type PDBTMChain = {
+    chain_label: string,
+    additional_chain_annotations: {
+        type: string,
+        num_tm: number
+    },
+    residues: {
+        pdb_res_label?: string,
+        aa_type?: string,
+        site_data?: { site_id_ref: number, confidence_classification: string }[]
+    }[]
+};
+export type PDBTMRegion = { site: string, auth_ids: number[], color: number[] };
+export type PDBTMVec3 = { x: number, y: number, z: number };
+type PDBTMTransformationMatrixRow = { x: number, y: number, z: number, t: number };
+export type PDBTMTransformationMatrix = {
+    rowx: PDBTMTransformationMatrixRow,
+    rowy: PDBTMTransformationMatrixRow,
+    rowz: PDBTMTransformationMatrixRow
+};
+export type PDBTMDescriptor = {
+    pdb_id: string,
+    side1: "Inside"|"Outside"|null,
+    chains: PDBTMChain[],
+    site: { site_id: number, label: string }[],
+    additional_entry_annotations: {
+        tm_type: string,
+        membrane: {
+            normal: PDBTMVec3,
+            transformation_matrix: PDBTMTransformationMatrix,
+            radius: number
+        },
+        biomatrix: {
+            chain_deletes: string[],
+            matrix_list: {
+                matrix_id: string,
+                apply_to_chain_list: {
+                    chain_id: string,
+                    new_chain_id: string
+                }[],
+                transformation_matrix: PDBTMTransformationMatrix
+            }[]
+        }
+    }
+};
+
+type StructureComponentType = StateObjectSelector<
+        PMS,
+        StateTransformer<StateObject<any, StateObject.Type<any>>,
+        StateObject<any, StateObject.Type<any>>, any>
+    > | undefined;
+
+export type ComponentsType = { polymer: StructureComponentType; ligand: StructureComponentType; water: StructureComponentType };
+
+// for coloring and labeling
+export type ResidueItem = {
+    authId: string|undefined,
+    siteId: number,     // for labels
+    siteColorId: number // for site colors
+};
+export type Chain = {
+    chainId: string,
+    type: string,
+    residues: ResidueItem[]
+};
+export type ChainList = Chain[];
+
+export function getResidue(chains: ChainList, chainId: string, residueId: string): ResidueItem|undefined {
+    let result = undefined;
+    const chain = getChain(chains, chainId);
+    if (chain) {
+        result = chain.residues.filter((res) => res.authId === residueId)[0];
+    }
+
+    return result;
+}
+
+export function getChain(chains: ChainList, chainId: string): Chain|undefined {
+    let result = undefined;
+    const chain = chains.filter((chain) => chain.chainId === chainId)[0];
+    if (chain) {
+        result = chain;
+    }
+    return result;
+}

+ 2 - 0
src/viewer/helpers/model.ts

@@ -42,8 +42,10 @@ export class ModelLoader {
     ): Promise<S | ReturnType<typeof RcsbPreset.apply> | undefined> {
         const trajectory = await this.plugin.builders.structure.parseTrajectory(data, format);
         if (reprProvider) {
+            console.log('REPR. PROVIDER:', reprProvider);
             return this.plugin.builders.structure.hierarchy.applyPreset(trajectory, reprProvider, params);
         } else {
+            console.log('ELSE REPR. PROVIDER:', reprProvider);
             const selector = await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, RcsbPreset, {
                 preset: props || { kind: 'standard', assemblyId: '' }
             });

+ 6 - 0
src/viewer/helpers/preset.ts

@@ -134,6 +134,7 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
     isApplicable: () => true,
     params: RcsbParams,
     async apply(trajectory, params, plugin) {
+        console.log('RCSB PRESET: apply start');
         const builder = plugin.builders.structure;
         const p = params.preset;
 
@@ -174,6 +175,8 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
             Object.assign(presetParams, { theme: { globalName: 'plddt-confidence', focus: { name: 'plddt-confidence' } } });
         }
         let representation: StructureRepresentationPresetProvider.Result | undefined = undefined;
+        console.log('PRESET params:', params);
+        console.log('PRESET p:', p);
 
         if (p.kind === 'alignment') {
             // This creates a single structure from selections/transformations as specified
@@ -275,9 +278,12 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
         } else if (p.kind === 'nakb') {
             representation = await plugin.builders.structure.representation.applyPreset<any>(structureProperties!, 'auto', { ...presetParams, theme: { globalName: 'nakb', focus: { name: 'nakb' } } });
         } else {
+            console.log('HERE WE GO:', { structure: structure, structureProps: structureProperties, presetParams: presetParams });
             representation = await plugin.builders.structure.representation.applyPreset(structureProperties!, 'auto', presetParams);
         }
 
+        console.log('STRUCTURE REPRESENTATION:', representation);
+
         // TODO align with 'motif'?
         if ((p.kind === 'feature' || p.kind === 'feature-density') && structure?.obj) {
             let loci = targetToLoci(p.target, structure!.obj.data);

+ 11 - 1
src/viewer/index.ts

@@ -53,6 +53,7 @@ import { AssemblySymmetry } from 'molstar/lib/extensions/rcsb/assembly-symmetry/
 import { wwPDBChemicalComponentDictionary } from 'molstar/lib/extensions/wwpdb/ccd/behavior';
 import { ChemicalCompontentTrajectoryHierarchyPreset } from 'molstar/lib/extensions/wwpdb/ccd/representation';
 import { StateTransforms } from 'molstar/lib/mol-plugin-state/transforms';
+import { TMDETMembraneOrientation, loadWithUNITMPMembraneRepresentation } from '../tmdet-extension/behavior';
 
 /** package version, filled in at bundle build time */
 declare const __RCSB_MOLSTAR_VERSION__: string;
@@ -72,6 +73,7 @@ const Extensions = {
     'model-export': PluginSpec.Behavior(ModelExport),
     'mp4-export': PluginSpec.Behavior(Mp4Export),
     'geo-export': PluginSpec.Behavior(GeometryExport),
+    'tmdet-membrane-orientation': PluginSpec.Behavior(TMDETMembraneOrientation)
 };
 
 const DefaultViewerProps = {
@@ -163,6 +165,7 @@ export class Viewer {
     private prevExpanded: boolean;
 
     constructor(elementOrId: string | HTMLElement, props: Partial<ViewerProps> = {}) {
+        console.log("RCSB constructor");
         const element = typeof elementOrId === 'string' ? document.getElementById(elementOrId)! : elementOrId;
         if (!element) throw new Error(`Could not get element with id '${elementOrId}'`);
 
@@ -340,7 +343,14 @@ export class Viewer {
     }
 
     loadStructureFromUrl<P, S>(url: string, format: BuiltInTrajectoryFormat, isBinary: boolean, config?: {props?: PresetProps & { dataLabel?: string }; matrix?: Mat4; reprProvider?: TrajectoryHierarchyPresetProvider<P, S>, params?: P}) {
-        return this.customState.modelLoader.load({ fileOrUrl: url, format, isBinary }, config?.props, config?.matrix, config?.reprProvider, config?.params);
+        console.log('RCSB loadStructureFromUrl II', this.customState);
+        const pdbId = '1afo';
+        //return this.customState.modelLoader.load({ fileOrUrl: url, format, isBinary }, config?.props, config?.matrix, config?.reprProvider, config?.params);
+        return loadWithUNITMPMembraneRepresentation(this._plugin, {
+            structureUrl: `https://www.ebi.ac.uk/pdbe/entry-files/download/${pdbId}_updated.cif`,
+            regionDescriptorUrl: `/${pdbId}.json`,
+            side1: "Periplasm"
+        });
     }
 
     loadSnapshotFromUrl(url: string, type: PluginState.SnapshotType) {