Skip to content
Snippets Groups Projects
Commit 274e5145 authored by Alexander Rose's avatar Alexander Rose
Browse files

openapi schema and swaggerui for volume-server

parent 25283aae
No related branches found
No related tags found
No related merge requests found
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as express from 'express'
import * as fs from 'fs'
import { getAbsoluteFSPath } from 'swagger-ui-dist'
import { ServeStaticOptions } from 'serve-static';
import { interpolate } from 'mol-util/string';
export function swaggerUiAssetsHandler(options?: ServeStaticOptions) {
const opts = options || {}
opts.index = false
return express.static(getAbsoluteFSPath(), opts)
}
function createHTML(swaggerUrl: string, apiPrefix: string) {
const htmlTemplate = fs.readFileSync(`${__dirname}/indexTemplate.html`).toString()
return interpolate(htmlTemplate, { swaggerUrl, apiPrefix })
}
export function swaggerUiIndexHandler(swaggerUrl: string, apiPrefix: string): express.Handler {
const html = createHTML(swaggerUrl, apiPrefix)
return (req: express.Request, res: express.Response) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
}
}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="${apiPrefix}/swagger-ui.css" >
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="${apiPrefix}/swagger-ui-bundle.js"> </script>
<script src="${apiPrefix}/swagger-ui-standalone-preset.js"> </script>
<script>
function HidePlugin() {
// this plugin overrides some components to return nothing
return {
components: {
Topbar: function () { return null },
Models: function () { return null },
}
}
}
window.onload = function () {
var ui = SwaggerUIBundle({
url: '${swaggerUrl}',
validatorUrl: null,
docExpansion: 'list',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl,
HidePlugin
],
layout: 'StandaloneLayout'
})
window.ui = ui
}
</script>
</body>
</html>
\ No newline at end of file
......@@ -26,16 +26,21 @@ export function getOutputFilename(source: string, id: string, { asBinary, box, d
return `${n(source)}_${n(id)}-${boxInfo}_${det}.${asBinary ? 'bcif' : 'cif'}`;
}
export interface ExtendedHeader extends DataFormat.Header {
availablePrecisions: { precision: number, maxVoxels: number }[]
isAvailable: boolean
}
/** Reads the header and includes information about available detail levels */
export async function getHeaderJson(filename: string | undefined, sourceId: string) {
export async function getExtendedHeaderJson(filename: string | undefined, sourceId: string) {
ConsoleLogger.log('Header', sourceId);
try {
if (!filename || !File.exists(filename)) {
ConsoleLogger.error(`Header ${sourceId}`, 'File not found.');
return void 0;
}
const header = { ...await readHeader(filename, sourceId) } as DataFormat.Header;
const { sampleCount } = header!.sampling[0];
const header: Partial<ExtendedHeader> = { ...await readHeader(filename, sourceId) };
const { sampleCount } = header.sampling![0];
const maxVoxelCount = sampleCount[0] * sampleCount[1] * sampleCount[2];
const precisions = LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel
.map((maxVoxels, precision) => ({ precision, maxVoxels }));
......@@ -44,8 +49,8 @@ export async function getHeaderJson(filename: string | undefined, sourceId: stri
availablePrecisions.push(p);
if (p.maxVoxels > maxVoxelCount) break;
}
(header as any).availablePrecisions = availablePrecisions;
(header as any).isAvailable = true;
header.availablePrecisions = availablePrecisions;
header.isAvailable = true;
return JSON.stringify(header, null, 2);
} catch (e) {
ConsoleLogger.error(`Header ${sourceId}`, e);
......
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer)
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import VERSION from './version'
import { LimitsConfig } from '../config';
export function getDocumentation() {
function detail(i: number) {
return `<span class='id'>${i}</span><small> (${Math.round(100 * LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel[i] / 1000 / 1000) / 100 }M voxels)</small>`;
}
const detailMax = LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1;
// TODO get from config
const dataSource = `Specifies the data source (determined by the experiment method). Currently, <span class='id'>x-ray</span> and <span class='id'>em</span> sources are supported.`;
const entryId = `Id of the entry. For <span class='id'>x-ray</span>, use PDB ID (i.e. <span class='id'>1cbs</span>) and for <span class='id'>em</span> use EMDB id (i.e. <span class='id'>emd-8116</span>).`;
return `<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<link rel='shortcut icon' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAnUExURQAAAMIrHrspHr0oH7soILonHrwqH7onILsoHrsoH7soH7woILwpIKgVokoAAAAMdFJOUwAQHzNxWmBHS5XO6jdtAmoAAACZSURBVDjLxZNRCsQgDAVNXmwb9f7nXZEaLRgXloXOhwQdjMYYwpOLw55fBT46KhbOKhmRR2zLcFJQj8UR+HxFgArIF5BKJbEncC6NDEdI5SatBRSDJwGAoiFDONrEJXWYhGMIcRJGCrb1TOtDahfUuQXd10jkFYq0ViIrbUpNcVT6redeC1+b9tH2WLR93Sx2VCzkv/7NjfABxjQHksGB7lAAAAAASUVORK5CYII=' />
<title>VolumeServer (${VERSION})</title>
<style>
html { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; }
body { margin: 0; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; font-weight: 300; color: #333; line-height: 1.42857143; font-size: 14px }
.container { padding: 0 15px; max-width: 970px; margin: 0 auto; }
small { font-size: 80% }
h2, h4 { font-weight: 500; line-height: 1.1; }
h2 { color: black; font-size: 24px; }
h4 { font-size: 18px; margin: 20px 0 10px 0 }
h2 small { color: #777; font-weight: 300 }
hr { box-sizing: content-box; height: 0; overflow: visible; }
a { background-color: transparent; -webkit-text-decoration-skip: objects; text-decoration: none }
a:active, a:hover { outline-width: 0; }
a:focus, a:hover { text-decoration: underline; color: #23527c }
.list-unstyled { padding: 0; list-style: none; margin: 0 0 10px 0 }
.cs-docs-query-wrap { padding: 24px 0; border-bottom: 1px solid #eee }
.cs-docs-query-wrap > h2 { margin: 0; color: black; }
.cs-docs-query-wrap > h2 > span { color: #DE4D4E; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; font-size: 90% }
.cs-docs-param-name, .cs-docs-template-link { color: #DE4D4E; font-family: Menlo,Monaco,Consolas,"Courier New",monospace }
table {margin: 0; padding: 0; }
table th { font-weight: bold; border-bottom: none; text-align: left; padding: 6px 12px }
td { padding: 6px 12px }
td:not(:last-child), th:not(:last-child) { border-right: 1px dotted #ccc }
tr:nth-child(even) { background: #f9f9f9 }
span.id { color: #DE4D4E; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; }
</style>
</head>
<body>
<div class="container">
<div style='text-align: center; margin-top: 24px;'><span style='font-weight: bold; font-size: 16pt'>VolumeServer</span> <span>${VERSION}</span></div>
<div style='text-align: justify; padding: 24px 0; border-bottom: 1px solid #eee'>
<p>
<b>VolumeServer</b> is a service for accessing subsets of volumetric density data. It automatically downsamples the data
depending on the volume of the requested region to reduce the bandwidth requirements and provide near-instant access to even the
largest data sets.
</p>
<p>
It uses the text based <a href='https://en.wikipedia.org/wiki/Crystallographic_Information_File'>CIF</a> and binary
<a href='https://github.com/dsehnal/BinaryCIF' style='font-weight: bold'>BinaryCIF</a>
formats to deliver the data to the client.
The server support is integrated into the <a href='https://github.com/dsehnal/LiteMol' style='font-weight: bold'>LiteMol Viewer</a>.
</p>
</div>
<div class="cs-docs-query-wrap">
<h2>Data Header / Check Availability <span>/&lt;source&gt;/&lt;id&gt;</span><br>
<small>Returns a JSON response specifying if data is available and the maximum region that can be queried.</small></h2>
<div id="coordserver-documentation-ambientResidues-body" style="margin: 24px 24px 0 24px">
<h4>Examples</h4>
<a href="/VolumeServer/x-ray/1cbs" class="cs-docs-template-link" target="_blank" rel="nofollow">/x-ray/1cbs</a><br>
<a href="/VolumeServer/em/emd-8116" class="cs-docs-template-link" target="_blank" rel="nofollow">/em/emd-8116</a>
<h4>Parameters</h4>
<table cellpadding="0" cellspacing="0" style='width: 100%'>
<tbody><tr><th style='width: 80px'>Name</th><th>Description</th></tr>
<tr>
<td class="cs-docs-param-name">source</td>
<td>${dataSource}</td>
</tr>
<tr>
<td class="cs-docs-param-name">id</td>
<td>${entryId}</td>
</tr>
</tbody></table>
</div>
</div>
<div class="cs-docs-query-wrap">
<h2>Box <span>/&lt;source&gt;/&lt;id&gt;/box/&lt;a,b,c&gt;/&lt;u,v,w&gt;?&lt;optional parameters&gt;</span><br>
<small>Returns density data inside the specified box for the given entry. For X-ray data, returns 2Fo-Fc and Fo-Fc volumes in a single response.</small></h2>
<div style="margin: 24px 24px 0 24px">
<h4>Examples</h4>
<a href="/VolumeServer/em/emd-8003/box/-2,7,10/4,10,15.5?encoding=cif&space=cartesian" class="cs-docs-template-link" target="_blank" rel="nofollow">/em/emd-8003/box/-2,7,10/4,10,15.5?excoding=cif&space=cartesian</a><br>
<a href="/VolumeServer/x-ray/1cbs/box/0.1,0.1,0.1/0.23,0.31,0.18?space=fractional" class="cs-docs-template-link" target="_blank" rel="nofollow">/x-ray/1cbs/box/0.1,0.1,0.1/0.23,0.31,0.18?space=fractional</a>
<h4>Parameters</h4>
<table cellpadding="0" cellspacing="0" style='width: 100%'>
<tbody><tr><th style='width: 80px'>Name</th><th>Description</th></tr>
<tr>
<td class="cs-docs-param-name">source</td>
<td>${dataSource}</td>
</tr>
<tr>
<td class="cs-docs-param-name">id</td>
<td>${entryId}</td>
</tr>
<tr>
<td class="cs-docs-param-name">a,b,c</td>
<td>Bottom left corner of the query region in Cartesian or fractional coordinates (determined by the <span class='id'>&amp;space</span> query parameter).</td>
</tr>
<tr>
<td class="cs-docs-param-name">u,v,w</td>
<td>Top right corner of the query region in Cartesian or fractional coordinates (determined by the <span class='id'>&amp;space</span> query parameter).</td>
</tr>
<tr>
<td class="cs-docs-param-name">encoding</td>
<td>Determines if text based <span class='id'>CIF</span> or binary <span class='id'>BinaryCIF</span> encoding is used. An optional argument, default is <span class='id'>BinaryCIF</span> encoding.</td>
</tr>
<tr>
<td class="cs-docs-param-name">space</td>
<td>Determines the coordinate space the query is in. Can be <span class='id'>cartesian</span> or <span class='id'>fractional</span>. An optional argument, default values is <span class='id'>cartesian</span>.</td>
</tr>
<tr>
<td class="cs-docs-param-name">detail</td>
<td>
Determines the maximum number of voxels the query can return. Possible values are in the range from ${detail(0)} to ${detail(detailMax)}.
Default value is <span class='id'>0</span>. Note: different detail levels might lead to the same result.
</td>
</tr>
</tbody></table>
</div>
</div>
<div class="cs-docs-query-wrap">
<h2>Cell <span>/&lt;source&gt;/&lt;id&gt;/cell?&lt;optional parameters&gt;</span><br>
<small>Returns (downsampled) volume data for the entire "data cell". For X-ray data, returns unit cell of 2Fo-Fc and Fo-Fc volumes, for EM data returns everything.</small></h2>
<div style="margin: 24px 24px 0 24px">
<h4>Example</h4>
<a href="/VolumeServer/em/emd-8116/cell?detail=1" class="cs-docs-template-link" target="_blank" rel="nofollow">/em/emd-8116/cell?detail=1</a><br>
<h4>Parameters</h4>
<table cellpadding="0" cellspacing="0" style='width: 100%'>
<tbody><tr><th style='width: 80px'>Name</th><th>Description</th></tr>
<tr>
<td class="cs-docs-param-name">source</td>
<td>${dataSource}</td>
</tr>
<tr>
<td class="cs-docs-param-name">id</td>
<td>${entryId}</td>
</tr>
<tr>
<td class="cs-docs-param-name">encoding</td>
<td>Determines if text based <span class='id'>CIF</span> or binary <span class='id'>BinaryCIF</span> encoding is used. An optional argument, default is <span class='id'>BinaryCIF</span> encoding.</td>
</tr>
<tr>
<td class="cs-docs-param-name">detail</td>
<td>
Determines the maximum number of voxels the query can return. Possible values are in the range from ${detail(0)} to ${detail(detailMax)}.
Default value is <span class='id'>0</span>. Note: different detail levels might lead to the same result.
</td>
</tr>
</tbody></table>
</div>
</div>
<div style="color: #999;font-size:smaller;margin: 20px 0; text-align: right">&copy; 2016 &ndash; now, David Sehnal | Node ${process.version}</div>
</body>
</html>`;
}
\ No newline at end of file
......@@ -10,19 +10,19 @@
import * as express from 'express'
import * as Api from './api'
import * as Data from './query/data-model'
import * as Coords from './algebra/coordinate'
import { getDocumentation } from './documentation'
import { ConsoleLogger } from 'mol-util/console-logger'
import { State } from './state'
import { LimitsConfig, ServerConfig } from '../config';
import { interpolate } from 'mol-util/string';
import { getSchema } from './web-schema';
import { swaggerUiIndexHandler, swaggerUiAssetsHandler } from 'servers/common/swagger-ui';
export default function init(app: express.Express) {
app.locals.mapFile = getMapFileFn()
function makePath(p: string) {
return ServerConfig.apiPrefix + '/' + p;
return `${ServerConfig.apiPrefix}/${p}`;
}
// Header
......@@ -32,10 +32,17 @@ export default function init(app: express.Express) {
// Cell /:src/:id/cell/?text=0|1&space=cartesian|fractional
app.get(makePath(':source/:id/cell/?'), (req, res) => queryBox(req, res, getQueryParams(req, true)));
app.get('*', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(getDocumentation());
app.get(makePath('openapi.json'), (req, res) => {
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'X-Requested-With'
});
res.end(JSON.stringify(getSchema()));
});
app.use(makePath(''), swaggerUiAssetsHandler());
app.get(makePath(''), swaggerUiIndexHandler(makePath('openapi.json'), ServerConfig.apiPrefix));
}
function getMapFileFn() {
......@@ -115,7 +122,7 @@ async function getHeader(req: express.Request, res: express.Response) {
try {
const { filename, id } = getSourceInfo(req);
const header = await Api.getHeaderJson(filename, id);
const header = await Api.getExtendedHeaderJson(filename, id);
if (!header) {
res.writeHead(404);
return;
......
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import VERSION from './version'
import { LimitsConfig, ServerConfig } from '../config';
export function getSchema() {
function detail(i: number) {
return `${i} (${Math.round(100 * LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel[i] / 1000 / 1000) / 100 }M voxels)`;
}
const detailMax = LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1;
const sources = ServerConfig.idMap.map(m => m[0])
return {
openapi: '3.0.0',
info: {
version: VERSION,
title: 'Volume Server',
description: 'The VolumeServer is a service for accessing subsets of volumetric data. It automatically downsamples the data depending on the volume of the requested region to reduce the bandwidth requirements and provide near-instant access to even the largest data sets.',
},
tags: [
{
name: 'General',
}
],
paths: {
[`${ServerConfig.apiPrefix}/{source}/{id}/`]: {
get: {
tags: ['General'],
summary: 'Returns a JSON response specifying if data is available and the maximum region that can be queried.',
operationId: 'getInfo',
parameters: [
{ $ref: '#/components/parameters/source' },
{ $ref: '#/components/parameters/id' },
],
responses: {
200: {
description: 'Volume availability and info',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/info' }
}
}
},
},
}
},
[`${ServerConfig.apiPrefix}/{source}/{id}/box/{a1,a2,a3}/{b1,b2,b3}/`]: {
get: {
tags: ['General'],
summary: 'Returns density data inside the specified box for the given entry. For X-ray data, returns 2Fo-Fc and Fo-Fc volumes in a single response.',
operationId: 'getBox',
parameters: [
{ $ref: '#/components/parameters/source' },
{ $ref: '#/components/parameters/id' },
{
name: 'bottomLeftCorner',
in: 'path',
description: 'Bottom left corner of the query region in Cartesian or fractional coordinates (determined by the `space` query parameter).',
required: true,
schema: {
type: 'list',
items: {
type: 'float',
}
},
style: 'simple'
},
{
name: 'topRightCorner',
in: 'path',
description: 'Top right corner of the query region in Cartesian or fractional coordinates (determined by the `space` query parameter).',
required: true,
schema: {
type: 'list',
items: {
type: 'float',
}
},
style: 'simple'
},
{ $ref: '#/components/parameters/encoding' },
{ $ref: '#/components/parameters/detail' },
{
name: 'space',
in: 'query',
description: 'Determines the coordinate space the query is in. Can be cartesian or fractional. An optional argument, default values is cartesian.',
schema: {
type: 'string',
enum: ['cartesian', 'fractional']
},
style: 'form'
}
],
responses: {
200: {
description: 'Volume box',
content: {
'text/plain': {},
'application/octet-stream': {},
}
},
},
}
},
[`${ServerConfig.apiPrefix}/{source}/{id}/cell/`]: {
get: {
tags: ['General'],
summary: 'Returns (downsampled) volume data for the entire "data cell". For X-ray data, returns unit cell of 2Fo-Fc and Fo-Fc volumes, for EM data returns everything.',
operationId: 'getCell',
parameters: [
{ $ref: '#/components/parameters/source' },
{ $ref: '#/components/parameters/id' },
{ $ref: '#/components/parameters/encoding' },
{ $ref: '#/components/parameters/detail' },
],
responses: {
200: {
description: 'Volume cell',
content: {
'text/plain': {},
'application/octet-stream': {},
}
},
},
}
}
},
components: {
schemas: {
// TODO how to keep in sync with (or derive from) `api.ts/ExtendedHeader`
info: {
properties: {
formatVersion: {
type: 'string',
description: 'Format version number'
},
axisOrder: {
type: 'array',
items: { type: 'number' },
description: 'Axis order from the slowest to fastest moving, same as in CCP4'
},
origin: {
type: 'array',
items: { type: 'number' },
description: 'Origin in fractional coordinates, in axisOrder'
},
dimensions: {
type: 'array',
items: { type: 'number' },
description: 'Dimensions in fractional coordinates, in axisOrder'
},
spacegroup: {
properties: {
number: { type: 'number' },
size: {
type: 'array',
items: { type: 'number' }
},
angles: {
type: 'array',
items: { type: 'number' }
},
isPeriodic: {
type: 'boolean',
description: 'Determine if the data should be treated as periodic or not. (e.g. X-ray = periodic, EM = not periodic)'
},
}
},
channels: {
type: 'array',
items: { type: 'string' }
},
valueType: {
type: 'string',
enum: ['float32', 'int16', 'int8'],
description: 'Determines the data type of the values'
},
blockSize: {
type: 'number',
description: 'The value are stored in blockSize^3 cubes'
},
sampling: {
type: 'array',
items: {
properties: {
byteOffset: { type: 'number' },
rate: {
type: 'number',
description: 'How many values along each axis were collapsed into 1'
},
valuesInfo: {
properties: {
mean: { type: 'number' },
sigma: { type: 'number' },
min: { type: 'number' },
max: { type: 'number' },
}
},
sampleCount: {
type: 'array',
items: { type: 'number' },
description: 'Number of samples along each axis, in axisOrder'
},
}
}
}
}
}
},
parameters: {
source: {
name: 'source',
in: 'path',
description: `Specifies the data source (determined by the experiment method). Currently supported sources are: ${sources.join(', ')}.`,
required: true,
schema: {
type: 'string',
enum: sources
},
style: 'simple'
},
id: {
name: 'id',
in: 'path',
description: 'Id of the entry. For x-ray, use PDB ID (i.e. 1cbs) and for em use EMDB id (i.e. emd-8116).',
required: true,
schema: {
type: 'string',
},
style: 'simple'
},
encoding: {
name: 'encoding',
in: 'query',
description: 'Determines if text based CIF or binary BinaryCIF encoding is used. An optional argument, default is BinaryCIF encoding.',
schema: {
type: 'string',
enum: ['cif', 'bcif']
},
style: 'form'
},
detail: {
name: 'detail',
in: 'query',
description: `Determines the maximum number of voxels the query can return. Possible values are in the range from ${detail(0)} to ${detail(detailMax)}. Default value is 0. Note: different detail levels might lead to the same result.`,
schema: {
type: 'integer',
},
style: 'form'
}
}
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment