Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finish setting center-of-rotation feature. Implement a Docker Container to build/server project #88

Merged
merged 3 commits into from
Jan 18, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
FROM ubuntu:20.04

#
# Dockerfile for the 4DGB Browser Runner
#
# This Dockerfile installs all the necessary Python3 and Javascript
# dependencies for the 4DGB Browser.
# When run, a container will import a project from whatever is mounted
# at the '/project' directory, build a release from it, then start up
# a server via gunicorn for it.
#

# Install dependencies
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
python3 python3 python3-pip rsync cpanminus gosu \
ca-certificates build-essential curl
RUN ln -s /usr/bin/python3 /usr/bin/python
# Install scroller (for pretty output!)
RUN cpanm Term::Scroller

# Setup NodeJS PPA
# Download setup script and verify hash
RUN curl -fsSL 'https://deb.nodesource.com/setup_16.x' \
> setup_16.x \
&& [ "$(sha256sum setup_16.x | cut -d' ' -f1)" \
= "a112ad2cf36a1a2e3e233310740de79d2370213f757ca1b7f93de2f744fb265c" ]
RUN bash setup_16.x
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs

#Setup Directories
WORKDIR /srv

# Persistent volume to store the project release directory
VOLUME /srv/release

# (We copy these files first because we don't exepct them to change as much)
COPY bin/db_pop ./
COPY bin/docker-entrypoint ./entrypoint
COPY bin/docker-setup ./setup
RUN chmod +x ./entrypoint ./setup

# Server and Python Stuff
COPY server/ ./server
RUN pip3 install -r ./server/requirements.txt && pip3 install gunicorn pandas

# Javascript Stuff
COPY package.json ./
RUN npm install
COPY client-js ./client-js
RUN npx webpack --config client-js/webpack.config.js

ENV BROWSERCONTAINER "yes"

EXPOSE 8000

ENTRYPOINT [ "./entrypoint" ]
19 changes: 19 additions & 0 deletions bin/docker-entrypoint
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

#
# The entrypoint for the browser runner container.
# Sets permissions on the /srv directory, changes
# user to www-data and executes the setup script.
#
# We do this at runtime (instead of a 'USER' directive in the Dockefile)
# because we need to set permissions on the in the persistent volume
# for the release directory (which isn't mounted until runtime)
#

if [ "$BROWSERCONTAINER" != "yes" ] ; then
echo "This script needs to be run inside the 4DGB Browser docker container" 1>&2
exit 1
fi

chown -R www-data:www-data /srv
gosu www-data /srv/setup
123 changes: 123 additions & 0 deletions bin/docker-setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/bin/bash

#
# Docker container setup script
#
# This will import a project from the mounted '/project' directory,
# build a release for it, then start up a server instance with gunicorn

########################
# CONSTANTS
########################

INPUTDIR=/project # Bind-mounted to the host-machine by the user
BUILDDIR=/srv/projects/project # Staging area when building releases
DESTDIR=/srv/release # A persistent Docker volume to store the release

set -eu

########################
# HELPER FUNCTIONS
########################

# dir_is_empty DIR
# Check that a directory is empty
function dir_is_empty {
[ -z "$(ls -A "$1")" ]
}

# dir_last_mod_time DIR
# Get the last-modified time of a directory and its subdirectories (in seconds since epoch)
function dir_last_mod_time {
find -P "$1" -print0 | xargs -0 stat --format='%Y' | sort -n | tail -n 1
}

# do_task MESSAGE COMMAND [ARGS...]
# Wrapper around performing a particular task
# Prints a convenient message and displays the command
# in scroller(1) if its installed
function do_task {
echo -e "\e[1m[\e[32m>\e[0m\e[1m]:\e[0m $1" >&2
shift

# Shell will re-interpret single-word commands
if [ "$#" -eq "1" ] ; then
set -- bash -c "$1"
fi

echo -e "\t\e[2;3m> $*\e[0m" >&2
set -- scroller --size 20 --color '[2m' --on-exit 'error' --window flagpole-ascii "$@"

if "$@" ; then
return 0
else
echo -e "\e[1;91m[ERROR]:\e[0m Failed. (exit status: $?)" >&2
return 1
fi
}

