import { readDataFile, guid, rgbToHsv, hsvToRgb } from "./utils"
import { moorhen } from "../types/moorhen";
import { webGL } from "../types/mgWebGL";
import { libcootApi } from "../types/libcoot";
import pako from "pako"
import MoorhenReduxStore from "../store/MoorhenReduxStore";
import { ToolkitStore } from "@reduxjs/toolkit/dist/configureStore";
import { MoorhenMtzWrapper } from "./MoorhenMtzWrapper";
const _DEFAULT_CONTOUR_LEVEL = 0.8
const _DEFAULT_RADIUS = 13
const _DEFAULT_STYLE = "lines"
const _DEFAULT_ALPHA = 1.0
const _DEFAULT_MAP_COLOUR = { r: 0.30000001192092896, g: 0.30000001192092896, b: 0.699999988079071}
const _DEFAULT_POSITIVE_MAP_COLOUR = {r: 0.4000000059604645, g: 0.800000011920929, b: 0.4000000059604645}
const _DEFAULT_NEGATIVE_MAP_COLOUR = {r: 0.800000011920929, g: 0.4000000059604645, b: 0.4000000059604645}
/**
* Represents a map
* @property {string} name - The name assigned to this map instance
* @property {number} molNo - The imol assigned to this map instance
* @property {string} style - Indicates whether the rendered map is drawn as lines, lit lines or a solid surphace
* @property {boolean} isDifference - Indicates whether this is a difference map instance
* @property {boolean} hasReflectionData - Indicates whether this map instance has been associated with observed reflection data
* @property {React.RefObject<moorhen.CommandCentre>} commandCentre - A react reference to the command centre instance
* @property {React.RefObject<webGL.MGWebGL>} glRef - A react reference to the MGWebGL instance
* @constructor
* @param {React.RefObject<moorhen.CommandCentre>} commandCentre - A react reference to the command centre instance
* @param {React.RefObject<webGL.MGWebGL>} glRef - A react reference to the MGWebGL instance
* @param {ToolkitStore} [store=undefined] - A Redux store. By default Moorhen Redux store will be used
* @example
* import { MoorhenMap } from "moorhen";
*
* // Create a new map
* const map = new MoorhenMap(commandCentre, glRef);
*
* // Load file from a URL
* const selectedColumns = { F: "FWT", PHI: "PHWT", Fobs: "FP", SigFobs: "SIGFP", FreeR: "FREE", isDifference: false, useWeight: false, calcStructFact: true }
* map.loadToCootFromMtzURL("/uri/to/file.mtz", "map-1", selectedColumns);
*
* // Draw map and set view on map centre
* map.drawMapContour();
* map.centreOnMap();
*
* // Delete map
* map.delete();
*/
export class MoorhenMap implements moorhen.Map {
type: string
name: string
isEM: boolean
molNo: number
store: ToolkitStore
commandCentre: React.RefObject<moorhen.CommandCentre>
glRef: React.RefObject<webGL.MGWebGL>
mapCentre: [number, number, number]
suggestedContourLevel: number
suggestedRadius: number
webMGContour: boolean
showOnLoad: boolean
displayObjects: any
isDifference: boolean
hasReflectionData: boolean
selectedColumns: moorhen.selectedMtzColumns
associatedReflectionFileName: string
uniqueId: string
mapRmsd: number
mapMean: number
suggestedMapWeight: number
otherMapForColouring: {molNo: number, min: number, max: number};
diffMapColourBuffers: { positiveDiffColour: number[], negativeDiffColour: number[] }
defaultMapColour: {r: number, g: number, b: number};
defaultPositiveMapColour: {r: number, g: number, b: number};
defaultNegativeMapColour: {r: number, g: number, b: number};
autoReadMtz: (source: File, commandCentre: React.RefObject<moorhen.CommandCentre>, glRef: React.RefObject<webGL.MGWebGL>, store: ToolkitStore) => Promise<moorhen.Map[]>;
constructor(commandCentre: React.RefObject<moorhen.CommandCentre>, glRef: React.RefObject<webGL.MGWebGL>, store: ToolkitStore = MoorhenReduxStore) {
this.type = 'map'
this.name = "unnamed"
this.isEM = false
this.molNo = null
this.commandCentre = commandCentre
this.glRef = glRef
this.store = store
this.webMGContour = false
this.showOnLoad = true
this.displayObjects = { Coot: [] }
this.isDifference = false
this.hasReflectionData = false
this.selectedColumns = null
this.associatedReflectionFileName = null
this.uniqueId = guid()
this.mapRmsd = null
this.mapMean = null
this.suggestedMapWeight = null
this.suggestedContourLevel = null
this.suggestedRadius = null
this.mapCentre = null
this.otherMapForColouring = null
this.diffMapColourBuffers = { positiveDiffColour: [], negativeDiffColour: [] }
this.defaultMapColour = _DEFAULT_MAP_COLOUR
this.defaultPositiveMapColour = _DEFAULT_POSITIVE_MAP_COLOUR
this.defaultNegativeMapColour = _DEFAULT_NEGATIVE_MAP_COLOUR
}
/**
* Helper function to set this map instance as the "active" map for refinement
*/
async setActive(): Promise<void> {
await this.commandCentre.current.cootCommand({
returnType: "status",
command: "set_imol_refinement_map",
commandArgs: [this.molNo]
}, false)
if (this.suggestedMapWeight === null) {
await this.estimateMapWeight()
}
await this.setMapWeight()
}
/**
* Delete the map instance
*/
async delete(): Promise<void> {
Object.getOwnPropertyNames(this.displayObjects).forEach(displayObject => {
if (this.displayObjects[displayObject].length > 0) { this.clearBuffersOfStyle(displayObject) }
})
this.glRef.current.drawScene()
const promises = [
this.commandCentre.current.cootCommand({
returnType: "status",
command: 'close_molecule',
commandArgs: [this.molNo]
}, true),
this.hasReflectionData ?
this.commandCentre.current.postMessage({
message: 'delete_file_name', fileName: this.associatedReflectionFileName
})
:
Promise.resolve(true)
]
await Promise.all(promises)
}
/**
* Replace the current map with the contents of a MTZ file
* @param {string} fileUrl - The uri to the MTZ file
* @param {moorhen.selectedMtzColumns} selectedColumns - Object indicating the selected MTZ columns
* @returns {Promise<void>}
*/
async replaceMapWithMtzFile(fileUrl: RequestInfo | URL, selectedColumns: moorhen.selectedMtzColumns): Promise<void> {
let mtzData: Uint8Array
let fetchResponse: Response
try {
fetchResponse = await fetch(fileUrl)
} catch (err) {
return Promise.reject(`Unable to fetch file ${fileUrl}`)
}
if (fetchResponse.ok) {
const reflectionData = await fetchResponse.blob()
const arrayBuffer = await reflectionData.arrayBuffer()
mtzData = new Uint8Array(arrayBuffer)
} else {
return Promise.reject(`Error fetching data from url ${fileUrl}`)
}
const cootResponse = await this.commandCentre.current.cootCommand({
returnType: "status",
command: 'shim_replace_map_by_mtz_from_file',
commandArgs: [this.molNo, mtzData, selectedColumns]
}, true) as moorhen.WorkerResponse<number>
if (cootResponse.data.result.status === 'Completed') {
return this.drawMapContour()
}
return Promise.reject(cootResponse.data.result.status)
}
/**
* Load map to moorhen using a MTZ url
* @param {string} url - The url to the MTZ file
* @param {string} name - The name that will be assigned to the map
* @param {moorhen.selectedMtzColumns} selectedColumns - Object indicating the selected MTZ columns
* @param {object} [options] - Options passed to fetch API
* @returns {Pormise<moorhen.Map>} This moorhenMap instance
*/
async loadToCootFromMtzURL(url: RequestInfo | URL, name: string, selectedColumns: moorhen.selectedMtzColumns, options?: RequestInit): Promise<moorhen.Map> {
try {
const response = await fetch(url, options)
if (!response.ok) {
return Promise.reject(`Error fetching data from url ${url}`)
}
const reflectionData: Blob = await response.blob()
const arrayBuffer: ArrayBuffer = await reflectionData.arrayBuffer()
const asUIntArray: Uint8Array = new Uint8Array(arrayBuffer)
await this.loadToCootFromMtzData(asUIntArray, name, selectedColumns)
if (selectedColumns.calcStructFact) {
await this.associateToReflectionData(selectedColumns, asUIntArray)
}
return this
} catch (err) {
console.log(err)
return Promise.reject(err)
}
}
/**
* Load map to moorhen using MTZ data
* @param {Uint8Array} data - The mtz data
* @param {string} name - The name that will be assigned to the map
* @param {moorhen.selectedMtzColumns} selectedColumns - Object indicating the selected MTZ columns
* @returns {Pormise<moorhen.Map>} This moorhenMap instance
*/
async loadToCootFromMtzData(data: Uint8Array, name: string, selectedColumns: moorhen.selectedMtzColumns): Promise<moorhen.Map> {
this.name = name
try {
const reply = await this.commandCentre.current.cootCommand({
returnType: "status",
command: "shim_read_mtz",
commandArgs: [data, name, selectedColumns]
}, true)
if (reply.data.result.status === 'Exception') {
return Promise.reject(reply.data.result.consoleMessage)
}
this.molNo = reply.data.result.result
this.selectedColumns = selectedColumns
if (Object.keys(selectedColumns).includes('isDifference')) {
this.isDifference = selectedColumns.isDifference
}
await this.getSuggestedSettings()
return this
} catch(err) {
return Promise.reject(err)
}
}
/**
* Load map to moorhen from a MTZ file
* @param {File} source - The MTZ file
* @param {moorhen.selectedMtzColumns} selectedColumns - Object indicating the selected MTZ columns
* @returns {Promise<moorhen.Map>} This moorhenMap instance
*/
loadToCootFromMtzFile = async function (source: File, selectedColumns: moorhen.selectedMtzColumns): Promise<moorhen.Map> {
const $this = this
let reflectionData = await readDataFile(source)
const asUIntArray = new Uint8Array(reflectionData)
await $this.loadToCootFromMtzData(asUIntArray, source.name, selectedColumns)
if (selectedColumns.calcStructFact) {
await $this.associateToReflectionData(selectedColumns, asUIntArray)
}
return $this
}
/**
* Load map to moorhen from a map file url
* @param {string} url - The url to the MTZ file
* @param {string} name - The name that will be assigned to the map
* @param {boolean} [isDiffMap=false] - Indicates whether the new map is a difference map
* @param {boolean} [decompress=false] - Indicates whether the new map should be decompressed before being passed to libcoot api
* @param {object} [options] - Options passed to fetch API
* @returns {Promise<moorhen.Map>} This moorhenMap instance
*/
async loadToCootFromMapURL(url: RequestInfo | URL, name: string, isDiffMap: boolean = false, decompress: boolean = false, options?: RequestInit): Promise<moorhen.Map> {
try {
const response = await fetch(url, options);
if (response.ok) {
const blobData = await response.blob();
const arrayBuffer = await blobData.arrayBuffer();
let mapData: ArrayBuffer | Uint8Array;
if (decompress) {
mapData = pako.inflate(arrayBuffer)
} else {
mapData = new Uint8Array(arrayBuffer)
}
return await this.loadToCootFromMapData(mapData, name, isDiffMap);
} else {
return Promise.reject(`Requested ${url} and response was not OK...`)
}
} catch (err) {
return Promise.reject(err);
}
}
/**
* Load map to moorhen from map data
* @param {ArrayBuffer | Uint8Array} data - The map data in the form of a array buffer
* @param {string} name - The name that will be assigned to the map
* @param {boolean} isDiffMap - Indicates whether the new map is a difference map
* @returns {Promise<moorhen.Map>} This moorhenMap instance
*/
async loadToCootFromMapData(data: ArrayBuffer | Uint8Array, name: string, isDiffMap: boolean): Promise<moorhen.Map> {
this.name = name
try {
const reply = await this.commandCentre.current.cootCommand({
returnType: "status",
command: "shim_read_ccp4_map",
commandArgs: [data, name, isDiffMap]
}, true)
if (reply.data.result?.status === 'Exception') {
console.warn('Exception raised when reading map')
return Promise.reject(reply.data.result.consoleMessage)
} else if (reply.data.result?.result === -1) {
console.warn('Returned map has molNo -1')
return Promise.reject(reply.data.result.consoleMessage)
}
this.molNo = reply.data.result.result
this.isDifference = isDiffMap
await this.getSuggestedSettings()
return this
} catch(err) {
console.warn(err)
return Promise.reject(err)
}
}
/**
* Load a map to moorhen from a map file data blob
* @param {File} source - The map file
* @param {boolean} [isDiffMap=false] - Indicates whether the new map is a difference map
* @param {boolean} [decompress=false] - Indicates whether the new map should be decompressed before being passed to libcoot api
* @returns {Promise<moorhen.Map>} This moorhenMap instance
*/
async loadToCootFromMapFile (source: File, isDiffMap: boolean = false, decompress: boolean = false): Promise<moorhen.Map> {
const arrayBuffer = await readDataFile(source)
let mapData: ArrayBuffer | Uint8Array;
let mapName: string;
if (decompress) {
mapData = pako.inflate(arrayBuffer)
mapName = source.name.replace('.gz', '')
} else {
mapData = new Uint8Array(arrayBuffer)
mapName = source.name
}
return this.loadToCootFromMapData(mapData, mapName, isDiffMap)
}
/**
* Static method used to automatically read multiple maps from a single mtz file
* @param {File} source - The mtz file
* @param {React.RefObject<moorhen.CommandCentre>} commandCentre - A react reference to the command centre instance
* @param {React.RefObject<webGL.MGWebGL>} glRef - A react reference to the MGWebGL instance
* @param {ToolkitStore} store - The redux store
* @returns {moorhen.Map[]} A list of maps resulting from reading the mtz file
*/
static async autoReadMtz(source: File, commandCentre: React.RefObject<moorhen.CommandCentre>, glRef: React.RefObject<webGL.MGWebGL>, store: ToolkitStore): Promise<moorhen.Map[]> {
const mtzWrapper = new MoorhenMtzWrapper()
await mtzWrapper.loadHeaderFromFile(source)
const response = await commandCentre.current.cootCommand({
returnType: "auto_read_mtz_info_array",
command: "shim_auto_read_mtz",
commandArgs: [mtzWrapper.reflectionData]
}, true) as moorhen.WorkerResponse<libcootApi.AutoReadMtzInfoJS[]>
if (response.data.result.status === "Exception" || response.data.result.result.length === 0) {
console.log(response.data.consoleMessage)
console.warn('There was a problem with auto-open mtz...')
return []
}
const isDiffMapResponses = await Promise.all(response.data.result.result.map(autoReadInfo => {
return commandCentre.current.cootCommand({
returnType: "status",
command: "is_a_difference_map",
commandArgs: [autoReadInfo.idx]
}, false) as Promise<moorhen.WorkerResponse<boolean>>
}))
if (isDiffMapResponses.some(result => result.data.result.status == "Exception")) {
console.log(isDiffMapResponses.find(result => result.data.result.status === "Exception").data.consoleMessage)
console.warn('There was a problem with auto-open mtz...')
return []
}
const newMaps = await Promise.all(
response.data.result.result.filter(item => item.idx !== -1).map(async (autoReadInfo, index) => {
const newMap = new MoorhenMap(commandCentre, glRef, store)
newMap.molNo = autoReadInfo.idx
newMap.name = `${source.name.replace('mtz', '')}-map-${index}`
newMap.isDifference = isDiffMapResponses[index].data.result.result
newMap.selectedColumns = {
F: autoReadInfo.F,
Fobs: autoReadInfo.F_obs,
FreeR: autoReadInfo.Rfree,
SigFobs: autoReadInfo.sigF_obs,
PHI: autoReadInfo.phi,
isDifference: newMap.isDifference,
useWeight: autoReadInfo.weights_used,
calcStructFact: true
}
await newMap.associateToReflectionData(newMap.selectedColumns, mtzWrapper.reflectionData)
await newMap.getSuggestedSettings()
return newMap
})
)
return newMaps
}
/**
* Get the current map
* @returns {Promise<moorhen.WorkerResponse>} A worker response with the map arrayBuffer
*/
getMap(): Promise<moorhen.WorkerResponse> {
return this.commandCentre.current.postMessage({
message: 'get_map',
molNo: this.molNo
})
}
/**
* Set the map weight
* @param {number} [weight=moorhen.Map.suggestedMapWeight] - The new map weight
* @returns {Promise<moorhen.WorkerResponse>} Void worker response
*/
setMapWeight(weight?: number): Promise<moorhen.WorkerResponse> {
let newWeight: number
if (typeof weight !== 'undefined') {
newWeight = weight
}
else {
newWeight = this.suggestedMapWeight
}
return this.commandCentre.current.cootCommand({
returnType: 'status',
command: "set_map_weight",
commandArgs: [newWeight]
}, false)
}
/**
* Get the current map weight
* @returns {Promise<number>} The current map weight
*/
async getMapWeight(): Promise<number> {
const result = await this.commandCentre.current.cootCommand({
returnType: 'status',
command: "get_map_weight",
commandArgs: []
}, false) as moorhen.WorkerResponse<number>
return result.data.result.result
}
/**
* Get map contour parameters from the redux store
* @returns {object} A description of map contour parameters as described in the redux store
*/
getMapContourParams(): {
mapRadius: number;
contourLevel: number;
mapAlpha: number;
mapStyle: "lines" | "solid" | "lit-lines";
mapColour: {r: number; g: number; b: number};
positiveMapColour: {r: number; g: number; b: number};
negativeMapColour: {r: number; g: number; b: number}
} {
const state = this.store.getState()
const radius = state.mapContourSettings.mapRadii.find(item => item.molNo === this.molNo)?.radius
const level = state.mapContourSettings.contourLevels.find(item => item.molNo === this.molNo)?.contourLevel
const alpha = state.mapContourSettings.mapAlpha.find(item => item.molNo === this.molNo)?.alpha
const style = state.mapContourSettings.mapStyles.find(item => item.molNo === this.molNo)?.style
const mapColour = state.mapContourSettings.mapColours.find(item => item.molNo === this.molNo)?.rgb
const negativeMapColour = state.mapContourSettings.negativeMapColours.find(item => item.molNo === this.molNo)?.rgb
const positiveMapColour = state.mapContourSettings.positiveMapColours.find(item => item.molNo === this.molNo)?.rgb
return {
mapRadius: radius ? radius : _DEFAULT_RADIUS,
contourLevel: level ? level : _DEFAULT_CONTOUR_LEVEL,
mapAlpha: alpha ? alpha : _DEFAULT_ALPHA,
mapStyle: style ? style : _DEFAULT_STYLE,
mapColour: mapColour ? {r: mapColour.r / 255., g: mapColour.g / 255., b: mapColour.b / 255.} : this.defaultMapColour,
negativeMapColour: negativeMapColour ? {r: negativeMapColour.r / 255., g: negativeMapColour.g / 255., b: negativeMapColour.b / 255.} : this.defaultNegativeMapColour,
positiveMapColour: positiveMapColour ? {r: positiveMapColour.r / 255., g: positiveMapColour.g / 255., b: positiveMapColour.b / 255.} : this.defaultPositiveMapColour
}
}
/**
* Contour the map with parameters from the redux store
*/
drawMapContour(): Promise<void> {
const { mapRadius, contourLevel, mapStyle } = this.getMapContourParams()
return this.doCootContour(...this.glRef.current.origin.map(coord => -coord) as [number, number, number], mapRadius, contourLevel, mapStyle)
}
/**
* Hide the map contour
*/
hideMapContour(): void {
this.clearBuffersOfStyle('Coot')
this.glRef.current.buildBuffers();
this.glRef.current.drawScene();
}
/**
* Clear MGWebGL buffers of a given style for this map
* @param {string} style - The map style that will be cleared
*/
clearBuffersOfStyle(style: string): void {
//Empty existing buffers of this type
this.displayObjects[style].forEach((buffer) => {
buffer.clearBuffers()
this.glRef.current.displayBuffers = this.glRef.current.displayBuffers?.filter(glBuffer => glBuffer.id !== buffer.id)
})
this.displayObjects[style] = []
}
setupContourBuffers(objects: any[], keepCootColours: boolean = false) {
const { mapAlpha, mapColour, positiveMapColour, negativeMapColour } = this.getMapContourParams()
const print_timing = false;
const t1 = performance.now();
try {
const diffMapColourBuffers = { positiveDiffColour: [], negativeDiffColour: [] }
objects.filter(object => typeof object !== 'undefined' && object !== null).forEach(object => {
let object_positive;
let object_negative;
if (this.isDifference) {
object_positive = structuredClone(object);
object_negative = structuredClone(object);
object_positive.idx_tri = [];
object_negative.idx_tri = [];
const tc = performance.now();
if(print_timing) console.log("End clone",tc-t1);
let i = 0;
object.idx_tri.forEach((idxss: number[][]) => {
let j = 0;
let pos_idx = [];
let neg_idx = [];
idxss.forEach((idxs: number[]) => {
let this_pos_idx = [];
let this_neg_idx = [];
for (let idx = 0; idx < idxs.length; idx++) {
const col = object.col_tri[i][j][idxs[idx]*4]
if (col < 0.5) {
this_pos_idx.push(idxs[idx])
diffMapColourBuffers.positiveDiffColour.push(idxs[idx]*4)
object_positive.col_tri[i][j][idxs[idx]*4] = positiveMapColour.r
object_positive.col_tri[i][j][idxs[idx]*4+1] = positiveMapColour.g
object_positive.col_tri[i][j][idxs[idx]*4+2] = positiveMapColour.b
object_positive.col_tri[i][j][idxs[idx]*4+3] = mapAlpha
} else {
this_neg_idx.push(idxs[idx])
diffMapColourBuffers.negativeDiffColour.push(idxs[idx]*4)
object_negative.col_tri[i][j][idxs[idx]*4] = negativeMapColour.r
object_negative.col_tri[i][j][idxs[idx]*4+1] = negativeMapColour.g
object_negative.col_tri[i][j][idxs[idx]*4+2] = negativeMapColour.b
object_negative.col_tri[i][j][idxs[idx]*4+3] = mapAlpha
}
}
pos_idx.push(this_pos_idx)
neg_idx.push(this_neg_idx)
j++;
})
object_positive.idx_tri.push(pos_idx)
object_negative.idx_tri.push(neg_idx)
i++;
})
const tl = performance.now();
if(print_timing) console.log("End loop",tl-t1)
} else if (!keepCootColours) {
if (mapAlpha < 0.98) {
object.col_tri.forEach((cols: number[][]) => {
cols.forEach((col: number[]) => {
for (let idx = 0; idx < col.length; idx += 4) {
col[idx] = mapColour.r
col[idx + 1] = mapColour.g
col[idx + 2] = mapColour.b
col[idx + 3] = mapAlpha
}
})
})
}
const tl = performance.now();
if(print_timing) console.log("End loop",tl-t1)
}
if (this.isDifference) {
this.clearBuffersOfStyle("Coot")
let a = this.glRef.current.appendOtherData(object_positive, true);
let b = this.glRef.current.appendOtherData(object_negative, true);
if(mapAlpha<0.99){
a[0].transparent = true;
b[0].transparent = true;
}
const ta = performance.now();
if(print_timing) console.log("End appendOtherData",ta-t1);
this.diffMapColourBuffers.positiveDiffColour = this.diffMapColourBuffers.positiveDiffColour.concat(diffMapColourBuffers.positiveDiffColour);
this.diffMapColourBuffers.negativeDiffColour = this.diffMapColourBuffers.negativeDiffColour.concat(diffMapColourBuffers.negativeDiffColour);
this.displayObjects['Coot'] = this.displayObjects['Coot'].concat(a);
this.displayObjects['Coot'] = this.displayObjects['Coot'].concat(b);
} else if (!keepCootColours) {
//console.log("DEBUG: Old buffers?", object.vert_tri[0][0],object.norm_tri[0][0])
if(this.displayObjects["Coot"].length>0 && (object.prim_types[0][0]===this.displayObjects["Coot"][0].bufferTypes[0])){
this.displayObjects["Coot"][0].triangleVertices[0] = object.vert_tri[0][0]
this.displayObjects["Coot"][0].triangleNormals[0] = object.norm_tri[0][0]
if(mapAlpha>0.98){
//console.log("DEBUG: Old buffers setCustomColour")
this.displayObjects["Coot"][0].setCustomColour([mapColour.r,mapColour.g,mapColour.b,1.0])
} else {
this.displayObjects["Coot"][0].triangleColours[0] = object.col_tri[0][0]
}
this.displayObjects["Coot"][0].triangleIndexs[0] = object.idx_tri[0][0]
this.displayObjects["Coot"][0].isDirty = true
} else {
this.clearBuffersOfStyle("Coot")
let a = this.glRef.current.appendOtherData(object, true);
if(mapAlpha>0.98){
a[0].setCustomColour([mapColour.r,mapColour.g,mapColour.b,1.0])
}
this.displayObjects['Coot'] = this.displayObjects['Coot'].concat(a);
}
const ta = performance.now();
if(print_timing) console.log("End appendOtherData",ta-t1);
this.diffMapColourBuffers.positiveDiffColour = this.diffMapColourBuffers.positiveDiffColour.concat(diffMapColourBuffers.positiveDiffColour);
this.diffMapColourBuffers.negativeDiffColour = this.diffMapColourBuffers.negativeDiffColour.concat(diffMapColourBuffers.negativeDiffColour);
} else {
//console.log("MOORHEN MAP do what keepCootColours wants")
this.clearBuffersOfStyle("Coot")
let a = this.glRef.current.appendOtherData(object, true);
this.displayObjects['Coot'] = this.displayObjects['Coot'].concat(a);
}
})
if(print_timing) console.log("Start buildBuffers");
this.glRef.current.buildBuffers();
const tb = performance.now();
if(print_timing) console.log("End buildBuffers",tb-t1);
this.glRef.current.drawScene();
const ts = performance.now();
if(print_timing) console.log("After drawScene",ts-t1);
} catch(err) {
//console.log(err)
}
const t2 = performance.now();
if(print_timing) console.log("Finished setupContourBuffers",t2-t1)
}
/**
* Draw the map contour around a given origin
* @param {number} x - Origin coord. X
* @param {number} y - Origin coord. Y
* @param {number} z - Origin coord. Z
* @param {number} radius - Radius around the origin that will be drawn
* @param {number} contourLevel - The map contour level
*/
async doCootContour(x: number, y: number, z: number, radius: number, contourLevel: number, style: "solid" | "lines" | "lit-lines"): Promise<void> {
let returnType: string
if (style === 'solid') {
returnType = "mesh_perm"
} else if (style === 'lit-lines') {
returnType = "lit_lines_mesh"
} else {
returnType = "lines_mesh"
}
let response: moorhen.WorkerResponse<any>
if (this.otherMapForColouring !== null) {
response = await this.commandCentre.current.cootCommand({
returnType: returnType,
command: "get_map_contours_mesh_using_other_map_for_colours",
commandArgs: [this.molNo, this.otherMapForColouring.molNo, x, y, z, radius, contourLevel, this.otherMapForColouring.min, this.otherMapForColouring.max, false]
}, false)
} else {
response = await this.commandCentre.current.cootCommand({
returnType: returnType,
command: "get_map_contours_mesh",
commandArgs: [this.molNo, x, y, z, radius, contourLevel]
}, false)
}
const objects = [response.data.result.result]
this.setupContourBuffers(objects, this.otherMapForColouring !== null)
}
/**
* Set colouring for this map instance based on another map
* @param {number} molNo - The imol for the other map
* @param {number} min - The min value
* @param {number} max - The max value
*/
setOtherMapForColouring(molNo: number, min: number = -0.9, max: number = 0.9) {
if (molNo === null) {
this.otherMapForColouring = null
} else {
this.otherMapForColouring = { molNo, min, max }
}
}
/**
* Fetch the colours for a difference map using values from redux store and redraw the map
* @param {'positiveDiffColour' | 'negativeDiffColour'} type - Indicates whether the negative or positive colours will be set
* @returns {Promise<void>}
*/
async fetchDiffMapColourAndRedraw(type: 'positiveDiffColour' | 'negativeDiffColour'): Promise<void> {
if (!this.isDifference) {
console.error('Cannot use moorhen.Map.fetchDiffMapColourAndRedraw to change non-diff map colour. Use moorhen.Map.fetchColourAndRedraw instead...')
return
}
const { mapAlpha, positiveMapColour, negativeMapColour } = this.getMapContourParams()
const mapColour = type === 'positiveDiffColour' ? positiveMapColour : negativeMapColour
if (mapAlpha < 0.99) {
this.displayObjects['Coot'].forEach((buffer, bufferIdx) => {
buffer.customColour = null;
buffer.transparent = true
buffer.triangleColours.forEach((colbuffer, colBufferIdx) => {
for (const idx of this.diffMapColourBuffers[type]) {
colbuffer[idx] = mapColour.r
colbuffer[idx + 1] = mapColour.g
colbuffer[idx + 2] = mapColour.b
}
})
buffer.isDirty = true
buffer.alphaChanged = true;
})
} else {
if(this.displayObjects['Coot'].length===2){
if(type==='positiveDiffColour'){
this.displayObjects['Coot'][0].setCustomColour([mapColour.r,mapColour.g,mapColour.b,1.0])
this.displayObjects['Coot'][0].transparent = false
} else {
this.displayObjects['Coot'][1].setCustomColour([mapColour.r,mapColour.g,mapColour.b,1.0])
this.displayObjects['Coot'][1].transparent = false
}
}
}
if (mapAlpha < 0.99) {
this.glRef.current.buildBuffers();
}
this.glRef.current.drawScene();
}
/**
* Set the colours for a non-difference map using values from redux store
*/
async fetchColourAndRedraw(): Promise<void> {
if (this.isDifference) {
console.error('Cannot use moorhen.Map.fetchColourAndRedraw to change difference map colour. Use moorhen.Map.fetchDiffMapColourAndRedraw instead...')
return
}
if (this.otherMapForColouring !== null) {
this.otherMapForColouring = null
}
const { mapAlpha, mapColour } = this.getMapContourParams()
this.displayObjects['Coot'].forEach(buffer => {
if (mapAlpha < 0.99) {
buffer.customColour = null;
buffer.transparent = true
buffer.triangleColours.forEach(colbuffer => {
for (let idx = 0; idx < colbuffer.length; idx += 4) {
colbuffer[idx] = mapColour.r
colbuffer[idx + 1] = mapColour.g
colbuffer[idx + 2] = mapColour.b
}
})
buffer.isDirty = true;
buffer.alphaChanged = true;
} else {
buffer.setCustomColour([mapColour.r,mapColour.g,mapColour.b,1.0])
buffer.transparent = false
}
})
if (mapAlpha < 0.99) {
this.glRef.current.buildBuffers();
}
this.glRef.current.drawScene();
}
/**
* Fetch the map alpha (transparency) for this map using values from redux store and redraw the map
*/
async fetchMapAlphaAndRedraw(): Promise<void> {
const { mapAlpha, mapColour } = this.getMapContourParams()
this.displayObjects['Coot'].forEach(buffer => {
buffer.triangleColours.forEach(colbuffer => {
if (this.isDifference) {
console.log("Setting alpha", mapAlpha)
}
for (let idx = 3; idx < colbuffer.length; idx += 4) {
colbuffer[idx] = mapAlpha
}
})
buffer.isDirty = true
buffer.alphaChanged = true
if (mapAlpha < 0.99) {
buffer.transparent = true
if (buffer.customColour && buffer.customColour.length===4) {
buffer.customColour = null;
if (this.isDifference) {
console.log("Setting colours to",mapColour)
}
buffer.triangleColours.forEach(colbuffer => {
for (let idx = 0; idx < colbuffer.length; idx += 4) {
colbuffer[idx] = mapColour.r
colbuffer[idx + 1] = mapColour.g
colbuffer[idx + 2] = mapColour.b
}
})
}
} else {
buffer.transparent = false
if (buffer.customColour && buffer.customColour.length === 4) {
buffer.setCustomColour([mapColour.r, mapColour.g, mapColour.b, 1.0])
}
}
})
this.glRef.current.buildBuffers();
this.glRef.current.drawScene();
}
/**
* Associate this map with a set of observed reflections
* @param {moorhen.selectedMtzColumns} selectedColumns - Object indicating the selected MTZ columns
* @param {Uint8Array} reflectionData - The reflection data that will be associates to this map
* @returns {Promise<moorhen.WorkerResponse>} - Void promise
*/
async associateToReflectionData (selectedColumns: moorhen.selectedMtzColumns, reflectionData: Uint8Array | ArrayBuffer): Promise<void> {
if (!selectedColumns.Fobs || !selectedColumns.SigFobs || !selectedColumns.FreeR) {
console.warn('WARNING: Missing column data, cannot associate reflection data with map')
return Promise.resolve()
}
const commandArgs = [
this.molNo, { fileName: this.uniqueId, data: reflectionData },
selectedColumns.Fobs, selectedColumns.SigFobs, selectedColumns.FreeR
]
const response = await this.commandCentre.current.cootCommand({
command: 'shim_associate_data_mtz_file_with_map',
commandArgs: commandArgs,
returnType: 'status'
}, false) as moorhen.WorkerResponse<string>
if (response.data.result.status === "Completed") {
this.hasReflectionData = true
this.selectedColumns = {
...this.selectedColumns,
...selectedColumns
}
this.associatedReflectionFileName = response.data.result.result
} else {
console.warn('Unable to associate reflection data with map')
}
}
/**
* Fetch the reflection data associated with this map
* @returns {Promise<moorhen.WorkerResponse<Uint8Array>>} The reflection data
*/
async fetchReflectionData(): Promise<moorhen.WorkerResponse<Uint8Array>> {
if (this.hasReflectionData) {
return await this.commandCentre.current.postMessage({
molNo: this.molNo,
message: 'get_mtz_data',
fileName: this.associatedReflectionFileName
})
} else {
console.log('Map has no reflection data associated...')
}
}
/**
* Create a copy of the map
* @returns {Promise<moorhen.Map>} New map instance
*/
async copyMap(): Promise<moorhen.Map> {
const reply = await this.getMap()
const newMap = new MoorhenMap(this.commandCentre, this.glRef, this.store)
await newMap.loadToCootFromMapData(reply.data.result.mapData, `Copy of ${this.name}`, this.isDifference)
const { mapRadius, contourLevel } = this.getMapContourParams()
newMap.suggestedContourLevel = contourLevel
newMap.suggestedRadius = mapRadius
return newMap
}
/**
* Blur the map
* @param {number} bFactor - The b-factor used for blurring
* @returns {Promise<moorhen.WorkerResponse<number>>} Status (-1 if failure)
*/
blur(bFactor: number): Promise<moorhen.WorkerResponse> {
return this.commandCentre.current.cootCommand({
command: 'sharpen_blur_map',
commandArgs: [this.molNo, bFactor, true],
returnType: "status"
}, true) as Promise<moorhen.WorkerResponse<number>>
}
/**
* Get the current map RMSD
* @returns {number} The map RMSD
*/
async fetchMapRmsd(): Promise<number> {
const result = await this.commandCentre.current.cootCommand({
command: 'get_map_rmsd_approx',
commandArgs: [this.molNo],
returnType: 'float'
}, false) as moorhen.WorkerResponse<number>
this.mapRmsd = result.data.result.result
return result.data.result.result
}
/**
* Get the suggested level for this map instance (only for MX maps)
* @returns {number} The suggested map contour level
*/
async fetchSuggestedLevel(): Promise<number> {
const result = await this.commandCentre.current.cootCommand({
command: 'get_suggested_initial_contour_level',
commandArgs: [this.molNo],
returnType: 'float'
}, false) as moorhen.WorkerResponse<number>
if (result.data.result.result !== -1) {
this.suggestedContourLevel = result.data.result.result
} else {
console.log('Problem getting suggested intial map level')
this.suggestedContourLevel = null
}
return result.data.result.result
}
/**
* Get the suggested map centre for this map instance (it will also fetch suggested level for EM maps)
* @returns {number[]} The map centre
*/
async fetchMapCentre(): Promise<[number, number, number]> {
const response = await this.commandCentre.current.cootCommand({
command: 'get_map_molecule_centre',
commandArgs: [this.molNo],
returnType: "map_molecule_centre_info_t"
}, false) as moorhen.WorkerResponse<libcootApi.MapMoleculeCentreInfoJS>
if (response.data.result.result.success) {
this.mapCentre = response.data.result.result.updated_centre.map(coord => -coord) as [number, number, number]
if (this.isEM) {
this.suggestedContourLevel = response.data.result.result.suggested_contour_level
this.suggestedRadius = response.data.result.result.suggested_radius
}
} else {
console.log('Problem finding map centre')
this.mapCentre = null
}
return this.mapCentre
}
/**
* Estimate the map weight based on the map rmsd
*/
async estimateMapWeight(): Promise<void> {
if (this.mapRmsd === null) {
await this.fetchMapRmsd()
}
this.suggestedMapWeight = 50 * 0.3 / this.mapRmsd
}
/**
* Get suggested contour level, radius and map centre for this map instance
*/
async getSuggestedSettings(): Promise<void> {
const response = await this.commandCentre.current.cootCommand({
command: 'is_EM_map',
commandArgs: [this.molNo],
returnType: "boolean"
}, false) as moorhen.WorkerResponse<boolean>
this.isEM = response.data.result.result
await Promise.all([
this.fetchMapRmsd().then(_ => this.estimateMapWeight()),
this.fetchMapCentre(),
this.setDefaultColour(),
this.fetchMapMean(),
!this.isEM && this.fetchSuggestedLevel()
])
}
async fetchMapMean() {
const result = await this.commandCentre.current.cootCommand({
command: 'get_map_mean',
commandArgs: [this.molNo],
returnType: "float"
}, false)
if (result.data.result.status !== "Exception") {
this.mapMean = result.data.result.result
} else {
console.warn(`Unable to fetch map meap for imol ${this.molNo}`)
}
return result.data.result?.result
}
/**
* Set the view in the centre of this map instance
*/
async centreOnMap(): Promise<void> {
if (this.mapCentre === null) {
await this.fetchMapCentre()
if (this.mapCentre === null) {
console.log('Problem finding map centre')
return
}
}
this.glRef.current.setOriginAnimated(this.mapCentre)
}
/**
* Get the histogram data for this map instance
* @returns {object} - An object with the histogram data
*/
async getHistogram(nBins: number = 200, zoomFactor: number = 1): Promise<libcootApi.HistogramInfoJS> {
const response = await this.commandCentre.current.cootCommand({
command: 'get_map_histogram',
commandArgs: [this.molNo, nBins, zoomFactor],
returnType: "histogram_info_t"
}, false) as moorhen.WorkerResponse<any>
return response.data.result.result
}
/**
* Fetch whether this is a difference map
* @returns {boolean} - True if this map instance is a difference map
*/
async fetchIsDifferenceMap(): Promise<boolean> {
const isDifferenceMap = await this.commandCentre.current.cootCommand({
command: 'is_a_difference_map',
commandArgs: [this.molNo],
returnType: "boolean"
}, false) as moorhen.WorkerResponse<boolean>
this.isDifference = isDifferenceMap.data.result.result
return this.isDifference
}
/**
* Set the default colour for this map depending on the current number of maps loaded in the session
*/
async setDefaultColour(): Promise<void> {
if (this.isDifference) {
return
}
const validMapMolNos = await Promise.all([...Array(this.molNo).keys()].map(async (molNo) => {
if (molNo === this.molNo) {
return false
}
const isValidMap = await this.commandCentre.current.cootCommand({
command: 'is_valid_map_molecule',
commandArgs: [molNo],
returnType: "boolean"
}, false) as moorhen.WorkerResponse<boolean>
if (!isValidMap.data.result.result) {
return false
} else {
const isDifferenceMap = await this.commandCentre.current.cootCommand({
command: 'is_a_difference_map',
commandArgs: [molNo],
returnType: "boolean"
}, false) as moorhen.WorkerResponse<boolean>
return !isDifferenceMap.data.result.result
}
}))
const numberOfMaps = validMapMolNos.filter(Boolean).length
let [h, s, v] = rgbToHsv(0.30000001192092896, 0.30000001192092896, 0.699999988079071)
h += (10 * numberOfMaps)
if (h > 360) {
h -= 360
}
const [r, g, b] = hsvToRgb(h, s, v)
this.defaultMapColour = { r, g, b }
}
/**
* Export the map as a gltf in binary format
* @returns {ArrayBuffer} - The contents of the gltf file (binary format)
*/
async exportAsGltf(): Promise<ArrayBuffer> {
const { mapRadius, contourLevel } = this.getMapContourParams()
const result = await this.commandCentre.current.cootCommand({
returnType: "arrayBuffer",
command: 'shim_export_map_as_gltf',
commandArgs: [ this.molNo, ...this.glRef.current.origin.map(coord => -coord), mapRadius, contourLevel ],
changesMolecules: [ ]
}, false) as moorhen.WorkerResponse<ArrayBuffer>
return result.data.result.result
}
}
Source