Module:CineMol
| This module is rated as alpha. It is ready for limited use and third-party feedback. It may be used on a small number of pages, but should be monitored closely. Suggestions for new features or adjustments to input and output are welcome. |
| This module uses TemplateStyles: |
| This module depends on the following other modules: |
Attempt to make a template that can render molecule descriptions. Adapted from CineMol.
It can use Wikidata's P3636 or P683 to decide what to render.
Example
[edit]As an example, the following produces a picture of folic acid using its Wikidata number.
{{CineMol|wikidata=Q127060|style=tube|ry=1.3}}
In most cases you probably want to use {{Image frame}}
{{Image frame|
|align=none
|style=width:250px
|caption=Caffine (wireframe mode)
|content=
{{CineMol
|style=wireframe
|look=cartoon
|pdb-ligand=CFF
|rx=1.57
|rz=1.9625
}}
}}
Usage
[edit]You can specify the molecule either by its pdb-ligand id, its wikidata Q number or the contents of a mol file. If unspecified will default to the Q number of the current page.
{{CineMol|wikidata=<Q id here>|...}}{{CineMol|pdb-ligand=<ligand id here>|...}}{{CineMol|mol=<SDF FILE HERE>|...}}
You can use the rx, ry, and rz parameters to adjust the viewpoint.
The style parameter determines the type of drawing. The choices are:
- Space filling
- Ball and stick
- Tube
- Wireframe
The look parameter determines the look of the drawing. There are two choices: Glossy and Cartoon.
By default hyrdogen atoms are not shown. To include them specify hydrogen=yes.
See template data for additional parameters.
Template data
[edit]Display diagrams of molecules based on a mol file or ligand id
| Parameter | Description | Type | Status | |
|---|---|---|---|---|
| style | style | Style of molecule drawing to do
| String | suggested |
| look | look | Look of rendering (Cartoon or glossy)
| String | suggested |
| resolution | resolution | Resolution of drawing (Todo: Clarify this)
| Number | optional |
| scale | scale | What scale to draw image at
| Number | optional |
| focal_length | focal_length | focal length of camera (This might not work properly) | Number | optional |
| rotation over x axis | rx | How much to rotate the x axis
| Number | optional |
| rotation over y axis | ry | How much to rotate the y axis
| Number | optional |
| rotation over z axis | rz | How much to rotate over the z axis
| Number | optional |
| hydrogen | hydrogen | Show hydrogen molecules
| Boolean | optional |
| lightbox | lightbox | Allow clicking to show a zoomed version of image.
| Boolean | optional |
| width | width | Width of images in pixels
| Number | suggested |
| height | height | Height in pixels. (Note: supplying both height and width will adjust the aspect ratio) | Number | optional |
| mol | mol | Contents of mol/sdf file to render | String | optional |
| pdb-ligand | pdb-ligand | The id of a compound in PDB's chemical component dictionary | String | optional |
| ChEBI | ChEBI | ChEBI id of molecule to render | String | optional |
| wikidata | wikidata | A Wikidata Q-ID to use the value of P3636 to render a molecule. Will default to the current page | String | optional |
| css | css | Extra CSS to apply to the image | String | optional |
| alt | alt | Alternate text for screen readers that can't view images | String | optional |
| class | class | CSS class to apply to image | String | optional |
| title | title | Tooltip to display on hover | String | optional |
| link | link | Either url or pagename to link image to | String | optional |
| lightbox_caption | lightbox_caption | Caption for when user clicks to zoom. If unspecified and using a wikidata or pdb-ligend source, a caption will be automatically generated. | Content | optional |
local p = {}
local compress = require( 'Module:libDeflate' )
require( 'strict' )
local m = require( 'Module:CineMol/api' )
local parse_sdf = require( 'Module:CineMol/parsers' ).parse_sdf
local getArgs = require( 'Module:Arguments' ).getArgs
local yesno = require( 'Module:Yesno' )
-- Stolen from Module:Cite_taxon
local b64decode = function(data)
local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
data = string.gsub(data, '[^'..b..'=]', '')
return (data:gsub('.', function(x)
if (x == '=') then return '' end
local r,f='',(b:find(x)-1)
for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
return r;
end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
if (#x ~= 8) then return '' end
local c=0
for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
return string.char(c)
end))
end
-- Keep in sync with Category:Tabular_data_of_the_Chemical_Component_Dictionary
local function getKey( id )
if string.sub( id, 1, 1 ) == 'A' then
local letter2 = #id <= 1 and 0 or string.byte( id, 2 )
local letter3 = #id <= 2 and 0 or string.byte( id, 3 )
if
letter2 < string.byte( '1' ) or
( string.sub( id, 1, 2 ) == 'A1' and letter3 < string.byte( 'B' ) )
then
return 'A'
elseif string.sub( id, 1, 2 ) == 'A1' and letter3 < string.byte( 'I' ) then
return 'A1B';
elseif string.sub( id, 1, 2 ) == 'A1' then
return 'A1I'
else
return 'A2'
end
else
return string.sub( id, 1, 1)
end
end
local function is2D( mol )
return string.match( mol, "^[^\n]*\n[^\n]*(2D)[\n ]" ) == '2D'
end
local function getMolFromCCD( id )
local data = mw.ext.data.get( string.format( "Chemical Component Dictionary/%s.tab", getKey(id) ) )
assert( data ~= false, 'Could not fetch CCD file from commons')
local ccd = data['data']
local molEncoded = ''
for i = 1,#ccd do
-- This assumes entries are in order.
-- if this becomes an efficiency issue, we could binary search it instead.
if ccd[i][1] == id then
molEncoded = molEncoded .. ccd[i][3]
end
end
assert( molEncoded ~= '', 'Could not find PDB-lignad id: ' .. id )
return compress:DecompressDeflate( b64decode( molEncoded ) )
end
local function getMolFromChEBI( id )
if string.upper(string.sub( id, 1, 6 )) == 'CHEBI:' then
id = string.sub( id, 7 )
end
local idNumb = tonumber( id )
local key
if idNumb < 160000 then
key = math.floor( idNumb / 2000 ) * 2000
elseif idNumb < 200000 then
key = math.floor( idNumb / 20000 ) * 20000
else
key = 200000
end
local data = mw.ext.data.get( string.format( "ChEBI/%d.tab", key ) )
assert( data ~= false, 'Could not fetch ChEBI data file from commons')
local dataset = data['data']
local molEncoded = ''
for i = 1,#dataset do
-- This assumes entries are in order.
-- if this becomes an efficiency issue, we could binary search it instead.
if dataset[i][1] == id then
molEncoded = molEncoded .. dataset[i][4]
end
end
assert( molEncoded ~= '', 'Could not find ChEBI: ' .. id )
return compress:DecompressDeflate( b64decode( molEncoded ) )
end
p.styleMap = {
spacefilling = m.Style.SPACEFILLING,
space_filling = m.Style.SPACEFILLING,
["space filling"] = m.Style.SPACEFILLING,
ballandstick = m.Style.BALL_AND_STICK,
ball_and_stick = m.Style.BALL_AND_STICK,
["ball and stick"] = m.Style.BALL_AND_STICK,
tube = m.Style.TUBE,
wireframe = m.Style.WIREFRAME
}
p.lookMap = {
cartoon = m.Look.CARTOON,
glossy = m.Look.GLOSSY
}
function p.draw_mol(frame)
local args = getArgs( frame )
return frame:extensionTag( "TemplateStyles", "", {src="Module:CineMol/styles.css"} )
.. p.draw_mol_internal(args)
end
-- Get automatic caption if using a ligand id
local function getAutoCaptionPdb(ligand)
local license = '<div class="cinemol-license-img" title="Public domain">[[File:Cc.logo.circle.svg|28px|class=skin-invert notpageimage|link=|Creative Commons]] [[File:Cc-zero.svg|28px|class=skin-invert notpageimage|link=https://creativecommons.org/publicdomain/zero/1.0/deed.en|CC-Zero]]</div>'
return license .. 'Rendered based on the entry in the [https://www.wwpdb.org/data/ccd Chemical Component Dictionary] for the molecule with id "[https://www.rcsb.org/ligand/' .. ligand .. ' ' .. ligand .. ']".'
end
local function getAutoCaptionChEBI(id)
local license = '<div class="cinemol-license-img" title="Creative Commons Attribution 4.0">[[File:Cc.logo.circle.svg|28px|class=skin-invert notpageimage|link=https://creativecommons.org/licenses/by/4.0/deed.en|Creative Commons]] [[File:Cc-by_new_white.svg|28px|class=skin-invert notpageimage|link=https://creativecommons.org/licenses/by/4.0/deed.en|CC-BY 4.0]]</div>'
return license .. 'Rendered based on the entry in the [[w:ChEBI|Chemical Entities of Biological Interest]] for the molecule with id "[https://www.ebi.ac.uk/chebi/searchId.do?chebiId=CHEBI:' .. id .. ' CHEBI:' .. id .. ']".'
end
function p.draw_mol_internal(args)
local style = args.style == nil and m.Style.SPACEFILLING or p.styleMap[string.lower(args.style)]
assert( style ~= nil, 'Unrecognized style' )
local look = args.look == nil and m.Look.GLOSSY or p.lookMap[string.lower(args.look)]
assert( look ~= nil, 'Unrecognized look' )
local resolution = args.resolution == nil and 30 or tonumber(args.resolution)
assert( type(resolution) == 'number', 'Resolution must be number' )
-- We can never zoom in so far that the molecule is out of frame, so best this default
-- a high number.
local scale = args.scale == nil and 5 or tonumber(args.scale)
assert( type(scale) == 'number', 'Scale must be number' )
local focal_length = args.focal_length ~= nil and tonumber(args.focal_length) or nil
local rx = args.rx == nil and 0 or tonumber(args.rx)
local ry = args.ry == nil and 0 or tonumber(args.ry)
local rz = args.rz == nil and 0 or tonumber(args.rz)
local useLightbox = yesno( args.lightbox, true ) or true
local hydrogen = yesno( args.include_hydrogen, false ) or false
local exclude = hydrogen and {} or {'H'}
local width = args.width
if args.height == nil and args.width == nil then
width = "250"
end
local internalLink = nil
local externalLink = nil
if args.link ~= nil then
if string.match( args.link, '^https?://' ) or string.sub( args.link, 1, 2 ) == '//' then
externalLink = args.link
useLightbox = false
else
internalLink = args.link
useLightbox = false
end
end
local lightboxCaption = args.lightbox_caption
local mol
if args.mol ~= nil and args.mol ~= '' then
mol = args.mol
elseif args['pdb-ligand'] ~= nil and args['pdb-ligand'] ~= '' then
mol = getMolFromCCD( args['pdb-ligand'] )
lightboxCaption = lightboxCaption == nil and getAutoCaptionPdb( args['pdb-ligand'] ) or lightboxCaption
elseif args.ChEBI ~= nil and args.ChEBI ~= '' then
mol = getMolFromChEBI( args.ChEBI )
lightboxCaption = lightboxCaption == nil and getAutoCaptionChEBI( args.ChEBI ) or lightboxCaption
else
local wd = args.wikidata == nil and mw.wikibase.getEntityIdForCurrentPage() or args.wikidata
if wd ~= nil then
local stmt = mw.wikibase.getBestStatements( wd, 'P3636' )
if stmt and stmt[1] and stmt[1].mainsnak then
local ccd = stmt[1].mainsnak.datavalue.value
mol = getMolFromCCD( ccd )
lightboxCaption = lightboxCaption == nil and getAutoCaptionPdb( ccd ) or lightboxCaption
else
stmt = mw.wikibase.getBestStatements( wd, 'P683' )
if stmt and stmt[1] and stmt[1].mainsnak then
local ChEBI = stmt[1].mainsnak.datavalue.value
mol = getMolFromChEBI( ChEBI )
lightboxCaption = lightboxCaption == nil and getAutoCaptionChEBI( ChEBI ) or lightboxCaption
end
end
end
end
assert( mol ~= nil and mol ~= '', 'Either mol, pdf-ligand, ChEBI or wikidata argument is required or the current page has to be connected to a wikidata item with P3636' )
local style = args.style == nil and ( is2D(mol) and m.Style.WIREFRAME or m.Style.SPACEFILLING ) or p.styleMap[string.lower(args.style)]
assert( style ~= nil, 'Unrecognized style' )
-- Actual template
local atoms, bonds = parse_sdf( mol )
local molSvg = m.draw_molecule( atoms, bonds, style, look, resolution, nil, nil, rx, ry, rz, scale, focal_length, exclude )
local mw_svg = molSvg
:to_mw_svg()
if ( width ) then
mw_svg:setImgAttribute( 'width', tostring( width ) )
end
if ( args.css ) then
mw_svg:setImgAttribute( 'style', tostring( args.css ) )
end
if ( args.alt ) then
mw_svg:setImgAttribute( 'alt', tostring( args.alt ) )
end
if ( args.class ) then
end
if ( args.height ) then
mw_svg:setImgAttribute( 'height', tostring( args.height ) )
end
if ( args.title ) then
mw_svg:setImgAttribute( 'title', tostring( args.title ) )
end
local class = 'cinemol-img'
if args.class then
class = class .. ' ' .. args.class
end
mw_svg:setImgAttribute( 'class', class )
local start = '<div class="plainlinks cinemol-container calculator-container">'
local tail = '</div>'
if useLightbox then
start = start .. '<div class="calculator-field cinemol-inner-container" data-calculator-type="passthru" id="calculator-field-lightbox">'
tail = '</div>' .. tail
end
if lightboxCaption and useLightbox then
-- potentially should have an aria-described-by pointing to it.
tail = '<div class="cinemol-lightbox-caption">' .. lightboxCaption .. '</div>' .. tail
end
if useLightbox then
start = start .. '<div class="cinemol-lightbox-controls">' .. mw.getCurrentFrame():preprocess('{{calculator button|contents=×|style=appearance:none|alt=Exit molecule lightbox view|title=Exit molecule lightbox view|for=lightbox|formula=0|type=plain}}') .. '</div>'
-- For now you can only open by click not by tab. It is unclear if there is any value for a screen reader
-- to zoom in on an image.
start = start .. '<div class="cinemol-inner calculator-field-buttonraw" data-calculator-for="lightbox" data-calculator-formula="1">'
tail = '</div>' .. tail
end
if internalLink then
start = start .. '[[' .. internalLink .. '|'
tail = ']]' .. tail
elseif externalLink then
start = start .. '[' .. externalLink .. ' '
tail = ']' .. tail
end
return start .. mw_svg:toImage() .. tail
end
-- Demo directly using the api
function p.spacefilling(frame)
local width = frame.args.width == nil and '250' or frame.args.width
local svg = m.draw_molecule(
{
m.Atom(0, "C", {0, 0, 0}),
m.Atom(1, "H", {1, 0, 0}),
m.Atom(2, "H", {0, 1, 0}),
m.Atom(3, "H", {0, 0, 1})
},
{
m.Bond(0, 1, 1),
m.Bond(0, 2, 1),
m.Bond(0, 3, 1),
},
m.Style.SPACEFILLING,
m.Look.GLOSSY,
50
)
return svg:to_mw_svg():setImgAttribute( 'width', tostring(width) ):toImage()
end
return p