# print_alert MESSAGE...
# Print a nicely-formatted alert message to stderr
function print_alert {
echo -e "\e[1m[\e[93m!\e[0m\e[1m]:\e[0m $1" >&2
shift
while [ "$#" -gt 0 ] ; do
echo -e " \e[1m|\e[0m $1"
shift
done
}

function main {

if [ "$BROWSERCONTAINER" != "yes" ] ; then
print_alert "This script needs to be run inside the 4DGB Browser docker container"
exit 1
fi

if dir_is_empty "$INPUTDIR" ; then
print_alert "No files found in input!" \
"Did you remember to mount a directory at $INPUTDIR" \
"in the container?"
exit 1
fi

mkdir -p "$BUILDDIR"

# We only want to import and re-build if the input directory has had
# changes made to it since last time
if [ "$(dir_last_mod_time $INPUTDIR)" -gt "$(dir_last_mod_time "$DESTDIR")" ] \
|| dir_is_empty "$DESTDIR" ;
then
do_task "Importing project..." rsync -hvrlt --delete "$INPUTDIR/" "$BUILDDIR/"

if [ -x "$BUILDDIR/process-data" ] ; then
( cd "$BUILDDIR" && do_task "Pre-processing project data..." ./process-data )
fi

do_task "Generating database..." ./db_pop "$BUILDDIR"

mkdir -p "$DESTDIR/server/static"

do_task "Copying files..." "
cp server/license.md \"$DESTDIR/\" \\
&& cp -r server/{gtkserver.py,gunicorn.conf.py,static} \"$DESTDIR/server/\" \\
&& cp -r \"$BUILDDIR\" \"$DESTDIR/server/static/\"
"

touch "$BUILDDIR"
fi

echo -e '
\e[1m#
\e[1m# \e[32mReady!\e[0m
\e[1m# Open your web browser and visit:
\e[1m# http://localhost:8000/compare.html?gtkproject=project
\e[1m#
'

# Start gunicorn
cd $DESTDIR/server
do_task "Starting server..." gunicorn --workers 4 gtkserver:app
}

main
11 changes: 9 additions & 2 deletions client-js/GTK/Component.js
Original file line number Diff line number Diff line change
@@ -97,14 +97,21 @@ class Component extends EventEmitter {
* @param {BackgroundSetting} value
* @param {EventOptions} options
*/
onBackgroundColorChanged(value, options) {}
onBackgroundColorChanged(value, options) {}

/**
* Called in response to a cameraPositionChanged event in the Controller
* @param {CameraSetting} value
* @param {EventOptions} options
*/
onCameraPositionChanged(value, options) {}
onCameraPositionChanged(value, options) {}

/**
* Called in response to a centerPositionChanged event in the Controller
* @param {CenterPosition} value
* @param {EventOptions} options
*/
onCenterPositionChanged(value, options) {}

}

15 changes: 15 additions & 0 deletions client-js/GTK/ControlPanel.js
Original file line number Diff line number Diff line change
@@ -282,6 +282,21 @@ class ControlPanel extends Component {
});
cell.appendChild(this.bgColorInput);

// reset camera button
row = cur_panel.insertRow(cur_row);
cur_row += 1;
cell = row.insertCell(0);
cell.colSpan = 3;
this.resetCamera = document.createElement('button');
this.resetCamera.innerText = "Reset Camera";
this.resetCamera.onclick = () => {
this.controller.updateCenterPosition(null);
this.controller.updateCameraPosition(
project.getApplicationData('gtk')['geometrycanvas']['scene']['camera']['position']
);
};
cell.appendChild(this.resetCamera);

// create the links section
// title
var row = cur_panel.insertRow(cur_row);
67 changes: 50 additions & 17 deletions client-js/GTK/Controller.js
Original file line number Diff line number Diff line change
@@ -63,7 +63,8 @@ class Controller extends EventEmitter {
*
* @typedef {String} BackgroundSetting Background color for the geometry view. A string in `#FFFFFF' format
*
* @typedef {Number[]} CameraSetting Array of [x,y,z] values for camera position
* @typedef {Number[]} Vector3Setting Array of [x,y,z] values representing a THREE.js vector3
* (like for the camera position of center-of-rotation position)
**/

/**
@@ -116,8 +117,11 @@ class Controller extends EventEmitter {
showUnmappedSegments: false,
/** @type {BackgroundSetting} */
backgroundColor: '#FFFFFF',
/** @type {CameraSetting} */
cameraPos: this.project.getApplicationData('gtk')['geometrycanvas']['scene']['camera']['position']
/** @type {Vector3Setting} */
cameraPos: this.project.getApplicationData('gtk')['geometrycanvas']['scene']['camera']['position'],
/** @type {vector3Setting?} */
centerPos: null // A null value indicates that the center-of-rotation should be
// a structure's centroid
}
}

@@ -191,7 +195,7 @@ class Controller extends EventEmitter {

/**
* Trigger an update to the `cameraPosition` setting on this Controller and Components connected to it.
* @param {CameraSetting} value The new setting value
* @param {Vector3Setting} value The new setting value
* @param {Component} source The component initiating the change. (If you're calling this method
* from a Component, then make this `this`).
* @param {*} decoration Any additional data to pass along to other Components
@@ -201,6 +205,19 @@ class Controller extends EventEmitter {
this._triggerEvent('cameraPositionChanged', 'onCameraPositionChanged', false, value, {decoration, source});
}

/**
* Trigger an update to the 'centerPosition' setting on this Controller and the Components connected to it.
* @param {Vector3Setting} value The new setting value
* @param {Component} source The component initiating the change. (If you're calling this method
* from a Component, then make this `this`).
* @param {*} decoration Any additional data to pass along to other Components
*/
updateCenterPosition = (value, source, decoration) => {
this.settings.centerPos = value;
this._triggerEvent('centerPositionChanged', 'onCenterPositionChanged', false, value, {decoration, source});
}


/**
* Add a new track based on the current selection and variable. Will trigger the tracksChanged
* event / handlers
@@ -245,12 +262,15 @@ class Controller extends EventEmitter {
const s = this.settings;
return UTIL.objToBase64url({
selection: this.selection === null ? null : this.selection.asPlainObject(),
settings: {
variable: s.variable,
colormap: s.colormap,
cameraPos: s.cameraPos,
centerPos: s.centerPos,
showUnmappedSegments: s.showUnmappedSegments,
backgroundColor: s.backgroundColor
},
tracks: this.tracks,
variable: s.variable,
colormap: s.colormap,
cameraPos: s.cameraPos,
showUnmappedSegments: s.showUnmappedSegments,
backgroundColor: s.backgroundColor
});
}

@@ -263,17 +283,30 @@ class Controller extends EventEmitter {
deserialize(str) {
const from = UTIL.base64urlToObj(str);

if (from.selection)
this.updateSelection( Selection.fromPlainObject(from.selection) );

this.updateVariable(from.variable);
this.updateColormap(from.colormap);
this.updateCameraPosition(from.cameraPos);
this.updateShowUnmappedSegments(from.showUnmappedSegments);
this.updateBackgroundColor(from.backgroundColor);
if (from.selection)
this.selection = Selection.fromPlainObject(from.selection);

this.settings = from.settings;

// Update tracks
this.tracks = from.tracks;

this.triggerAll();
}

/**
* Trigger all event handlers with the current state of the Controller
*/
triggerAll() {
this.updateSelection(this.selection);

this.updateVariable( this.settings.variable );
this.updateColormap( this.settings.colormap );
this.updateCameraPosition( this.settings.cameraPos );
this.updateCenterPosition( this.settings.centerPos );
this.updateShowUnmappedSegments( this.settings.showUnmappedSegments );
this.updateBackgroundColor( this.settings.backgroundColor );

this._triggerEvent('tracksChanged', 'onTracksChanged', false, this.tracks, {});
}

45 changes: 37 additions & 8 deletions client-js/GTK/GeometryCanvas.js
Original file line number Diff line number Diff line change
@@ -119,7 +119,6 @@ class GeometryCanvas extends Component {
this.camera.updateProjectionMatrix();

// controls
// this.controls = new THREE.TrackballControls(this.camera, this.canvas);
this.controls = new THREE.OrbitControls(this.camera, this.canvas);
this.controls.target.set(cam["center"][0], cam["center"][1], cam["center"][2]);
this.controls.update();
@@ -137,6 +136,16 @@ class GeometryCanvas extends Component {
this.raycaster = new THREE.Raycaster();
this.canvas.onclick = (e) => this.onMouseClick(e);

// We have to do a little workaround to stop 'click' events from
// registering at the end of a click-and-drag. Basically, we time how
// long the mouse was down, and if it's too long, the onMouseClick handler
// will ignore it.
// This bit sets a listener to record the time whenever the mouse is pressed down.
this.last_mousedown_time = Date.now();
this.canvas.onmousedown = (e) => {
this.last_mousedown_time = Date.now();
}

// axes
if (false) {
this.axes = new THREE.AxesHelper( 1 );
@@ -197,11 +206,6 @@ class GeometryCanvas extends Component {
this.scalarBarCanvas.setLUT(this.geometry.LUT);
}

setRotationCenter( center ) {
this.controls.target.set(center.x, center.y, center.z);
this.controls.update();
}

/**
* Called in response to 'selectionChanged' events. Sets the visibility of segments
*/
@@ -224,6 +228,22 @@ class GeometryCanvas extends Component {
this.render();
}

onCenterPositionChanged(value, options) {
// Ignore this if it's coming from this very same Geometry Canvas
if (options && options.source === this) return;

if (value == null) {
if (!this.loaded) return;
// A null center-of-rotation means to use the structure's centroid
this.controls.target.copy( this.geometry.centroid );
}
else {
this.controls.target.fromArray(value);
}

this.controls.update();
}

showAxes( state ) {
this.showAxes.visible = state;
}
@@ -279,7 +299,12 @@ class GeometryCanvas extends Component {
// If we haven't loaded yet, it doesn't matter
if (!this.loaded) return;


// Ignore this if its been a long time since the mouse was pressed down
// (implying this is the end of a click-and-drag)
const now = Date.now();
if (now - this.last_mousedown_time > 300) return;
//^^^-- 300 milliseconds is our cutoff

// Coordinates of the mouse click, THREE.js wants this to
// be normalized with the origin in the center of the viewport/canvas
const bounds = this.canvas.getBoundingClientRect();
@@ -294,9 +319,13 @@ class GeometryCanvas extends Component {

if (intersections.length > 0) {
const i = intersections[0];
// Uncomment to set selection to clicked segment
/*
const seg = this.geometry.meshesToSegments[i.object.uuid];
const selection = Selection.fromSegments( Util.valuesToRanges([seg]) );
this.controller.updateSelection(selection, this);
*/
this.controller.updateCenterPosition( i.point.toArray() );
}
}

@@ -306,7 +335,7 @@ class GeometryCanvas extends Component {
this.geometry.addToScene(this.scene);

// set the centroid
this.setRotationCenter( this.geometry.centroid );
this.onCenterPositionChanged( this.controller.settings.centerPos );

// turn off the unmapped ones
this.onShowUnmappedSegmentsChanged( false );
58 changes: 0 additions & 58 deletions doc/deployment.md

This file was deleted.

15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: "3"

volumes:
4dgbprojects:

services:
browser:
image: "4dgbrunner"
volumes:
- "./projects/test.01/:/project:ro"
- "4dgbprojects:/srv/release"
stdin_open: true
tty: true
ports:
- "127.0.0.1:8000:8000"
4 changes: 0 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
@@ -53,7 +53,3 @@ Clicking on the grey zone or the image of the app will take you to the running i
<div align="center">
<img src="doc/img/test.01.png"></img>
</div>

## To run a public instance

See the [Deployment Guide](doc/deployment.md)
8 changes: 8 additions & 0 deletions server/gunicorn.conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#
# Gunicorn Config
# Used in the Docker container
#

bind = "0.0.0.0:8000"
accesslog='-'
loglevel='warning'
8 changes: 8 additions & 0 deletions server/static/gtk/js/compare.js
Original file line number Diff line number Diff line change
@@ -67,6 +67,10 @@ function main( project ) {
if (store.getItem(key)) {
TheController.deserialize( store.getItem(key) );
}
else {
// Init with default settings
TheController.triggerAll();
}

// Save state after any change
TheController.on('anyChanged', (value, options) => {
@@ -76,6 +80,10 @@ function main( project ) {
}
});
}
else {
// Set all components to default settings
TheController.triggerAll();
}

}