summaryrefslogtreecommitdiffstats
path: root/apps/samples/vrml
diff options
context:
space:
mode:
Diffstat (limited to 'apps/samples/vrml')
-rw-r--r--apps/samples/vrml/README.md150
-rw-r--r--apps/samples/vrml/viewer.html48
-rw-r--r--apps/samples/vrml/viewer.js386
-rw-r--r--apps/samples/vrml/vrml-server.scxml396
4 files changed, 980 insertions, 0 deletions
diff --git a/apps/samples/vrml/README.md b/apps/samples/vrml/README.md
new file mode 100644
index 0000000..2a019da
--- /dev/null
+++ b/apps/samples/vrml/README.md
@@ -0,0 +1,150 @@
+# VRML Server
+
+The VRML Server allows clients to retrieve sceneshots of 3D models on the filesystem in a variety of formats.
+
+## General Mode of Operation
+
+The VRML server will monitor its vrml directory recursively for <tt>vrml</tt> and <tt>wrl</tt>
+files. Whenever such a file is found, it is converted into a native, binary representation and
+saved in the tmp directory. Clients can now access sceneshots of this model by specifying the
+desired pose and format.
+
+## Accessing Sceneshots
+
+In the simplest case, a sceneshot is retrieved by simply requested its respective URL on the VRML server:
+
+<tt>http://host/vrml/HARD_MP_VAL_000.png</tt>
+
+All paths start with vrml and then reflect the vrml directory structure as it is being monitored. When a directory
+<tt>foo</tt> was added (either by creation or linking) in the vrml directory, its 3D models will be available at:
+
+<tt>http://host/vrml/foo/FANCY_MODEL_000.png</tt>
+
+When you do not pass any parameters, you will get a sceneshot of the model with its largest, axis aligned surface area
+facing the camera. That is, the model will be rotated by multiples of 90deg to show the side of the bounding box which
+has the largest surface area. The implied assumption is that this side is suited to identify the model and its eventual
+problems the quickest.
+
+If you do not like the standard sceneshot, you can pass a couple of parameters to adapt most aspects of the scene:
+
+<table>
+ <tr><th>Name</th><th>Range</th><th>Description</th></tr>
+ <tr><td><tt>pitch</tt></td><td>[0 .. 2&pi;] rad</td><td>Rotation along the x-axis</td></tr>
+ <tr><td><tt>roll</tt></td><td>[0 .. 2&pi;] rad</td><td>Rotation along the z-axis</td></tr>
+ <tr><td><tt>yaw</tt></td><td>[0 .. 2&pi;] rad</td><td>Rotation along the y-axis</td></tr>
+ <tr><td><tt>zoom</tt></td><td>[0 .. &infin;] bounding-sphere units</td><td>Distance of camera to model center</td></tr>
+ <tr><td><tt>x</tt></td><td>[-&infin; .. &infin;] OpenGL units</td><td>Translation on x-axis</td></tr>
+ <tr><td><tt>y</tt></td><td>[-&infin; .. &infin;] OpenGL units</td><td>Translation on y-axis</td></tr>
+ <tr><td><tt>z</tt></td><td>[-&infin; .. &infin;] OpenGL units</td><td>Translation on z-axis (consider using zoom instead)</td></tr>
+ <tr><td><tt>width</tt></td><td>[0 .. BIG] pixels</td><td>The width of the image (limited by your GPU)</td></tr>
+ <tr><td><tt>height</tt></td><td>[0 .. BIG] pixels</td><td>The height of the image</td></tr>
+ <tr><td><tt>autorotate</tt></td><td>[<tt>on</tt> | <tt>off</tt>]</td><td>Whether or not to autorotate first</td></tr>
+</table>
+
+<tt>http://host/vrml/HARD_MP_VAL_000.png?pitch=0.3&width=2560&height=1600</tt>
+
+There are some caveats:
+<ul>
+ <li>With euler angles such as pitch/roll/yaw, a gimbal lock can occur.
+ <li>Choosing zoom, x, y or z to big will move the model off the clipping distance.
+ <li>width and height have no upper limit, this might be a potential DoS.
+ <li>When observing series of models with autorotating on, not every model is guaranteed to start with the same pose.
+ <li>The OpenGL units really ought to be expressed in multiples of bounding-sphere units.
+</ul>
+
+## REST API
+
+The main purpose of the REST API is to provide clients with a list of available model files.
+
+<table>
+ <tr><th>Path</th><th>Type</th><th>Return Value</th><th>Example</th></tr>
+ <tr>
+ <td><tt>/vrml</tt></td>
+ <td><tt>GET</tt></td>
+ <td>
+ A JSON structure identifying all known models in the hierarchy as found on the filesystem.<br/>
+
+ The entries are organized in a tree, reflecting the original locations relative to the vrml
+ directory. The suffix of <tt>png</tt> is just one example, supported extensions are ultimately
+ defined by the available <a href="http://www.link.de/here">OSG writer plugins</a>, but limited for
+ now to <tt>gif</tt>, <tt>jpg</tt>, <tt>png</tt>, <tt>tif</tt> and <tt>bmp</tt>.
+ </td>
+ <td>
+<pre>
+{
+ "models": {
+ "HARD_MP_VAL_000": {
+ "path": "/HARD_MP_VAL_000.png",
+ "url": "http://host/vrml/HARD_MP_VAL_000.png"
+ },
+ "HARD_MP_VAL_001": {
+ "path": "/HARD_MP_VAL_001.png",
+ "url": "http://host/vrml/HARD_MP_VAL_001.png"
+ },
+ "data": {
+ "SOFT_MP_VAL_000": {
+ "path": "/data/SOFT_MP_VAL_000.png",
+ "url": "http://host/vrml/data/SOFT_MP_VAL_000.png"
+ },
+ ...
+</pre>
+ </td>
+ </tr>
+
+ <tr>
+ <td><tt>/vrml/models</tt>, <tt>/vrml/wrls</tt></td>
+ <td><tt>GET</tt></td>
+ <td>
+ A JSON structure with information about the available binary model files in the tmp directory or the wrl
+ files in the vrml directory respectively.<br/>
+
+ The entries correspond to the tree at <tt>/vrml</tt> but all paths are flattened using the path delimiter
+ ('<tt>:</tt>' per default).
+ </td>
+ <td>
+<pre>
+{
+ "HARD_MP_VAL_000": {
+ "atime": 1363002503,
+ "ctime": 1362521747,
+ "dir": "/tmp",
+ "extension": "osgb",
+ "group": "/",
+ "mtime": "1362521747",
+ "name": "HARD_MP_VAL_000.osgb",
+ "path": "/tmp/HARD_MP_VAL_000.osgb",
+ "relDir": "/",
+ "relPath": "/HARD_MP_VAL_000.osgb",
+ "size": "580201",
+ "strippedName": "HARD_MP_VAL_000"
+ },
+...
+</pre>
+ </td>
+ </tr>
+
+ <tr>
+ <td><tt>/vrml/processed</tt></td>
+ <td><tt>GET</tt></td>
+ <td>
+ A JSON structure with information about the sceneshots that were requested recently and are still on disc.<br/>
+
+ The individual entries within a model key encode the request parameters seperated by underscores, that is:<br/>
+ The euler angles <tt>pitch</tt>, <tt>roll</tt>, <tt>yaw</tt>, <tt>zoom</tt>, translation in <tt>x</tt>, translation
+ in <tt>y</tt>, translation in <tt>z</tt>, <tt>width</tt>, <tt>height</tt>, and whether or not to <tt>autorotate</tt>.
+ </td>
+ <td>
+ <pre>
+{
+ "HARD_MP_VAL_000": {
+ "0.94_0_0_1_0_0_0_640_480_on.png": {
+ "atime": 1363002687,
+ "ctime": 1363002687,
+ "dir": "/tmp",
+...
+ </pre>
+ </td>
+ </tr>
+
+
+</table>
diff --git a/apps/samples/vrml/viewer.html b/apps/samples/vrml/viewer.html
new file mode 100644
index 0000000..4cf971b
--- /dev/null
+++ b/apps/samples/vrml/viewer.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+ <link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/dojo/1.8.3/dijit/themes/tundra/tundra.css">
+ <link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/dojo/1.8.3/dojox/layout/resources/FloatingPane.css">
+ <link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/dojo/1.8.3/dojox/layout/resources/ResizeHandle.css">
+
+ <style type="text/css">
+ .alternateDock {
+ position:absolute;
+ background-color:#ededed;
+ right:0px; top:0px;
+ border-left:1px solid #ccc;
+ height:100%;
+
+ }
+ #alternateDock ul.dojoxDockList { display:block; }
+ .testFixedSize {
+ width:300px;
+ height:200px;
+ padding:7px;
+ }
+ </style>
+
+ <script type="text/javascript">
+ // dojoConfig = {
+ // async : false,
+ // isDebug : true,
+ // debugAtAllCosts : true,
+ // }
+ </script>
+
+ <script src="http://ajax.googleapis.com/ajax/libs/dojo/1.8.3/dojo/dojo.js"></script>
+ <script type="text/javascript" src="viewer.js"></script>
+
+ <script type="text/javascript">
+ require(["dojo/domReady!", "dojo"], function(dom, dojo) {
+ var viewer = new VRMLViewer(dojo.byId("scene1"), { 'dojo': dojo });
+ });
+ </script>
+ </head>
+ <body class="tundra">
+ <div style="width: 600px; height: 400px">
+ <div id="scene1"></div>
+ </div>
+ </body>
+</html>
diff --git a/apps/samples/vrml/viewer.js b/apps/samples/vrml/viewer.js
new file mode 100644
index 0000000..dac3a96
--- /dev/null
+++ b/apps/samples/vrml/viewer.js
@@ -0,0 +1,386 @@
+function VRMLViewer(element, params) {
+
+ // private attributes
+ var self = this;
+ var dojo = require("dojo");
+ var domConst = dojo.require('dojo/dom-construct');
+ var xhr = dojo.require("dojo/_base/xhr");
+
+ require(["dojox/storage"], function (storage) {
+ self.localStorage = dojox.storage.manager.getProvider();
+ self.localStorage.initialize();
+ var savedServerURL = self.localStorage.get("vrmlServer");
+ if (savedServerURL) {
+ self.serverURL = savedServerURL;
+ self.serverBox.set('value', savedServerURL);
+ }
+ });
+
+ /**
+ * Why can't I fetch dijits via CommonJS?
+ */
+ // var Button = require('dijit/form/Button');
+ // var HorizontalSlider = require('dijit/form/HorizontalSlider');
+ // var VerticalSlider = require('dijit/form/VerticalSlider');
+
+
+ // private instanceId
+ if (!VRMLViewer.instances)
+ VRMLViewer.instances = 0;
+ var instanceId = VRMLViewer.instances++;
+
+ // public attributes
+ this.pitch = 0;
+ this.roll = 0;
+ this.yaw = 0;
+ this.zoom = 1;
+ this.x = 0;
+ this.y = 0;
+ this.z = 0;
+ this.width = 640;
+ this.height = 480;
+ this.autorotate = false;
+
+ this.serverURL = "http://88.69.49.213:8080/vrml";
+ this.imageURL;
+
+ view = "normal";
+ if (params.view == "maximized") {
+ view = "maximized";
+ }
+
+ // if (this.view == "maximized") {
+ // this.width = Math.min(element.clientWidth - 150, 800);
+ // this.height = (this.width * 3) / 4;
+ // } else {
+ // this.width = Math.min(element.clientWidth - 50, 800);
+ // this.height = (this.width * 3) / 4;
+ // }
+
+ // privileged public methods
+ this.updateScene = function() {
+ if (self.imageURL) {
+ self.imgElem.src = self.imageURL +
+ '?width=' + self.width +
+ '&height=' + self.height +
+ '&pitch=' + self.pitch +
+ '&roll=' + self.roll +
+ '&yaw=' + self.yaw +
+ '&x=' + self.x +
+ '&y=' + self.y +
+ '&z=' + self.z +
+ '&zoom=' + self.zoom +
+ '&autorotate=' + (self.autorotate ? '1' : '0');
+ }
+ }
+
+ this.refreshServer = function(server) {
+ serverURL = server;
+ self.localStorage.put("vrmlServer", serverURL, null);
+ xhr.get({
+ // The URL to request
+ url: server,
+ handleAs:"json",
+ headers:{"X-Requested-With":null},
+ load: function(result) {
+ (function fillstore(tree, parentId) {
+ for (key in tree) {
+ if ('url' in tree[key]) {
+ self.fileStore.add({id:parentId+key, name:key, url:self.serverURL + tree[key].path, parent:parentId});
+// self.messageBox.innerHTML += '<pre>' + self.serverURL + tree[key].path + '</pre>';
+// self.messageBox.innerHTML += '<pre>' + tree[key].url + '?width=200&height=150</pre>' + '<img src="' + tree[key].url + '?width=200&height=150" />';
+ } else {
+ self.fileStore.add({id:parentId+key, name:key, parent:parentId});
+ fillstore(tree[key], parentId+key);
+ }
+ }
+ } (result.models, "root", ""));
+ }
+ });
+ }
+
+ // establish our dom
+ element.appendChild(domConst.toDom('\
+ <div id="floatPane">\
+ <div style="text-align: right"><div class="server" /></div><button type="button" class="browseButton"></button></div>\
+ <div style="height: 100%; overflow: auto" class="fileList"></div>\
+ </div>\
+ <table>\
+ <tr>\
+ <td valign="top">\
+ <div style="position: relative; padding: 0px">\
+ <img class="model" style="z-index: -1; min-width: ' + self.width + 'px; min-height: ' + self.height + 'px"></img>\
+ <div style="position: absolute; left: 10px; top: 7%; height: 100%">\
+ <div class="pitchSlide"></div>\
+ </div>\
+ <div style="position: absolute; right: 10px; top: 15%; height: 50%">\
+ <div class="zoomSlide"></div>\
+ </div>\
+ <div style="position: absolute; left: 7%; top: 10px; width: 100%">\
+ <div class="rollSlide"></div>\
+ </div>\
+ <div style="position: absolute; left: 7%; bottom: 15px;">\
+ <div class="yawSlide"></div>\
+ </div>\
+ <table cellspacing="0" style="position: absolute; right: 5px; bottom: 25px">\
+ <tr>\
+ <td align="right">x: <input type="text" class="xSpinner"></input></td>\
+ </tr>\
+ <tr>\
+ <td align="right">y: <input type="text" class="ySpinner"></input></td>\
+ </tr>\
+ <tr>\
+ <td align="right"><button type="button" class="resetButton"></button></td>\
+ </tr>\
+ </table>\
+ </div>\
+ </td>\
+ <td valign="top" height="100%">\
+ </td>\
+ </tr>\
+ <tr>\
+ <td colspan="2"><div class="messages"></div></td>\
+ </tr>\
+ </table>\
+ '));
+
+ // fetch special dom nodes for content
+ this.messageBox = dojo.query("div.messages", element)[0];
+ this.imgElem = dojo.query("img.model", element)[0];
+ this.serverBoxElem = dojo.query("div.server", element)[0];
+ this.browseButtonElem = dojo.query("button.browseButton", element)[0];
+ this.fileListElem = dojo.query("div.fileList", element)[0];
+
+ this.resetButtonElem = dojo.query("button.resetButton", element)[0];
+ this.xSpinnerElem = dojo.query("input.xSpinner", element)[0];
+ this.ySpinnerElem = dojo.query("input.ySpinner", element)[0];
+ this.pitchSlideElem = dojo.query("div.pitchSlide", element)[0];
+ this.rollSlideElem = dojo.query("div.rollSlide", element)[0];
+ this.yawSlideElem = dojo.query("div.yawSlide", element)[0];
+ this.zoomSlideElem = dojo.query("div.zoomSlide", element)[0];
+
+ require(["dojox/layout/FloatingPane"], function(FloatingPane) {
+ self.floatPane = new FloatingPane({
+ title: "VRML Viewer",
+ resizable: true, dockable: false, closable: false,
+ style: "position:absolute;top:10;left:10;width:250px;height:300px;z-index: 2",
+ id: "floatPane",
+ }, dojo.byId("floatPane"));
+
+ self.floatPane.startup();
+ });
+
+ // require(['dijit/form/Button','dijit/Dialog'],
+ // function (Button) {
+ // var d = new Dialog({
+ // 'title':'I am nonmodal',
+ // 'class':'nonModal'
+ // });
+ // });
+
+ // setup fileStore for tree list
+ require(["dojo/store/Memory", "dojo/store/Observable", "dijit/tree/ObjectStoreModel"],
+ function(Memory, Observable, ObjectStoreModel){
+ self.fileStore = new Observable(new Memory({
+ data: [ { id: 'root', name:'3D Models'} ],
+ getChildren: function(object){
+ return this.query({parent: object.id});
+ }
+ }));
+ self.fileTreeModel = new ObjectStoreModel({
+ store: self.fileStore,
+ query: { id: "root" }
+ });
+ });
+
+ // setup actual tree dijit
+ require(["dojo/dom", "dojo/store/Memory", "dojo/store/Observable", "dijit/tree/ObjectStoreModel", "dijit/Tree"],
+ function(dom, Memory, Observable, ObjectStoreModel, Tree) {
+ self.fileList = new dijit.Tree({
+ id: "fileList",
+ model: self.fileTreeModel,
+ persist: false,
+ showRoot: false,
+ onClick: function(item){
+// self.messageBox.innerHTML = '<pre>' + item.url + '?width=200&height=150</pre>' + '<img src="' + item.url + '?width=200&height=150" />';
+ if ('url' in item) {
+ self.imageURL = item.url;
+ self.updateScene();
+ }
+ },
+ getIconClass: function(item, opened) {
+ return (!item || !('url' in item)) ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "dijitLeaf"
+ },
+ getIconStyle: function(item, opened){
+ if('url' in item) {
+ return { backgroundImage: "url('" + item.url + "?width=16&height=16')"};
+ }
+ }
+ //return {backgroundImage: "url('" + item.url + "?width=16&height=16')"};
+
+ }).placeAt(self.fileListElem);
+// tree.dndController.singular = true;
+ });
+
+ require(["dijit/form/TextBox"], function(TextBox) {
+ self.serverBox = new TextBox({
+ name: "Server",
+ value: self.serverURL,
+ style: "width: 70%",
+
+ onKeyDown: function(e) {
+ var code = e.keyCode || e.which;
+ if( code === 13 ) {
+ e.preventDefault();
+ self.refreshServer(this.get("value"));
+ return false;
+ }
+ },
+ }, self.serverBoxElem);
+ });
+
+ require(["dijit/form/Button"], function(Button) {
+ self.resetButton = new Button({
+ label: "Reset",
+ onClick: function(){
+ self.xSpinner.set('value',0);
+ self.ySpinner.set('value',0);
+ self.zSpinner.set('value',0);
+ self.pitchSlide.attr('value',0);
+ self.rollSlide.attr('value',0);
+ self.yawSlide.attr('value',0);
+ self.zoomSlide.attr('value',1);
+
+ self.floatPane.startup();
+ self.floatPane.show();
+ }
+ }, self.resetButtonElem);
+ });
+
+ require(["dijit/form/NumberSpinner"], function(NumberSpinner) {
+ self.xSpinner = new NumberSpinner({
+ value: 0,
+ smallDelta: 1,
+ constraints: { places:0 },
+ style: "width:60px",
+ onChange: function(value){
+ self.x = value;
+ self.updateScene();
+ }
+ }, self.xSpinnerElem );
+ });
+
+ require(["dijit/form/NumberSpinner"], function(NumberSpinner) {
+ self.ySpinner = new NumberSpinner({
+ value: 0,
+ smallDelta: 1,
+ constraints: { places:0 },
+ style: "width:60px",
+ onChange: function(value){
+ self.y = value;
+ self.updateScene();
+ }
+ }, self.ySpinnerElem );
+ });
+
+ require(["dijit/form/NumberSpinner"], function(NumberSpinner) {
+ self.zSpinner = new NumberSpinner({
+ value: 0,
+ smallDelta: 1,
+ constraints: { places:0 },
+ style: "width:60px",
+ onChange: function(value){
+ self.z = value;
+ self.updateScene();
+ }
+ }, self.zSpinnerElem );
+ });
+
+ require(["dijit/form/Button"], function(Button) {
+ self.browseButton = new Button({
+ label: "Browse",
+ onClick: function(){
+ self.refreshServer(self.serverBox.get("value"));
+ }
+ }, self.browseButtonElem);
+ });
+
+ // add zoom slider
+ require(["dijit/form/VerticalSlider"], function(VerticalSlider) {
+ self.zoomSlide = new VerticalSlider({
+ minimum: 0.001,
+ showButtons: false,
+ maximum: 1,
+ value: 1,
+ intermediateChanges: false,
+ style: "height: 90%",
+ onChange: function(value){
+ self.zoom = Math.ceil(value * 1000) / 1000;
+ self.updateScene();
+ }
+ }, self.zoomSlideElem);
+ });
+
+ // add pitch slider
+ require(["dijit/form/VerticalSlider", "dijit/form/VerticalRuleLabels", "dijit/form/VerticalRule"], function(VerticalSlider, VerticalRuleLabels, VerticalRule) {
+ // Create the rules
+ // var rulesNode = dojo.create("div", {}, self.pitchSlideElem, "first");
+ // var sliderRules = new VerticalRule({
+ // container: "leftDecoration",
+ // count: 11,
+ // style: "width: 5px;"}, rulesNode);
+
+ // Create the labels
+ // var labelsNode = dojo.create(
+ // "div", {}, self.pitchSlideElem, "first");
+ // var sliderLabels = new VerticalRuleLabels({
+ // labels: ["Pitch", ""],
+ // container: "rightDecoration",
+ // labelStyle: "-webkit-transform: rotate(-90deg); -moz-transform: rotate(-90deg); padding-left: -3px; font-size: 0.75em"
+ // }, labelsNode);
+
+ self.pitchSlide = new VerticalSlider({
+ minimum: 0,
+ showButtons: false,
+ maximum: 2 * 3.14159,
+ value: 0,
+ intermediateChanges: false,
+ style: "height: 90%",
+ onChange: function(value){
+ self.pitch = Math.ceil(value * 100) / 100;
+ self.updateScene();
+ }
+ }, self.pitchSlideElem);
+ });
+
+ // add roll slider
+ require(["dijit/form/HorizontalSlider"], function(HorizontalSlider) {
+ self.rollSlide = new HorizontalSlider({
+ minimum: 0,
+ showButtons: false,
+ maximum: 2 * 3.14159,
+ intermediateChanges: false,
+ style: "width: 90%",
+ onChange: function(value){
+ self.roll = Math.ceil(value * 100) / 100;
+ self.updateScene();
+ }
+ }, self.rollSlideElem);
+ });
+
+ // add yaw slider
+ require(["dijit/form/HorizontalSlider"], function(HorizontalSlider) {
+ self.yawSlide = new HorizontalSlider({
+ minimum: 0,
+ showButtons: false,
+ maximum: 2 * 3.14159,
+ intermediateChanges: false,
+ style: "width: 90%",
+ onChange: function(value){
+ self.yaw = Math.ceil(value * 100) / 100;
+ self.updateScene();
+ }
+ }, self.yawSlideElem);
+ });
+
+}
diff --git a/apps/samples/vrml/vrml-server.scxml b/apps/samples/vrml/vrml-server.scxml
new file mode 100644
index 0000000..0d6517c
--- /dev/null
+++ b/apps/samples/vrml/vrml-server.scxml
@@ -0,0 +1,396 @@
+<scxml datamodel="ecmascript" name="vrml">
+ <script src="http://uscxml.tk.informatik.tu-darmstadt.de/scripts/dump.js" />
+ <script src="http://uscxml.tk.informatik.tu-darmstadt.de/scripts/string.endsWith.js" />
+ <script src="http://uscxml.tk.informatik.tu-darmstadt.de/scripts/array.last.js" />
+ <script>
+ var wrls = {}; // information of the wrl, vrml files
+ var models = {}; // information of the osgb files
+ var processed = {}; // information about processed files
+
+ var pathDelim = ':'; // we need to flatten directories - this will seperate them in filenames
+
+ /**
+ * This pattern matches the query string we use as part of generated image filenames
+ */
+ var numPattern = '(-?[0-9]+\.?[0-9]*)';
+ var formatPattern = new RegExp(
+ '-(' + numPattern + // pitch
+ '_' + numPattern + // roll
+ '_' + numPattern + // yaw
+ '_' + numPattern + // zoom
+ '_' + numPattern + // x
+ '_' + numPattern + // y
+ '_' + numPattern + // z
+ '_' + numPattern + // width
+ '_' + numPattern + // height
+ '_(off|on)' + // autorotate
+ ')\\.\\w+$'); // end
+
+ /**
+ * Transform a file we found into a processed or model struct
+ */
+ function fileToStruct(file) {
+ var struct = {};
+ var formatMatch = formatPattern.exec(file.name);
+ // is this a processed file?
+ if (formatMatch &amp;&amp; formatMatch.length == 12) {
+ struct.key = file.relDir.replace(/\//g, pathDelim).substr(1) + file.name.substr(0, file.name.length - formatMatch[0].length);
+ struct.format = formatMatch[1] + '.' + file.extension;
+ struct.pitch = parseFloat(formatMatch[2]);
+ struct.roll = parseFloat(formatMatch[3]);
+ struct.yaw = parseFloat(formatMatch[4]);
+ struct.zoom = parseFloat(formatMatch[5]);
+ struct.x = parseFloat(formatMatch[6]);
+ struct.y = parseFloat(formatMatch[7]);
+ struct.z = parseFloat(formatMatch[8]);
+ struct.width = parseFloat(formatMatch[9]);
+ struct.height = parseFloat(formatMatch[10]);
+ struct.autorotate = parseFloat(formatMatch[11]);
+ } else {
+ struct.key = file.relDir.replace(/\//g, pathDelim).substr(1) + file.strippedName;
+ }
+ return struct;
+ }
+
+ /**
+ * Transform a http request into something to look up in the processed structure
+ */
+ function reqToStruct(req) {
+ var struct = {};
+
+ var query = (('query' in req) ? req.query : {});
+
+ struct.pitch = (('pitch' in query &amp;&amp; isNumber(query.pitch)) ? query.pitch : 0);
+ struct.roll = (('roll' in query &amp;&amp; isNumber(query.roll)) ? query.roll : 0);
+ struct.yaw = (('yaw' in query &amp;&amp; isNumber(query.yaw)) ? query.yaw : 0);
+ struct.zoom = (('zoom' in query &amp;&amp; isNumber(query.zoom)) ? query.zoom : 1);
+ struct.x = (('x' in query &amp;&amp; isNumber(query.x)) ? query.x : 0);
+ struct.y = (('y' in query &amp;&amp; isNumber(query.y)) ? query.y : 0);
+ struct.z = (('z' in query &amp;&amp; isNumber(query.z)) ? query.z : 0);
+ struct.width = (('width' in query &amp;&amp; isNumber(query.width)) ? query.width : 640);
+ struct.height = (('height' in query &amp;&amp; isNumber(query.height)) ? query.height : 480);
+ struct.autorotate = (('autorotate' in query &amp;&amp; (query.autorotate === 'on' || query.autorotate === 'off')) ? query.autorotate : 'on');
+
+ var fileComp = req.pathComponent[req.pathComponent.length - 1];
+ struct.file = fileComp.substr(0, fileComp.indexOf('.'));
+ struct.ext = fileComp.substr(fileComp.indexOf('.') + 1);
+
+ struct.key = _event.data.pathComponent.slice(1, _event.data.pathComponent.length - 1).join(pathDelim);
+ if (struct.key.length > 0)
+ struct.key += pathDelim;
+ struct.key += struct.file;
+
+ struct.format =
+ struct.pitch + '_' +
+ struct.roll + '_' +
+ struct.yaw + '_' +
+ struct.zoom + '_' +
+ struct.x + '_' +
+ struct.y + '_' +
+ struct.z + '_' +
+ struct.width + '_' +
+ struct.height + '_' +
+ struct.autorotate + '.' +
+ struct.ext;
+ return struct;
+ }
+
+ function isSupportedFormat(extension) {
+ if (extension === "gif")
+ return true;
+ if (extension === "jpg")
+ return true;
+ if (extension === "jpeg")
+ return true;
+ if (extension === "png")
+ return true;
+ if (extension === "tif")
+ return true;
+ if (extension === "tiff")
+ return true;
+ if (extension === "bmp")
+ return true;
+ return false;
+ }
+
+ // list all available models in a summary format for the topmost request
+ function overviewList() {
+ var struct = {};
+ struct.models = {};
+ for (key in models) {
+ var model = models[key];
+ var group = models[key].group
+ var name = model.strippedName.split(pathDelim).last();
+ var entry = assign(struct, ['models'].concat(group.substr(1).split('/')).concat(name), {});
+ entry.url =
+ _ioprocessors['http'].location + model.relDir + model.strippedName.split(pathDelim).join('/') + '.png';
+ entry.path = model.relDir + model.strippedName.split(pathDelim).join('/') + '.png';
+ }
+ return struct;
+ }
+
+ // check whether a given string represents a number
+ function isNumber(n) {
+ return !isNaN(parseFloat(n)) &amp;&amp; isFinite(n);
+ }
+
+ // allow to set deep keys in an object
+ function assign(obj, path, value) {
+ if (typeof path === 'string')
+ path = path.split('.');
+ if (!(path instanceof Array))
+ return undefined;
+
+ lastKeyIndex = path.length-1;
+ for (var i = 0; i &lt; lastKeyIndex; ++ i) {
+ key = path[i];
+ if (key.length == 0)
+ continue;
+ if (!(key in obj))
+ obj[key] = {}
+ obj = obj[key];
+ }
+ obj[path[lastKeyIndex]] = value;
+ return obj[path[lastKeyIndex]];
+ }
+ </script>
+ <state id="main">
+ <!-- Stop processing if no vrml-path was given on command line -->
+ <transition target="final" cond="_x['args']['vrml-path'] == undefined || _x['args']['vrml-path'].length == 0">
+ <log expr="'No --vrml-path given'" />
+ </transition>
+
+ <!-- Stop processing if no tmp-path was given on command line -->
+ <transition target="final" cond="_x['args']['tmp-path'] == undefined || _x['args']['tmp-path'].length == 0">
+ <log expr="'No --tmp-path given'" />
+ </transition>
+
+ <!-- Stop processing if any error occurs -->
+ <transition target="final" event="error">
+ <log expr="'An error occured:'" />
+ <script>dump(_event);</script>
+ </transition>
+
+ <!-- Start the directory monitor for generated files -->
+ <invoke type="dirmon" id="dirmon.processed">
+ <param name="dir" expr="_x['args']['tmp-path']" />
+ <param name="recurse" expr="false" />
+ <param name="reportExisting" expr="true" />
+ <!-- Called for every file we found -->
+ <finalize>
+ <script>
+ _event.fileStruct = fileToStruct(_event.data.file);
+
+ if (_event.data.file.extension === "osgb") {
+ // this is a binary 3D file converted from the wrls
+
+ if (_event.name === "file.deleted") {
+ delete models[_event.fileStruct.key];
+ print("Removed a vanished osgb file at " + _event.fileStruct.key + "\n");
+ } else {
+ models[_event.fileStruct.key] = _event.data.file;
+ models[_event.fileStruct.key].group = '/' + _event.data.file.name.split(pathDelim).slice(0,-1).join('/');
+ print("Inserted a new osgb file at " + _event.fileStruct.key + "\n");
+ }
+
+ } else if ('format' in _event.fileStruct) {
+ // this is a processed file generated for some request
+
+ if (_event.name === "file.deleted") {
+ delete processed[_event.fileStruct.key][_event.fileStruct.format];
+ print("Removed a vanished processed file at " + _event.fileStruct.key + "\n");
+ } else {
+ if (!(_event.fileStruct.key in processed)) {
+ processed[_event.fileStruct.key] = {}
+ }
+ processed[_event.fileStruct.key][_event.fileStruct.format] = _event.data.file;
+ print("Inserted a new processed file at " + _event.fileStruct.key + "\n");
+ }
+ } else {
+ print("Ignoring " + _event.data.file.name + "\n");
+ }
+ </script>
+ </finalize>
+ </invoke>
+
+ <!-- Start the directory monitor for wrl files -->
+ <invoke type="dirmon" id="dirmon.vrml">
+ <param name="dir" expr="_x['args']['vrml-path']" />
+ <param name="recurse" expr="true" />
+ <param name="suffix" expr="'vrml wrl'" />
+ <finalize>
+ <script>
+ _event.fileStruct = fileToStruct(_event.data.file);
+ if (_event.name === "file.existing" || _event.name === "file.added") {
+ wrls[_event.fileStruct.key] = _event.data.file;
+ print("Inserting wrl " + _event.data.file.path + " from " +_event.data.file.relDir + " at " + _event.fileStruct.key + "\n");
+ }
+ if (_event.name === "file.deleted") {
+ delete wrls[_event.fileStruct.key];
+ print("Deleting wrl " + _event.data.file.path + " from " +_event.data.file.relDir + " at " + _event.fileStruct.key + "\n");
+ }
+ </script>
+ <if cond="models &amp;&amp;
+ (!(_event.fileStruct.key in models) ||
+ wrls[_event.fileStruct.key].mtime > models[_event.fileStruct.key].mtime)
+ ">
+ <send target="#_osgvonvert.osgb">
+ <param name="source" expr="_event.data.file.path" />
+ <param name="dest" expr="_x['args']['tmp-path'] + '/' + _event.fileStruct.key + '.osgb'" />
+ </send>
+ </if>
+ </finalize>
+ </invoke>
+
+ <!-- Start the osgconvert invoker to transform 3D files -->
+ <invoke type="osgconvert" id="osgvonvert.osgb">
+ <param name="threads" expr="4" />
+ <!--finalize>
+ <script>
+ // we could put the file into the osgbs or processed here, but we rely on the directory monitors for now
+ print("Received " + _event.name + " regarding " + _event.data.dest + "\n");
+ </script>
+ </finalize-->
+ </invoke>
+
+ <!-- Idle here -->
+ <state id="idle">
+ <transition event="http.get" target="idle" cond="
+ _event.data.pathComponent.length >= 2 &amp;&amp;
+ _event.data.pathComponent[_event.data.pathComponent.length - 1].indexOf('.') !== -1">
+ <!-- request for a specific format
+ http://host/vrml/relative/path/format?query=string -->
+ <script>
+ _event.fileStruct = reqToStruct(_event.data);
+ _event.dest = _x['args']['tmp-path'] + '/' + _event['fileStruct'].key + '-' + _event['fileStruct'].format;
+
+ print("Got a request for [" + _event['fileStruct'].key + '-' + _event['fileStruct'].format + "]\n");
+// dump(_event);
+ </script>
+ <if cond="_event['fileStruct'].key in models &amp;&amp; isSupportedFormat(_event['fileStruct'].ext)">
+ <!-- There is such a file available as osgb -->
+ <if cond="
+ _event['fileStruct'].key in processed &amp;&amp;
+ _event['fileStruct'].format in processed[_event['fileStruct'].key]">
+ <script>
+ //print("Sending " + processed[_event['fileStruct'].key][_event['fileStruct'].format].path + "\n");
+ </script>
+ <response status="200" requestexpr="_event.origin">
+ <header name="Connection" value="close" />
+ <header name="Access-Control-Allow-Origin" value="*" />
+ <content fileexpr="processed[_event['fileStruct'].key][_event['fileStruct'].format].path" />
+ </response>
+ <else>
+ <if cond="_event.name.endsWith('postponed')">
+ <!--
+ A postponed event we couldn't answer
+ -->
+ <response status="404" requestexpr="_event.origin">
+ <header name="Connection" value="close" />
+ </response>
+ <else>
+ <script>
+ print("Processing outfile " + _event['dest'] + " from model " + _event['file'] + "\n");
+ </script>
+ <send target="#_osgvonvert.osgb">
+ <param name="source" expr="models[_event['fileStruct'].key].path" />
+ <param name="dest" expr="_event['dest']" />
+ <param name="pitch" expr="_event.fileStruct.pitch" />
+ <param name="roll" expr="_event.fileStruct.roll" />
+ <param name="yaw" expr="_event.fileStruct.yaw" />
+ <param name="zoom" expr="_event.fileStruct.zoom" />
+ <param name="x" expr="_event.fileStruct.x" />
+ <param name="y" expr="_event.fileStruct.y" />
+ <param name="z" expr="_event.fileStruct.z" />
+ <param name="width" expr="_event.fileStruct.width" />
+ <param name="height" expr="_event.fileStruct.height" />
+ <param name="autorotate" expr="_event.fileStruct.autorotate" />
+ </send>
+ <!--
+ Redeliver the event once the untilexpr is true. The untilexpr has to evaluate
+ into another valid expression that we will check again on stable configurations.
+ -->
+ <postpone
+ untilexpr="
+ '\'' + _event['fileStruct'].key + '\' in processed &amp;&amp;
+ \'' + _event['fileStruct'].format + '\'' + ' in processed[\'' + _event['fileStruct'].key + '\'] ||
+ _event.name === \'convert.failure\' &amp;&amp; _event.data.dest === \'' + _event['dest'] + '\''
+ "/>
+ </else>
+ </if>
+ </else>
+ </if>
+ <else>
+ <!-- There is no such model -->
+ <response status="404" requestexpr="_event.origin">
+ <header name="Connection" value="close" />
+ </response>
+ </else>
+ </if>
+ </transition>
+
+ <!--
+ process request for JSON datastructures
+ -->
+ <transition event="http.get" target="idle" cond="
+ _event.data.pathComponent.length == 2 &amp;&amp;
+ _event.data.pathComponent[1] === 'models'">
+ <script>//dump(_event)</script>
+ <response status="200" requestexpr="_event.origin">
+ <header name="Connection" value="close" />
+ <header name="Content-Type" value="application/json" />
+ <header name="Access-Control-Allow-Origin" value="*" />
+ <content expr="models" />
+ </response>
+ </transition>
+
+ <transition event="http.get" target="idle" cond="
+ _event.data.pathComponent.length == 2 &amp;&amp;
+ _event.data.pathComponent[1] === 'processed'">
+ <script>//dump(_event)</script>
+ <response status="200" requestexpr="_event.origin">
+ <header name="Connection" value="close" />
+ <header name="Content-Type" value="application/json" />
+ <header name="Access-Control-Allow-Origin" value="*" />
+ <content expr="processed" />
+ </response>
+ </transition>
+
+ <transition event="http.get" target="idle" cond="
+ _event.data.pathComponent.length == 2 &amp;&amp;
+ _event.data.pathComponent[1] === 'wrls'">
+ <script>//dump(_event)</script>
+ <response status="200" requestexpr="_event.origin">
+ <header name="Connection" value="close" />
+ <header name="Content-Type" value="application/json" />
+ <header name="Access-Control-Allow-Origin" value="*" />
+ <content expr="wrls" />
+ </response>
+ </transition>
+
+ <!-- request for topmost list of all files -->
+ <transition event="http.get" target="idle" cond="
+ _event.data.pathComponent.length == 1">
+ <script>//dump(_event);</script>
+ <response status="200" requestexpr="_event.origin">
+ <header name="Connection" value="close" />
+ <header name="Content-Type" value="application/json" />
+ <header name="Access-Control-Allow-Origin" value="*" />
+ <content expr="overviewList()" />
+ </response>
+ </transition>
+
+ <!-- XHR CORS preflight response -->
+ <transition event="http.options" target="idle">
+ <script>dump(_event);</script>
+ <response status="200" requestexpr="_event.origin">
+ <header name="Access-Control-Allow-Origin" value="*" />
+ <header name="Access-Control-Allow-Methods" value="GET, OPTIONS" />
+ <header name="Access-Control-Allow-Headers" value="X-Requested-With" />
+ </response>
+ </transition>
+
+ </state>
+ </state>
+ <state id="final" final="true" />
+</scxml> \ No newline at end of file