<commit_before>'use strict';

/**
 * A thin MongoDB layer that wraps
 * [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) to be
 * used with [persistence](https://github.com/n-fuse/persistence).
 *
 * @module persistence-mongodb
 */

var mongo = require('mongodb');
var async = require('async');

/**
 * Connects to a MongoDB database.
 *
 * @param {Object}
 *            [options] a single object for the Server options and the DB
 *            options.
 * @param {String}
 *            options.host defaults to `'localhost'`
 * @param {Int}
 *            options.port defaults to `27017` created: -1}]`
 * @param {String}
 *            options.name name of the database created: -1}]`
 * @param {Function(err,
 *            db)} callback is called when an error occurs or when the database
 *            connection has been established.
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {DB}
 *            callback.db the `DB` instance
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/markdown-docs/database.html#server-options}
 *      and
 *      {@link http://mongodb.github.com/node-mongodb-native/markdown-docs/database.html#db-options}
 *      for more information on the possible options
 */
exports.connect = function(options, callback) {
  // set optional arguments
  if (arguments.length == 1) {
    callback == options;
    options = {};
  }

  var host = options.host || 'localhost';
  var port = options.port || 27017;

  // strict mode must always be off, since otherwise, collection creation etc.
  // would not work
  options.strict = false;
  options.safe = true;

  var mongoServer = new mongo.Server(host, port, options);
  var dbConnector = new mongo.Db(options.name, mongoServer, options);

  // open database but only return an object that only has a few methods
  dbConnector.open(function(err, conn) {
    if (err)
      return callback(err);

    callback(null, new DB(conn));
  });
};

/**
 * @classdesc Thin wrapper around mongodb's Db class.
 * @constructor
 *
 * @param {Db}
 *            dbConnection db connection object
 * @property {String} protocol Database protocol
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/db.html}
 */
exports.DB = function DB(dbConnection) {
  this._db = dbConnection;
}

/**
 * Creates/gets a collection. Additionally, this method ensures the fields given
 * in `indexes` are always indexed.
 *
 * @param {String}
 *            name collection's name
 * @param {Array}
 *            [indexes] array of indexes passed to `ensureIndex()` eg. `['id', {
 *            created: -1}]`
 * @param {Function(err,
 *            collection)} callback is called when an error occurs or when the
 *            collection is returned.
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {Collection}
 *            callback.collection the `Collection` instance
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/markdown-docs/indexes.html}
 */
DB.prototype.getCollection = function(name, indexes, callback) {
  if (arguments.length == 2) {
    callback = indexes;
    indexes = null;
  }

  this._db.collection(name, function(err, coll) {
    if (err)
      return callback(err);

    // when there are no indexes, return the collection immediately
    if (indexes === null || indexes.length == 0)
      return callback(null, new Collection(coll));

    // ensure all indexes before returning the collection
    // therefore indexes are mapped to the ensureIndex method
    indexes.forEach(function(index) {
      coll.ensureIndex(index, function() {
      });
    });

    callback(null, new Collection(coll));
  });
};

// NOTE:
// Removing collections or databases is not supported and can only be done
// manually via the mongo console.

/**
 * @classdesc Thin wrapper around mongodb's collection type.
 * @constructor
 *
 * @param {Collection}
 *            collection collection object
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/collection.html}
 */
function Collection(coll) {
  this._coll = coll;
}

/**
 * Finds all records that match a given query.
 *
 * @param {Object|String}
 *            query resulting objects must match this query. Consult the
 *            [node-mongodb-native
 *            documentation](http://mongodb.github.com/node-mongodb-native/markdown-docs/queries.html#query-object)
 * @param {String|Int|ObjectId}
 *            [query._id] If specified, MongoDB will search by ID
 * @param {String[]}
 *            [fields] specifies the fields of the resulting objects
 * @param {Object}
 *            [options] defines extra logic (sorting options, paging etc.)
 * @param {Function(err,
 *            res)} callback is called when an error occurs or when the
 *            record(s) return
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {Object|Int}
 *            callback.saved the record, if it has been inserted and `1` if the
 *            record has been updated
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/collection.html#find}
 */
Collection.prototype.find = function(query, fields, options, callback) {
  // optional arguments
  if (arguments.length == 2) {
    callback = fields;
    fields = null;
    options = {};
  } else if (arguments.length == 3) {
    callback = options;
    options = fields;
    fields = null;
  }

  // call the query
  if (fields === null)
    this._coll.find(query, options).toArray(callback);
  else
    this._coll.find(query, fields).toArray(callback);
};

/**
 * Finds the first record that matches a given query. Use this method, if you
 * know that there will be only one resulting document. (E.g. when you want to
 * find a result by its `_id`.)
 *
 * @param {Object|String}
 *            query resulting objects must match this query. Consult the
 *            [node-mongodb-native
 *            documentation](http://mongodb.github.com/node-mongodb-native/markdown-docs/queries.html#query-object)
 * @param {String|Int|ObjectId}
 *            [query._id] If specified, MongoDB will search by ID
 * @param {String[]}
 *            [fields] specifies the fields of the resulting objects
 * @param {Object}
 *            [options] defines extra logic (sorting options, paging etc.)
 * @param {Function(err,
 *            res)} callback is called when an error occurs or when the
 *            record(s) return
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {Object|Int}
 *            callback.saved the record, if it has been inserted and `1` if the
 *            record has been updated
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/collection.html#find}
 */
Collection.prototype.findOne = function(query, fields, options, callback) {
  // optional arguments
  if (arguments.length == 2) {
    callback = fields;
    fields = null;
    options = {};
  } else if (arguments.length == 3) {
    callback = options;
    options = fields;
    fields = null;
  }

  // call the query
  if (fields == null)
    this._coll.findOne(query, options, callback);
  else
    this._coll.findOne(query, fields, options, callback);
};

/**
 * Saves a record. If an object with the same `_id` exists already, this will
 * overwrite it completely.
 *
 * @param {Object}
 *            record record that shall be saved. This parameter can be an
 *            arbitrary _non-circular_ JS object that contains only primitive
 *            values or arrays and objects, no functions.
 * @param {String|Int|ObjectId}
 *            [record._id] ID that is used by MongoDB. If no ID is specified,
 *            the a default MongoDB ID will be generated.
 * @param {Object}
 *            [options] defines extra logic (sorting options, paging etc.)
 * @param {Function(err,
 *            saved)} callback is called when an error occurs or when the record
 *            has been saved
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {Object|Int}
 *            callback.saved the record, if it has been inserted and `1` if the
 *            record has been updated
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/collection.html#save}
 */
Collection.prototype.save = function(record, options, callback) {
  // optional arguments
  if (arguments.length == 2) {
    callback = options;
    options = {};
  }

  options.safe = true;

  this._coll.save(record, options, callback);
};

Collection.prototype.delete = function(query, options, callback) {
  // optional arguments
  if (arguments.length == 2) {
    callback = options;
    options = {};
  }

  options.safe = true;

  this._coll.remove(query, options, callback);
};
<commit_msg>Add close()<commit_after>'use strict';

/**
 * A thin MongoDB layer that wraps
 * [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) to be
 * used with [persistence](https://github.com/n-fuse/persistence).
 *
 * @module persistence-mongodb
 */

var mongo = require('mongodb');
var async = require('async');

/**
 * Connects to a MongoDB database.
 *
 * @param {Object}
 *            [options] a single object for the Server options and the DB
 *            options.
 * @param {String}
 *            options.host defaults to `'localhost'`
 * @param {Int}
 *            options.port defaults to `27017` created: -1}]`
 * @param {String}
 *            options.name name of the database created: -1}]`
 * @param {Function(err,
 *            db)} callback is called when an error occurs or when the database
 *            connection has been established.
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {DB}
 *            callback.db the `DB` instance
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/markdown-docs/database.html#server-options}
 *      and
 *      {@link http://mongodb.github.com/node-mongodb-native/markdown-docs/database.html#db-options}
 *      for more information on the possible options
 */
exports.connect = function(options, callback) {
  // set optional arguments
  if (arguments.length == 1) {
    callback == options;
    options = {};
  }

  var host = options.host || 'localhost';
  var port = options.port || 27017;

  // strict mode must always be off, since otherwise, collection creation etc.
  // would not work
  options.strict = false;
  options.safe = true;

  var mongoServer = new mongo.Server(host, port, options);
  var dbConnector = new mongo.Db(options.name, mongoServer, options);

  // open database but only return an object that only has a few methods
  dbConnector.open(function(err, conn) {
    if (err)
      return callback(err);

    callback(null, new DB(conn));
  });
};

/**
 * @classdesc Thin wrapper around mongodb's Db class.
 * @constructor
 *
 * @param {Db}
 *            dbConnection db connection object
 * @property {String} protocol Database protocol
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/db.html}
 */
function DB(dbConnection) {
  this._db = dbConnection;
}

/**
 * Creates/gets a collection. Additionally, this method ensures the fields given
 * in `indexes` are always indexed.
 *
 * @param {String}
 *            name collection's name
 * @param {Array}
 *            [indexes] array of indexes passed to `ensureIndex()` eg. `['id', {
 *            created: -1}]`
 * @param {Function(err,
 *            collection)} callback is called when an error occurs or when the
 *            collection is returned.
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {Collection}
 *            callback.collection the `Collection` instance
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/markdown-docs/indexes.html}
 */
DB.prototype.getCollection = function(name, indexes, callback) {
  if (arguments.length == 2) {
    callback = indexes;
    indexes = null;
  }

  this._db.collection(name, function(err, coll) {
    if (err)
      return callback(err);

    // when there are no indexes, return the collection immediately
    if (indexes === null || indexes.length == 0)
      return callback(null, new Collection(coll));

    // ensure all indexes before returning the collection
    // therefore indexes are mapped to the ensureIndex method
    indexes.forEach(function(index) {
      coll.ensureIndex(index, function() {
      });
    });

    callback(null, new Collection(coll));
  });
};

/**
 * Closes the connection to the database.
 *
 * @param {Boolean}
 *            [force] force to close the connect so that it cannot be reused.
 * @param {Function(err,
 *            results)} callback is called when the db has been closed or an
 *            error occurred
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {Object}
 *            callback.results
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/db.html#close}
 */
DB.prototype.close = function(force, callback) {
  if (arguments.length == 1)
    this._db.close(arguments[0]);
  else
    this._db.close(force, callback);
};

// NOTE:
// Removing whole collections or databases is not supported and can only be done
// manually via the mongo console.

/**
 * @classdesc Thin wrapper around mongodb's collection type.
 * @constructor
 *
 * @param {Collection}
 *            collection collection object
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/collection.html}
 */
function Collection(coll) {
  this._coll = coll;
}

/**
 * Finds all records that match a given query.
 *
 * @param {Object|String}
 *            query resulting objects must match this query. Consult the
 *            [node-mongodb-native
 *            documentation](http://mongodb.github.com/node-mongodb-native/markdown-docs/queries.html#query-object)
 * @param {String|Int|ObjectId}
 *            [query._id] If specified, MongoDB will search by ID
 * @param {String[]}
 *            [fields] specifies the fields of the resulting objects
 * @param {Object}
 *            [options] defines extra logic (sorting options, paging etc.)
 * @param {Function(err,
 *            res)} callback is called when an error occurs or when the
 *            record(s) return
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {Object|Int}
 *            callback.saved the record, if it has been inserted and `1` if the
 *            record has been updated
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/collection.html#find}
 */
Collection.prototype.find = function(query, fields, options, callback) {
  // optional arguments
  if (arguments.length == 2) {
    callback = fields;
    fields = null;
    options = {};
  } else if (arguments.length == 3) {
    callback = options;
    options = fields;
    fields = null;
  }

  // call the query
  if (fields === null)
    this._coll.find(query, options).toArray(callback);
  else
    this._coll.find(query, fields).toArray(callback);
};

/**
 * Finds the first record that matches a given query. Use this method, if you
 * know that there will be only one resulting document. (E.g. when you want to
 * find a result by its `_id`.)
 *
 * @param {Object|String}
 *            query resulting objects must match this query. Consult the
 *            [node-mongodb-native
 *            documentation](http://mongodb.github.com/node-mongodb-native/markdown-docs/queries.html#query-object)
 * @param {String|Int|ObjectId}
 *            [query._id] If specified, MongoDB will search by ID
 * @param {String[]}
 *            [fields] specifies the fields of the resulting objects
 * @param {Object}
 *            [options] defines extra logic (sorting options, paging etc.)
 * @param {Function(err,
 *            res)} callback is called when an error occurs or when the
 *            record(s) return
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {Object|Int}
 *            callback.saved the record, if it has been inserted and `1` if the
 *            record has been updated
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/collection.html#find}
 */
Collection.prototype.findOne = function(query, fields, options, callback) {
  // optional arguments
  if (arguments.length == 2) {
    callback = fields;
    fields = null;
    options = {};
  } else if (arguments.length == 3) {
    callback = options;
    options = fields;
    fields = null;
  }

  // call the query
  if (fields == null)
    this._coll.findOne(query, options, callback);
  else
    this._coll.findOne(query, fields, options, callback);
};

/**
 * Saves a record. If an object with the same `_id` exists already, this will
 * overwrite it completely.
 *
 * @param {Object}
 *            record record that shall be saved. This parameter can be an
 *            arbitrary _non-circular_ JS object that contains only primitive
 *            values or arrays and objects, no functions.
 * @param {String|Int|ObjectId}
 *            [record._id] ID that is used by MongoDB. If no ID is specified,
 *            the a default MongoDB ID will be generated.
 * @param {Object}
 *            [options] defines extra logic (sorting options, paging etc.)
 * @param {Function(err,
 *            saved)} callback is called when an error occurs or when the record
 *            has been saved
 * @param {Error*}
 *            callback.err the error, if an error occurred or `null`
 * @param {Object|Int}
 *            callback.saved the record, if it has been inserted and `1` if the
 *            record has been updated
 *
 * @see {@link http://mongodb.github.com/node-mongodb-native/api-generated/collection.html#save}
 */
Collection.prototype.save = function(record, options, callback) {
  // optional arguments
  if (arguments.length == 2) {
    callback = options;
    options = {};
  }

  options.safe = true;

  this._coll.save(record, options, callback);
};

Collection.prototype.delete = function(query, options, callback) {
  // optional arguments
  if (arguments.length == 2) {
    callback = options;
    options = {};
  }

  options.safe = true;

  this._coll.remove(query, options, callback);
};
<|endoftext|>


<commit_before>/*global define, window, console, jQuery, document, setTimeout */

define("krusovice/music", ["krusovice/thirdparty/jquery-bundle", "krusovice/core", "krusovice/utils"], function($, krusovice, utils) {
"use strict";

krusovice.music = krusovice.music || {};

krusovice.music.Registry = $.extend(true, {}, utils.Registry, {

    /**
     * Load loudness .json data generated by levels.py.
     * @type {Boolean}
     */
    useLevelData : false,

    /**
     * Load music data from JSON file
     *
     * @param {String} url URL to songs.json
     *
     * @param {String} mediaURL Base URL to image and video data
     */
    loadData : function(url, mediaURL, callback, errorCallback) {
        var self = this;

        console.log("Loading song data bank:" + url);

        var dfd = $.getJSON(url, function(data) {
            console.log("Got song data");
            console.log(data);
            self.processData(data, mediaURL);
            callback();
        });

        dfd.error(function() {
            console.error("Bad music db:" + url);
            errorCallback();
        });

    },

    /**
     * Decode song aritist info and media URLs to internal format.
     */
    processData : function(data, mediaURL) {
        var self = this;

        data.forEach(function(obj) {
            self.fixMediaURLs(obj, mediaURL);
            self.register(obj);
        });
    },

    /**
     * Make image URLs loadable
     */
    fixMediaURLs : function(obj, mediaURL) {

        if(obj.mp3 && typeof(obj.mp3) == "string") {
            if(!obj.mp3.match("^http")) {
                // Convert background source url from relative to absolute
                obj.mp3 = krusovice.tools.url.joinRelativePath(mediaURL, obj.mp3);
            }
        }

    },

    /**
     * Load a song into audio element and load related rhytm data too.
     *
     * Song URL must be preprocessed to be platform compatible.
     *
     * @param {Object} audio HTMLAudio element used for music playback, or null if only to load rhytm data
     *
     * @param {Function} callback(songURL, rhytmhURL, rhytmData) called when all done
     *
     * @param {boolean} prelisten Load low quality audio version
     *
     */
    loadSong : function(songURL, rhythmURL, audio, callback, prelisten) {
        throw new Error("Use loadSongDeferred()");
    },

    /**
     * Load a song into audio element and load related rhytm data too.
     *
     * Creates HTMLAudio object which has special attributes *rhytmData* and *levelData*
     * containing server pre-processed information about the music.
     *
     * Rhytm data is described by Echo Next REMIX API.
     *
     * Level data is described by levels.py.
     *
     * @param {Object} audio HTMLAudio element used for music playback.
     *
     * @param {Object} urls { song, rhytm, levels }
     *
     * @param {boolean} prelisten Load low quality audio version
     *
     * @return jQuery.Deferred object with callback of function(audio) and audio object has special rhytmData and songData attributes
     *
     */
    loadSongDeferred : function(audio, urls, prelisten) {

        // Deferred loaders of three different data files
        var dfds = [];

        var audioLoader = $.Deferred(), rhythm, levels;

        console.log("loadSongDeferred()");
        console.log(urls);

        if(!urls || $.isEmptyObject(urls)) {
            throw new Error("Missing song data urls object");
        }

        // Return a Promise to load JSON, but with preprocessor done()
        // which is run before Promise is resolved
        function loadDFDJSON(url, done) {
            var loader = $.Deferred();

            var jqXHR = $.ajax({
                url: url,
                dataType: 'json',

                success: function(data) {
                    console.error("ASDFASF");
                    done(data);
                    loader.resolve();
                },

                error : function() {
                    console.error("Bad loadSongDeferred() url " + url);
                    loader.fail("Could not load:" + url);
                }
            });

            return loader;
        }

        // Load rhytm data if given
        if(urls.rhythm) {
            rhythm = loadDFDJSON(urls.rhythm, function(data) {
                audio.rhytmData = data;
            });
        } else {
            rhythm = null;
        }

        // Load levels data if given
        if(urls.levels) {
            levels = loadDFDJSON(urls.levels, function(data) {
                audio.levelData = data;
            });
        } else {
            levels = null;
        }

        // http://www.w3.org/TR/html5/media-elements.html#mediaevents
        $(audio).one("canplay", function() {
            audioLoader.resolve();
        });

        $(audio).one("error", function() {
            audioLoader.reject("Could not load audio: " + urls.song);
        });

        audio.src = urls.song;

        return $.when(audio, rhythm, levels);
    },

    /**
     * Load a song based on krusovice.Design object.
     *
     * Song can be id (stock) or custom URL.
     *
     * Callback will receive
     *
     * @return {jQuery.Deferred} object
     */
    loadSongFromDesign : function(design, audio, callback, prelisten) {

        // Data URLs we need to load
        var urls = {};

        if(design.songData && design.songData.url) {
            urls.song = design.songData.url;
        } else if(design.songId) {

            urls.song = this.getAudioURL(design.songId);

            if(!urls.song) {
                throw new Error("Library did not have a song with id:" + design.songId);
            }
        }
        if(!urls.song) {
            // Mute design
            return null;
        }

        urls.rhythm = urls.song.replace(".mp3", ".json");

        if(this.useLevelData) {
            urls.levels = urls.song.replace(".mp3", ".levels.json");
        }

        if(prelisten) {
            urls.song = this.convertToPrelistenURL(urls.song);
        } else {

            //  Check FF / Opera
            var needOGG = audio.canPlayType('audio/ogg; codecs="vorbis"') !== "";
            if(needOGG) {
                urls.song = urls.song.replace(".mp3", ".ogg");
            }
        }

        return this.loadSongDeferred(audio, urls, prelisten);

    },

    /**
     * Return URL converted to a file in the same place, but with a compatible format / suffix
     *
     * @param  {String} url [description]
     * @return {String}     [description]
     */
    getBrowserAudioFormat : function(url) {

        var audio = document.createElement("audio");

        var needAAC = audio.canPlayType('audio/mp4; codecs="mp4a.40.5"') !== "";
        var needOGG = audio.canPlayType('audio/ogg; codecs="vorbis"') !== "";

        if(needOGG) {
            url = url.replace(".mp3", ".ogg");
        } else if(needAAC){
            url = url.replace(".mp3", ".m4a");
        } else {
            console.error("Could not detect prelisten audio format support");
        }

        return url;
    },

    /**
     * Get prelisten quality of uploaded song.
     *
     * @param  {[type]} url [description]
     * @return {[type]}     [description]
     */
    convertToPrelistenURL : function(url) {
        url = url.replace(".mp3", ".prelisten.mp3");
        return this.getBrowserAudioFormat(url);
    },

    /**
     * Load audio file from stock library.
     *
     * @param {Boolean} prelisten Get low quality preview version
     *
     * @return null if unknown song id
     */
    getAudioURL : function(songId, prelisten) {

        var song = this.get(songId);

        if(!song) {

            if($.isEmptyObject(this.data)) {
                throw new Error("Attempted to load song from uninitialized song db");
            }

            return null;
        }

        var url = song.mp3;

        if(prelisten) {
            return this.convertToPrelistenURL(url);
        }

        return url;
    }

});

return krusovice.music;

});
<commit_msg>Added option for 'silent' music load<commit_after>/*global define, window, console, jQuery, document, setTimeout */

define("krusovice/music", ["krusovice/thirdparty/jquery-bundle", "krusovice/core", "krusovice/utils"], function($, krusovice, utils) {
"use strict";

krusovice.music = krusovice.music || {};

krusovice.music.Registry = $.extend(true, {}, utils.Registry, {

    /**
     * Load loudness .json data generated by levels.py.
     * @type {Boolean}
     */
    useLevelData : false,

    /**
     * Load music data from JSON file
     *
     * @param {String} url URL to songs.json
     *
     * @param {String} mediaURL Base URL to image and video data
     */
    loadData : function(url, mediaURL, callback, errorCallback) {
        var self = this;

        console.log("Loading song data bank:" + url);

        var dfd = $.getJSON(url, function(data) {
            console.log("Got song data");
            console.log(data);
            self.processData(data, mediaURL);
            callback();
        });

        dfd.error(function() {
            console.error("Bad music db:" + url);
            errorCallback();
        });

    },

    /**
     * Decode song aritist info and media URLs to internal format.
     */
    processData : function(data, mediaURL) {
        var self = this;

        data.forEach(function(obj) {
            self.fixMediaURLs(obj, mediaURL);
            self.register(obj);
        });
    },

    /**
     * Make image URLs loadable
     */
    fixMediaURLs : function(obj, mediaURL) {

        if(obj.mp3 && typeof(obj.mp3) == "string") {
            if(!obj.mp3.match("^http")) {
                // Convert background source url from relative to absolute
                obj.mp3 = krusovice.tools.url.joinRelativePath(mediaURL, obj.mp3);
            }
        }

    },

    /**
     * Load a song into audio element and load related rhytm data too.
     *
     * Song URL must be preprocessed to be platform compatible.
     *
     * @param {Object} audio HTMLAudio element used for music playback, or null if only to load rhytm data
     *
     * @param {Function} callback(songURL, rhytmhURL, rhytmData) called when all done
     *
     * @param {boolean} prelisten Load low quality audio version
     *
     */
    loadSong : function(songURL, rhythmURL, audio, callback, prelisten) {
        throw new Error("Use loadSongDeferred()");
    },

    /**
     * Load a song into audio element and load related rhytm data too.
     *
     * Creates HTMLAudio object which has special attributes *rhytmData* and *levelData*
     * containing server pre-processed information about the music.
     *
     * Rhytm data is described by Echo Next REMIX API.
     *
     * Level data is described by levels.py.
     *
     * @param {Object} audio HTMLAudio element used for music playback.
     *
     * @param {Object} urls { song, rhytm, levels }
     *
     * @param {boolean} prelisten Load low quality audio version
     *
     * @return jQuery.Deferred object with callback of function(audio) and audio object has special rhytmData and songData attributes
     *
     */
    loadSongDeferred : function(audio, urls, prelisten, quiet) {

        // Deferred loaders of three different data files
        var dfds = [];

        var audioLoader = $.Deferred(), rhythm, levels;

        console.log("loadSongDeferred()");
        console.log(urls);

        if(!urls || $.isEmptyObject(urls)) {
            throw new Error("Missing song data urls object");
        }

        // Return a Promise to load JSON, but with preprocessor done()
        // which is run before Promise is resolved
        function loadDFDJSON(url, done) {
            var loader = $.Deferred();

            var jqXHR = $.ajax({
                url: url,
                dataType: 'json',

                success: function(data) {
                    console.error("ASDFASF");
                    done(data);
                    loader.resolve();
                },

                error : function() {
                    console.error("Bad loadSongDeferred() url " + url);
                    loader.fail("Could not load:" + url);
                }
            });

            return loader;
        }

        // Load rhytm data if given
        if(urls.rhythm) {
            rhythm = loadDFDJSON(urls.rhythm, function(data) {
                audio.rhytmData = data;
            });
        } else {
            rhythm = null;
        }

        // Load levels data if given
        if(urls.levels) {
            levels = loadDFDJSON(urls.levels, function(data) {
                audio.levelData = data;
            });
        } else {
            levels = null;
        }

        // http://www.w3.org/TR/html5/media-elements.html#mediaevents
        if(!quiet) {

            $(audio).one("canplay", function() {
                audioLoader.resolve();
            });

            $(audio).one("error", function() {
                audioLoader.reject("Could not load audio: " + urls.song);
            });

            audio.src = urls.song;
        } else {
            // Don't load audio data
            audioLoader.resolve();
        }

        return $.when(audio, rhythm, levels);
    },

    /**
     * Load a song based on krusovice.Design object.
     *
     * Song can be id (stock) or custom URL.
     * Returns a deferred object which on complete
     *
     * @param design {krusovice.Design} Show design which tells what kind of audio to use
     *
     * @param {HTMLAudio} audio Audio object which will contain audio data and rhythm datas
     *
     * @param {Boolean} prelisten convert audio URLs to prelisten versions
     *
     * @parma {Boolean} quiet Don't actually load audio data (used on the server-side rendering)
     *
     * @return {jQuery.Deferred} object
     */
    loadSongFromDesign : function(design, audio, prelisten, quiet) {

        // Data URLs we need to load
        var urls = {};

        if(design.songData && design.songData.url) {
            urls.song = design.songData.url;
        } else if(design.songId) {

            urls.song = this.getAudioURL(design.songId);

            if(!urls.song) {
                throw new Error("Library did not have a song with id:" + design.songId);
            }
        }
        if(!urls.song) {
            // Mute design
            return null;
        }

        urls.rhythm = urls.song.replace(".mp3", ".json");

        if(this.useLevelData) {
            urls.levels = urls.song.replace(".mp3", ".levels.json");
        }

        if(prelisten) {
            urls.song = this.convertToPrelistenURL(urls.song);
        } else if(audio) {

            //  Check FF / Opera
            //  need for audio format remapping
            var needOGG = audio.canPlayType('audio/ogg; codecs="vorbis"') !== "";
            if(needOGG) {
                urls.song = urls.song.replace(".mp3", ".ogg");
            }
        }

        return this.loadSongDeferred(audio, urls, prelisten, quiet);

    },

    /**
     * Return URL converted to a file in the same place, but with a compatible format / suffix
     *
     * @param  {String} url [description]
     * @return {String}     [description]
     */
    getBrowserAudioFormat : function(url) {

        var audio = document.createElement("audio");

        var needAAC = audio.canPlayType('audio/mp4; codecs="mp4a.40.5"') !== "";
        var needOGG = audio.canPlayType('audio/ogg; codecs="vorbis"') !== "";

        if(needOGG) {
            url = url.replace(".mp3", ".ogg");
        } else if(needAAC){
            url = url.replace(".mp3", ".m4a");
        } else {
            console.error("Could not detect prelisten audio format support");
        }

        return url;
    },

    /**
     * Get prelisten quality of uploaded song.
     *
     * @param  {[type]} url [description]
     * @return {[type]}     [description]
     */
    convertToPrelistenURL : function(url) {
        url = url.replace(".mp3", ".prelisten.mp3");
        return this.getBrowserAudioFormat(url);
    },

    /**
     * Load audio file from stock library.
     *
     * @param {Boolean} prelisten Get low quality preview version
     *
     * @return null if unknown song id
     */
    getAudioURL : function(songId, prelisten) {

        var song = this.get(songId);

        if(!song) {

            if($.isEmptyObject(this.data)) {
                throw new Error("Attempted to load song from uninitialized song db");
            }

            return null;
        }

        var url = song.mp3;

        if(prelisten) {
            return this.convertToPrelistenURL(url);
        }

        return url;
    }

});

return krusovice.music;

});
<|endoftext|>


<commit_before>/*
Copyright (C) 2011 by Gregory Burlet, Alastair Porter

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

/**
 * Creates a neume
 * Each neume has: name, root pitch, list of neume elements, each element has difference from root pitch (int)
 *                 modifier (liquescence etc.) - alt, shift, ctrl, ctrl+shift
 * @class Neume
 * @param {Object} options key {string}, type {Toe.Model.Neume.Type}, rootNote.pitch {string},
 *                 rootNote.octave {number}, modifier {Toe.Model.Neume.Modifier}, interact {Boolean}
 */
Toe.Model.Neume = function(options) {
    // initialize bounding box
    this.zone = new Object();

    this.props = {
        key: "punctum",
        type: null,
        rootNote: {
            pitch: "c",
            octave: 3
        },
        modifier: null,
        interact: true
    };

    $.extend(this.props, options);

    this.props.key = this.props.key.toLowerCase();
    this.props.type = Toe.Model.Neume.Type[this.props.key];

    // check neume name is known
    if (this.props.type == undefined) {
        this.props.key = "compound";
        this.props.type = Toe.Model.Neume.Type.compound;
    }

    // check neume modifier is known
    this.props.modifier = Toe.Model.Neume.Modifier[this.props.modifier];
    if (this.props.modifier == undefined) {
        this.props.modifier = null;
    }

    // displacement from the clef - set later by the staff model
    // when mounting the neume on the staff. Should not be manually set.
    this.rootDiff = null;

    // initialize neume component array
    this.components = new Array();
}

Toe.Model.Neume.prototype.constructor = Toe.Model.Neume;

/**
 * Types of neume variants: alt, shift, ctrl and ctrl_shift are the keyboard
 * shortcuts used in the Medieval Finale plugin for selection neume variants.
 * The major variant 'liquescence' occurs when the user holds down alt while 'neumifying'
 *
 * @constant
 * @public
 * @fieldOf Toe.Model.Neume
 */
Toe.Model.Neume.Modifier = {
    alt: "liquescence",
    shift: "variant 2",
    ctrl: "variant 3",
    ctrl_shift: "variant 4"
}

/**
 * Neumes encoded from the Fundamental Neumes Board in Medieval Finale
 *
 * @constant
 * @public
 * @fieldOf Toe.Model.Neume
 */
Toe.Model.Neume.Type = {
    punctum: {
        name: "Punctum",
        melodicMove: [0]
    },
    virga: {
        name: "Virga",
        melodicMove: [0]
    },
    bivirga: {
        name: "Bivirga",
        melodicMove: [0, 0]
    },
    distropha: {
        name: "Distropha",
        melodicMove: [0, 0]
    },
    tristropha: {
        name: "Tristropha",
        melodicMove: [0, 0, 0]
    },
    clivis: {
        name: "Clivis",
        melodicMove: [0, -1]
    },
    climacus: {
        name: "Climacus",
        melodicMove: [0, -1, -1]
    },
    climacus2: {
        name: "Climacus 2",
        melodicMove: [0, -1, -1, -1]
    },
    climacus3: {
        name: "Climacus 3",
        melodicMove: [0, -1, -1, -1, -1]
    },
    climacus4: {
        name: "Climacus 4",
        melodicMove: [0, -1, -1, -1, -1, -1]
    },
    climacusresup: {
        name: "Climacus resupinus",
        melodicMove: [0, -1, -1, 1]
    },
    climacusresup2: {
        name: "Climacus resupinus 2",
        melodicMove: [0, -1, -1, -1, 1]
    },
    climacusresup3: {
        name: "Climacus resupinus 3",
        melodicMove: [0, -1, -1, -1, -1, 1]
    },
    climacusresup4: {
        name: "Climacus resupinus 4",
        melodicMove: [0, -1, -1, -1, -1, -1, 1]
    },
    podatus: {
        name: "Podatus (Pes)",
        melodicMove: [0, 1]
    },
    podatussub: {
        name: "Podatus subpunctis",
        melodicMove: [0, 1, -1, -1]
    },
    podatussub2: {
        name: "Podatus subpunctis 2",
        melodicMove: [0, 1, -1, -1, -1]
    },
    podatussub3: {
        name: "Podatus subpunctis 3",
        melodicMove: [0, 1, -1, -1, -1, -1]
    },
    podatussub4: {
        name: "Podatus subpunctis 4",
        melodicMove: [0, 1, -1, -1, -1, -1, -1]
    },
    podatussubresup: {
        name: "Podatus Subpunctis resupinus",
        melodicMove: [0, 1, -1, -1, 1]
    },
    podatussubresup2: {
        name: "Podatus Subpunctis resupinus 2",
        melodicMove: [0, 1, -1, -1, -1, 1]
    },
    podatussubresup3: {
        name: "Podatus Subpunctis resupinus 3",
        melodicMove: [0, 1, -1, -1, -1, -1, 1]
    },
    scandicus: {
        name: "Scandicus",
        melodicMove: [0, 1, 1]
    },
    scandicus2: {
        name: "Scandicus 2",
        melodicMove: [0, 1, 1, 1]
    },
    scandicus3: {
        name: "Scandicus 3",
        melodicMove: [0, 1, 1, 1, 1]
    },
    scandicusflex: {
        name: "Scandicus flexus",
        melodicMove: [0, 1, 1, -1]
    },
    scandicusflex2: {
        name: "Scandicus flexus 2",
        melodicMove: [0, 1, 1, 1, -1]
    },
    scandicusflex3: {
        name: "Scandicus flexus 3",
        melodicMove: [0, 1, 1, 1, 1, -1]
    },
    scandicussub: {
        name: "Scandicus subpunctis",
        melodicMove: [0, 1, 1, -1, -1]
    },
    scandicussub2: {
        name: "Scandicus subpunctis 2",
        melodicMove: [0, 1, 1, -1, -1, -1]
    },
    porrectus: {
        name: "Porrectus",
        melodicMove: [0, -1, 1]
    },
    porrectusflex: {
        name: "Porrectus flexus",
        melodicMove: [0, -1, 1, -1]
    },
    porrectussub: {
        name: "Porrectus subpunctis",
        melodicMove: [0, -1, 1, -1, -1]
    },
    porrectussub2: {
        name: "Porrectus subpunctis 2",
        melodicMove: [0, -1, 1, -1, -1, -1]
    },
    porrectussubresup: {
        name: "Porrectus Subpunctis resupinus",
        melodicMove: [0, -1, 1, -1, -1, 1]
    },
    porrecussubresup2: {
        name: "Porrectus Subpunctis resupinus 2",
        melodicMove: [0, -1, 1, -1, -1, -1, 1]
    },
    compound1: {
        name: "Compound Neume 1",
        melodicMove: [0, -1, 1, 1, -1]
    },
    compound2: {
        name: "Compound Neume 2",
        melodicMove: [0, -1, 1, 1]
    },
    torculus: {
        name: "Torculus",
        melodicMove: [0, 1, -1]
    },
    torculusresup: {
        name: "Torculus resupinus",
        melodicMove: [0, 1, -1, 1]
    },
    torculusresup2: {
        name: "Torculus resupinus 2",
        melodicMove: [0, 1, -1, 1, -1]
    },
    torculusresup3: {
        name: "Torculus resupinus 3",
        melodicMove: [0, 1, -1, 1, -1, -1]
    },
    torculusresup4: {
        name: "Torculus resupinus 4",
        melodicMove: [0, 1, -1, 1, -1, -1, -1]
    },
    compound: {
        name: "Compound neume",
        melodicMove: []
    }
};

/**
 * Sets the bounding box of the neume
 *
 * @methodOf Toe.Model.Neume
 * @param {Array} bb [ulx, uly, lrx, lry]
 */
Toe.Model.Neume.prototype.setBoundingBox = function(bb) {
    if(!Toe.validBoundingBox(bb)) {
        throw new Error("Neume: invalid bounding box");
    }

    // set position
    this.zone.ulx = bb[0];
    this.zone.uly = bb[1];
    this.zone.lrx = bb[2];
    this.zone.lry = bb[3];
}

/**
 * Sets the root note of the neume.
 * Staff is optional here to facilitate setting an arbitrary note and attaching it to a different staff
 *
 * @param {string} pname root pitch
 * @param {number} oct root octave
 * @param {Object} options staff {Toe.Model.Staff}: Staff the neume is on to get the clef position information
 */
Toe.Model.Neume.prototype.setRootNote = function(pname, oct, options) {
    var opts = {
        staff: null
    };

    $.extend(opts, options);

    this.props.rootNote.pitch = pname;
    this.props.rootNote.octave = oct;

    // calculate pitch difference relative to the clef on the given staff
    if (opts.staff) {
        this.rootDiff = this.calcPitchDifference(opts.staff, pname, oct);
    }
}

/**
 * Calculate the neume component pitch difference with respect to the position of the clef
 *
 * @methodOf Toe.Model.Neume
 * @param {Toe.Model.Staff} staff Staff the neume is on to get the clef position information
 * @param {string} pitch neume component pitch
 * @param {number} octave neume component octave
 */
Toe.Model.Neume.prototype.calcPitchDifference = function(staff, pitch, octave) {
    // get clef pos
    var c_type = staff.clef.shape;

    // ["a", "b", "c", "d", "e", "f", "g"]
    var numChroma = Toe.neumaticChroma.length;

    // make root note search in relation to the clef index
    var iClef = $.inArray(c_type, Toe.neumaticChroma);
    var iRoot = $.inArray(pitch, Toe.neumaticChroma);

    var offset = Math.abs(iRoot - iClef);
    if (iClef > iRoot) {
        offset = numChroma + iRoot - iClef;
    }
    // 4 is no magic number! clef position corresponds to fourth octave
    //var diff = Math.abs(iRoot - iClef) + numChroma*(this.props.rootNote.octave - 4);
    return numChroma*(octave - 4) + offset;
}

/**
 * Calculates the neume component pitch difference with respect to the root note of the neume
 * Requires that the root note has been set.
 * @see Toe.Model.Neume.setRootNote()
 *
 * @methodOf Toe.Model.Neume
 * @param {Toe.Model.Staff} staff Staff the neume is on to get the clef position information
 * @param {string} pitch neume component pitch
 * @param {number} octave neume component octave
 */
Toe.Model.Neume.prototype.calcComponentDifference = function(staff, pitch, octave) {
    var ncClefDiff = this.calcPitchDifference(staff, pitch, octave);

    return ncClefDiff - this.rootDiff;
}

/**
 * Fills the neume with data from an MEI neume element
 *
 * @methodOf Toe.Model.Neume
 * @param {jQuery wrapped element set} neumeData the MEI neume data
 * @param {jQuery wrapped element set} facs the MEI facs data for the provided neume
 * @param {Toe.Model.Staff} staff Staff the neume is on to get the clef position information
 */
Toe.Model.Neume.prototype.neumeFromMei = function(neumeData, facs, staff) {
    // check the DOM element is in fact a neume
    if (neumeData.nodeName.toLowerCase() != "neume") {
        throw new Error("neumeFromMei: invalid neume data");
    }

    var nName = $(neumeData).attr("name");
    // perform neume -> neume & modifier transformations
    // For example, in the current MEI neumes module, cephalicus and epiphonus are their
    // own neumes. In the Medieval Finale plugin they are clivis and podatus neumes, respectively,
    // with an alt (liquescence) modifier.
    if (nName == "epiphonus") {
        nName = "podatus";
        this.props.modifier = Toe.Model.Neume.Modifier["alt"];
    }
    else if (nName == "cephalicus") {
        nName = "clivis";
        this.props.modifier = Toe.Model.Neume.Modifier["alt"];
    }

    this.props.key = nName;
    this.props.type = Toe.Model.Neume.Type[this.props.key];
    // if neume is unknown
    if (this.props.type == undefined) {
        this.props.type = "unknown";
    }

    // set bounding box
    var ulx = parseInt($(facs).attr("ulx"));
    var uly = parseInt($(facs).attr("uly"));
    var lrx = parseInt($(facs).attr("lrx"));
    var lry = parseInt($(facs).attr("lry"));

    this.setBoundingBox([ulx, uly, lrx, lry]);

    // cache a local copy of this so the handle isn't overidden within the jquery function
    var theNeume = this;
    $(neumeData).find("note").each(function(it, el) {
        var pname = $(el).attr("pname");
        var oct = parseInt($(el).attr("oct"));

        // set root note
        var diff = 0;
        if (it == 0) {
            theNeume.setRootNote(pname, oct, {staff: staff});
        }
        else {
            diff = theNeume.calcComponentDifference(staff, pname, oct);
        }

        var ncType = "punctum";
        if ($(this).parent().attr("inclinatum") == "true") {
            ncType = "inclinatum";
        }

        // add note ornaments
        var ornaments = new Array();

        // check for dot
        var dotForm = $("> dot", this).attr("form");
        if (dotForm) {
            ornaments.push(new Toe.Model.Ornament("dot", {form: dotForm}));
        }

        theNeume.addComponent(ncType, diff, {ornaments: ornaments});
    });

    // for chaining
    return this;
}

/**
 * Adds a neume component to the neume
 * nInd is index 0 based
 *
 * @methodOf Toe.Model.Neume
 * @param {string} Neume component type
 * @diff {number} pitch difference from the root
 * @options {Object} options neumeInd {number} index of where to insert the component in the neume
 */
Toe.Model.Neume.prototype.addComponent = function(type, diff, options) {
    opts = {
        ncInd: this.components.length,
        ornaments: []
    };

    $.extend(opts, options);

    // TODO: check that diff corresponds with the neume melodic move

    var nc = new Toe.Model.NeumeComponent(diff, {type: type, ornaments: opts.ornaments});

    this.components.splice(opts.ncInd, 0, nc);
}

/**
 * Gets the pitch differences for each component
 *
 * @methodOf Toe.Model.Neume
 * @returns {Array} array of pitch differences for each neume component
 */
Toe.Model.Neume.prototype.getDifferences = function() {
    var diffs = new Array();
    for(var i = 0; i < this.components.length; i++) {
        diffs.push(this.components[i].diff);
    }
    return diffs;
}

/**
 * Converts neume component difference integers to basic up/downs in the form of
 * 1 and -1, for up and down, respectively.
 *
 * @methodOf Toe.Model.Neume
 * @returns {Array} melodic movement in ups and downs
 */
Toe.Model.Neume.prototype.diffToMelodicMove = function() {
    var diffs = this.getDifferences();

    // convert to ups and downs
    var prevDiff = diffs[0];
    diffs = $.map(diffs, function(x, i) {
        var relation = 0;
        if (x > prevDiff) {
            relation = 1;
        }
        else if (x < prevDiff) {
            relation = -1;
        }

        prevDiff = x;
        return relation;
    });

    return diffs;
}

/**
 * Derives the name of the neume using the pitch differences
 * and sets the name on the model.
 * TODO: use binary search tree instead of linear search
 *
 * @methodOf Toe.Model.Neume
 * @returns {string} neume name
 */
Toe.Model.Neume.prototype.deriveName = function() {
    // checks
    if (this.components.length == 0) {
        return "unknown";
    }

    var diffs = this.diffToMelodicMove();

    // linear search for now
    var found = false;
    for(var key in Toe.Model.Neume.Type) {
        var melody = Toe.Model.Neume.Type[key].melodicMove;
        console.log("key: " + key + ", melody: " + melody, ", diffs: " + diffs);
        if($.arraysEqual(diffs, melody)) {
            this.props.key = key;
            this.props.type = Toe.Model.Neume.Type[key];

            found = true;
            break;
        }
    }

    // if neume is not in the dictionary
    if (!found) {
        this.props.key = "compound";
        this.props.type = Toe.Model.Neume.Type.compound;
    }

    return this.props.type.name;
}
<commit_msg>Add neume ID to model<commit_after><|endoftext|>


<commit_before>'use strict';

function FullScreenImage(){
  var self = this;

  this.container = e({
    class: 'fs-container',
  });
  this.container.style.display = "none";
  this.container.setAttribute('tabindex', -1);

  this.container.addEventListener('keydown', function(e){
    var nextKeys = [76, 68, 74, 83, 9];
    var prevKeys = [65, 72, 75, 87];
    var exitCodes;
    if(e.keyCode === 39 ){
      self.next();
    }
    else if(e.keyCode === 37){
      self.prev();
    }
    else if(e.keyCode === 13){
      self.next();
    }
    else if(e.keyCode === 27){
      self.hide();
    }
    else if(e.keyCode === 32){
      self.next();
    }
    else if(e.keyCode === 8){
      self.prev();
    }
    else if(nextKeys.indexOf(e.keyCode) !== -1){
      self.next();
    }
    else if(prevKeys.indexOf(e.keyCode) !== -1){
      self.prev();
    }
    else{
      return;
    }
    e.preventDefault();
    e.stopPropagation();
  });

  this.toolbar = e({
    class: 'fs-toolbar',
    parent: this.container,
  });

  this.body = e({
    class: 'fs-body',
    parent: this.container,
  });

  this.navigation = e({
    class: 'fs-navigation',
    parent: this.container,
  });

  this.prevButton = e({
    class:'fs-button',
    tag:'button',
    parent:this.navigation,
    content:'<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 24 24" width="32">\
      <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>',
    action: function(e){
      self.prev();
    }
  });

  this.nextButton = e({
    class:'fs-button',
    tag:'button',
    parent:this.navigation,
    content:'<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 24 24" width="32">\
      <path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/></svg>',
    action: function(e){
      self.next();
    }
  });

  this.gallery = e({
    tag:'div',
    class:'fs-gallery',
    parent: this.container,
    action: function(e){
      self.setImage(e.target.src);
    }
  });

  this.closeButton = e({
    class:'fs-button fs-close',
    tag:'button',
    parent:this.toolbar,
    content:'<svg xmlns="http://www.w3.org/2000/svg" fill="white" height="32" viewBox="0 0 24 24" width="32">\
      <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>\
      </svg>',
    action: function(e){
      self.hide();
    }
  });

  // Centering active thumbnail on resize.
  var resizeRunning = false;
  window.addEventListener('resize', function(){
    if(resizeRunning){
      return;
    }

    resizeRunning = true;

    window.requestAnimationFrame(function(){
      self.fixGalleryPosition();
      resizeRunning = false;
    });
  });

  // Properties
  this.images = {};
  this.imageKeys = [];
  this.currentKeyIndex;
  this.current; // Current image object

  document.body.appendChild(this.container);

}

FullScreenImage.prototype.show = function(target){
  if(target instanceof HTMLElement){
    document.body.style.overflow = 'hidden';
    this.container.style.zIndex = 100;
    this.container.style.opacity = 0;
    this.container.style.display = 'block';
    this.setImage(target.src);
    this.container.focus();
    this.container.style.opacity = 1;
  }
}

FullScreenImage.prototype.hide = function(){
  document.body.style.overflow = null;
  this.container.style.opacity = 0;
  this.container.style.zIndex = -100;
}

FullScreenImage.prototype.getImageObj = function(arg){
  if(typeof arg === "string"){
    return this.images[arg];
  }
  else if(typeof arg === "number"){
    if(arg >= this.imageKeys.length){
      return this.getImageObj(arg - this.imageKeys.length);
    }
    else if(arg < 0){
      return this.getImageObj(arg + this.imageKeys.length);
    }
    else{
      return this.getImageObj(this.imageKeys[arg]);
    }
  }
  else if(arg === this.images[arg.src]){
    return arg;
  }
}

FullScreenImage.prototype.prepareImage = function(obj){
  obj = this.getImageObj(obj);
  if (obj == null){
    return;
  }
  if(obj.big == null){
    obj.big = e({
      tag: 'img',
      class: 'fs-image',
    });
    obj.big.src = obj.src;
    obj.big.style.opacity = 0; // 0.01
    //obj.big.style.visibility = 'hidden';
    obj.big.style.zIndex = 0;
    this.body.appendChild(obj.big);
  }
}

FullScreenImage.prototype.fixGalleryPosition = function(){
  var contWidth = window.innerWidth;
  var imgWidth = this.current.elem.clientWidth;
  var elemPos = this.current.elem.offsetLeft;
  var galleryPos = this.gallery.offsetLeft;
  var elemAbsPos = elemPos + galleryPos;
  var desiredPos = ( contWidth / 2 ) - ( imgWidth / 2 );

  var shift = elemAbsPos - desiredPos;

  this.gallery.style.transform = 'translateX('+ (galleryPos - shift) +'px)';
}

FullScreenImage.prototype.setImage = function(img){
  var self = this;
  if(typeof img === "string"){
    var old = this.current;
    this.currentKeyIndex = this.images[img].i;
    var obj = this.images[img];
    this.current = obj;
    this.current.elem.classList.add('fs-active');
    self.prepareImage(obj);
    this.fixGalleryPosition();

    self.prepareImage(obj.i + 1);
    self.prepareImage(obj.i - 1);

    if(old != null && old != this.current){

      old.elem.classList.remove('fs-active');
      old.big.style.opacity = 0;
      //old.big.style.visibility = 'hidden';
      old.big.style.zIndex = 0;
    }
    this.current.big.style.opacity = 1;
    this.current.big.style.visibility = null;
    this.current.big.style.zIndex = 1;
  }
  else if(typeof img === "number"){
    if(img >= this.imageKeys.length){
      this.setImage(img - this.imageKeys.length);
    }
    else if(img < 0){
      this.setImage(img + this.imageKeys.length);
    }
    else{
      this.setImage(this.imageKeys[img]);
    }
  }

}

FullScreenImage.prototype.next = function(){
  this.setImage(this.currentKeyIndex + 1);
}

FullScreenImage.prototype.prev = function(){
  this.setImage(this.currentKeyIndex - 1);
}

FullScreenImage.prototype.setGallery = function(images){
  var self = this;
  this.gallery.innerHTML = '';
  this.images = {};
  this.imageKeys = [];

  images.each(function(i,v){
    if( self.images[v.src] !== undefined){
      return;
    }
    var dest = e({
      tag: 'img',
      parent: self.gallery,
    });
    dest.src = v.src;
    self.imageKeys.push(dest.src);
    var obj = {
      src: dest.src,
      elem: dest,
      desc: v.alt,
      i: self.imageKeys.length - 1,
    };
    self.images[dest.src] = obj;
  });
}

function e(args){
  args = args || {};

  var element = document.createElement( args.tag || 'div' );
  if(typeof args.class === "string"){
    args.class.split(" ").forEach(function(className){
      element.classList.add(className);
    });
  }

  if(typeof args.content === "string" || typeof args.content === "number"){
    element.innerHTML = args.content;
  }

  if(args.parent instanceof HTMLElement){
    args.parent.appendChild(element);
  }

  if(typeof args.action === "function"){
    // TODO: Touch events
    element.addEventListener('click', args.action);
  }

  return element;
}<commit_msg>Disable on-resize gallery fixing when hidden with 'displayed' property<commit_after>'use strict';

function FullScreenImage(){
  var self = this;

  this.container = e({
    class: 'fs-container',
  });
  this.container.style.display = "none";
  this.container.setAttribute('tabindex', -1);

  this.container.addEventListener('keydown', function(e){
    var nextKeys = [76, 68, 74, 83, 9];
    var prevKeys = [65, 72, 75, 87];
    var exitCodes;
    if(e.keyCode === 39 ){
      self.next();
    }
    else if(e.keyCode === 37){
      self.prev();
    }
    else if(e.keyCode === 13){
      self.next();
    }
    else if(e.keyCode === 27){
      self.hide();
    }
    else if(e.keyCode === 32){
      self.next();
    }
    else if(e.keyCode === 8){
      self.prev();
    }
    else if(nextKeys.indexOf(e.keyCode) !== -1){
      self.next();
    }
    else if(prevKeys.indexOf(e.keyCode) !== -1){
      self.prev();
    }
    else{
      return;
    }
    e.preventDefault();
    e.stopPropagation();
  });

  this.toolbar = e({
    class: 'fs-toolbar',
    parent: this.container,
  });

  this.body = e({
    class: 'fs-body',
    parent: this.container,
  });

  this.navigation = e({
    class: 'fs-navigation',
    parent: this.container,
  });

  this.prevButton = e({
    class:'fs-button',
    tag:'button',
    parent:this.navigation,
    content:'<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 24 24" width="32">\
      <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>',
    action: function(e){
      self.prev();
    }
  });

  this.nextButton = e({
    class:'fs-button',
    tag:'button',
    parent:this.navigation,
    content:'<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 24 24" width="32">\
      <path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/></svg>',
    action: function(e){
      self.next();
    }
  });

  this.gallery = e({
    tag:'div',
    class:'fs-gallery',
    parent: this.container,
    action: function(e){
      self.setImage(e.target.src);
    }
  });

  this.closeButton = e({
    class:'fs-button fs-close',
    tag:'button',
    parent:this.toolbar,
    content:'<svg xmlns="http://www.w3.org/2000/svg" fill="white" height="32" viewBox="0 0 24 24" width="32">\
      <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>\
      </svg>',
    action: function(e){
      self.hide();
    }
  });

  // Centering active thumbnail on resize.
  var resizeRunning = false;
  window.addEventListener('resize', function(){
    if(resizeRunning || !self.displayed){
      return;
    }

    resizeRunning = true;

    window.requestAnimationFrame(function(){
      self.fixGalleryPosition();
      resizeRunning = false;
    });
  });

  // Properties
  this.displayed = false;
  this.images = {};
  this.imageKeys = [];
  this.currentKeyIndex;
  this.current; // Current image object

  document.body.appendChild(this.container);

}

FullScreenImage.prototype.show = function(target){
  if(target instanceof HTMLElement){
    this.displayed = true;
    document.body.style.overflow = 'hidden';
    this.container.style.zIndex = 100;
    this.container.style.opacity = 0;
    this.container.style.display = 'block';
    this.setImage(target.src);
    this.container.focus();
    this.container.style.opacity = 1;
  }
}

FullScreenImage.prototype.hide = function(){
  this.displayed = false;
  document.body.style.overflow = null;
  this.container.style.opacity = 0;
  this.container.style.zIndex = -100;
}

FullScreenImage.prototype.getImageObj = function(arg){
  if(typeof arg === "string"){
    return this.images[arg];
  }
  else if(typeof arg === "number"){
    if(arg >= this.imageKeys.length){
      return this.getImageObj(arg - this.imageKeys.length);
    }
    else if(arg < 0){
      return this.getImageObj(arg + this.imageKeys.length);
    }
    else{
      return this.getImageObj(this.imageKeys[arg]);
    }
  }
  else if(arg === this.images[arg.src]){
    return arg;
  }
}

FullScreenImage.prototype.prepareImage = function(obj){
  obj = this.getImageObj(obj);
  if (obj == null){
    return;
  }
  if(obj.big == null){
    obj.big = e({
      tag: 'img',
      class: 'fs-image',
    });
    obj.big.src = obj.src;
    obj.big.style.opacity = 0; // 0.01
    //obj.big.style.visibility = 'hidden';
    obj.big.style.zIndex = 0;
    this.body.appendChild(obj.big);
  }
}

FullScreenImage.prototype.fixGalleryPosition = function(){
  var contWidth = window.innerWidth;
  var imgWidth = this.current.elem.clientWidth;
  var elemPos = this.current.elem.offsetLeft;
  var galleryPos = this.gallery.offsetLeft;
  var elemAbsPos = elemPos + galleryPos;
  var desiredPos = ( contWidth / 2 ) - ( imgWidth / 2 );

  var shift = elemAbsPos - desiredPos;

  this.gallery.style.transform = 'translateX('+ (galleryPos - shift) +'px)';
}

FullScreenImage.prototype.setImage = function(img){
  var self = this;
  if(typeof img === "string"){
    var old = this.current;
    this.currentKeyIndex = this.images[img].i;
    var obj = this.images[img];
    this.current = obj;
    this.current.elem.classList.add('fs-active');
    self.prepareImage(obj);
    this.fixGalleryPosition();

    self.prepareImage(obj.i + 1);
    self.prepareImage(obj.i - 1);

    if(old != null && old != this.current){

      old.elem.classList.remove('fs-active');
      old.big.style.opacity = 0;
      //old.big.style.visibility = 'hidden';
      old.big.style.zIndex = 0;
    }
    this.current.big.style.opacity = 1;
    this.current.big.style.visibility = null;
    this.current.big.style.zIndex = 1;
  }
  else if(typeof img === "number"){
    if(img >= this.imageKeys.length){
      this.setImage(img - this.imageKeys.length);
    }
    else if(img < 0){
      this.setImage(img + this.imageKeys.length);
    }
    else{
      this.setImage(this.imageKeys[img]);
    }
  }

}

FullScreenImage.prototype.next = function(){
  this.setImage(this.currentKeyIndex + 1);
}

FullScreenImage.prototype.prev = function(){
  this.setImage(this.currentKeyIndex - 1);
}

FullScreenImage.prototype.setGallery = function(images){
  var self = this;
  this.gallery.innerHTML = '';
  this.images = {};
  this.imageKeys = [];

  images.each(function(i,v){
    if( self.images[v.src] !== undefined){
      return;
    }
    var dest = e({
      tag: 'img',
      parent: self.gallery,
    });
    dest.src = v.src;
    self.imageKeys.push(dest.src);
    var obj = {
      src: dest.src,
      elem: dest,
      desc: v.alt,
      i: self.imageKeys.length - 1,
    };
    self.images[dest.src] = obj;
  });
}

function e(args){
  args = args || {};

  var element = document.createElement( args.tag || 'div' );
  if(typeof args.class === "string"){
    args.class.split(" ").forEach(function(className){
      element.classList.add(className);
    });
  }

  if(typeof args.content === "string" || typeof args.content === "number"){
    element.innerHTML = args.content;
  }

  if(args.parent instanceof HTMLElement){
    args.parent.appendChild(element);
  }

  if(typeof args.action === "function"){
    // TODO: Touch events
    element.addEventListener('click', args.action);
  }

  return element;
}<|endoftext|>


<commit_before>/**
* Stricture - Generator - Markdown Documentation
*
* @license MIT
*
* @author Steven Velozo <steven@velozo.com>
* @module Stricture
*/
var libFS = require('fs');

var formatMarkdownTime = function(pDate)
{
	// Left zerofill minifunction
	var fPad = function(pPadding)
	{
		return (''+pPadding).length <2?'0'+pPadding:''+pPadding;
	};

	if (typeof pDate === 'undefined')
		var pDate = new Date();

	return pDate.getFullYear()+'-'+fPad(pDate.getMonth()+1)+'-'+fPad(pDate.getDate())+' at '+fPad(pDate.getHours())+':'+fPad(pDate.getMinutes());
};

/***********
 * Markdown generation
 *****/
var GenerateMarkdownDictionary = function(pFable)
{
	var tmpMarkdownFolder = pFable.settings.OutputLocation;
	var tmpTableFile = tmpMarkdownFolder+'Dictionary.md';
	var tmpChangeTrackingFile = tmpMarkdownFolder+'ModelChangeTracking.md';

	console.log('--> Building the markdown data model documentation...');
	console.log('  > Tables of Contents');
	// TODO: Change this to underscore templates.
	libFS.writeFileSync(tmpTableFile, '##### {DocumentationIndex|Home} > {Model/DataModel|Data Model} > {Model/Dictionary/Dictionary|Data Dictionary}'+"\n");
	libFS.appendFileSync(tmpTableFile, "\n");
	libFS.appendFileSync(tmpTableFile, "Headlight Data Dictionary\n");
	libFS.appendFileSync(tmpTableFile, "=========================\n");
	libFS.appendFileSync(tmpTableFile, "\n");
	libFS.appendFileSync(tmpTableFile, "Each entry below describes a single table in the Headlight database.\n");
	libFS.appendFileSync(tmpTableFile, "\n");
	libFS.appendFileSync(tmpTableFile, "Table | Column Count \n");
	libFS.appendFileSync(tmpTableFile, "----- | -----------: \n");
	for(var i = 0; i < pFable.Model.Tables.length; i++)
		libFS.appendFileSync(tmpTableFile, "{Model/Dictionary/Model-"+pFable.Model.Tables[i].TableName+"|"+pFable.Model.Tables[i].TableName+"} | "+pFable.Model.Tables[i].Columns.length+"\n");
	libFS.appendFileSync(tmpTableFile, "\n");
	libFS.appendFileSync(tmpTableFile, "- - -\n");
	libFS.appendFileSync(tmpTableFile, "\n");
	libFS.appendFileSync(tmpTableFile, 'Generated on '+formatMarkdownTime()+''+"\n");

	console.log('  > Raw Tables');
	for(var i = 0; i < pFable.Model.Tables.length; i++)
	{
		var tmpTableModelFile = tmpMarkdownFolder+'Model-'+pFable.Model.Tables[i].TableName+'.md';
		libFS.writeFileSync(tmpTableModelFile, '##### {DocumentationIndex|Home} > {Model/DataModel|Data Model} > {Model/Dictionary/Dictionary|Data Dictionary} > {Model/Dictionary/Model-'+pFable.Model.Tables[i].TableName+'|'+pFable.Model.Tables[i].TableName+' Table}'+"\n");
		libFS.appendFileSync(tmpTableModelFile, "\n");
		libFS.appendFileSync(tmpTableModelFile, pFable.Model.Tables[i].TableName+"\n");
		libFS.appendFileSync(tmpTableModelFile, "===\n");
		libFS.appendFileSync(tmpTableModelFile, "\n");
		libFS.appendFileSync(tmpTableModelFile, "Column Name | Size | Data Type | Join \n");
		libFS.appendFileSync(tmpTableModelFile, "----------- | ---: | --------- | ---- \n");
		for (var j = 0; j < pFable.Model.Tables[i].Columns.length; j++)
		{
			// Dump out each column......
			var tmpSize = (pFable.Model.Tables[i].Columns[j].Size == undefined) ? '' : pFable.Model.Tables[i].Columns[j].Size;
			var tmpJoin = (pFable.Model.Tables[i].Columns[j].Join == undefined) ? '' : pFable.ModelIndices[pFable.Model.Tables[i].Columns[j].Join]+'.'+pFable.Model.Tables[i].Columns[j].Join;
			libFS.appendFileSync(tmpTableModelFile, pFable.Model.Tables[i].Columns[j].Column+" | "+tmpSize+" | "+pFable.Model.Tables[i].Columns[j].DataType+" | "+tmpJoin+" \n");
		}
		libFS.appendFileSync(tmpTableModelFile, "- - -\n");
		libFS.appendFileSync(tmpTableModelFile, "\n");
		libFS.appendFileSync(tmpTableModelFile, 'Generated on '+formatMarkdownTime()+''+"\n");
	}

	console.log('  > Table Change Tracking');
	libFS.writeFileSync(tmpChangeTrackingFile, '##### {DocumentationIndex|Home} > {Model/DataModel|Data Model} > {Model/Dictionary/ModelChangeTracking|Table Change Tracking}'+"\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "Table Change Tracking\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "=====================\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "The following table describes which Headlight tables have implicit create, update and delete change tracking (provided by the API architecture).  This does not include any kind of media archival or record longitudinal backups; just timestamps and user stamps for the last action of each type.\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "Table | Create | Update | Delete \n");
	libFS.appendFileSync(tmpChangeTrackingFile, "----- | :----: | :----: | :----: \n");
	for(var i = 0; i < pFable.Model.Tables.length; i++)
	{
		var tmpDelete = '';
		var tmpUpdate = '';
		var tmpCreate = '';

		for (var j = 0; j < pFable.Model.Tables[i].Columns.length; j++)
		{
			switch (pFable.Model.Tables[i].Columns[j].Column)
			{
				case 'UpdateDate':
					tmpUpdate = 'X';
					break;
				case 'Deleted':
					tmpDelete = 'X';
					break;
				case 'CreateDate':
					tmpCreate = 'X';
					break;
			}
		}
		libFS.appendFileSync(tmpChangeTrackingFile, pFable.Model.Tables[i].TableName+" | "+tmpCreate+" | "+tmpUpdate+" | "+tmpDelete+" \n");
	}
	libFS.appendFileSync(tmpChangeTrackingFile, "- - -\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "\n");
	libFS.appendFileSync(tmpChangeTrackingFile, 'Generated on '+formatMarkdownTime()+''+"\n");
};

module.exports = GenerateMarkdownDictionary;
<commit_msg>Cleaning up documentation generation to work with Orator-wiki<commit_after>/**
* Stricture - Generator - Markdown Documentation
*
* @license MIT
*
* @author Steven Velozo <steven@velozo.com>
* @module Stricture
*/
var libFS = require('fs');

var formatMarkdownTime = function(pDate)
{
	// Left zerofill minifunction
	var fPad = function(pPadding)
	{
		return (''+pPadding).length <2?'0'+pPadding:''+pPadding;
	};

	if (typeof pDate === 'undefined')
		var pDate = new Date();

	return pDate.getFullYear()+'-'+fPad(pDate.getMonth()+1)+'-'+fPad(pDate.getDate())+' at '+fPad(pDate.getHours())+':'+fPad(pDate.getMinutes());
};

/***********
 * Markdown generation
 *****/
var GenerateMarkdownDictionary = function(pFable)
{
	var tmpMarkdownFolder = pFable.settings.OutputLocation;
	var tmpTableFile = tmpMarkdownFolder+'Dictionary.md';
	var tmpChangeTrackingFile = tmpMarkdownFolder+'ModelChangeTracking.md';

	// This generates markdown with wiki add-ins compatible with the orator-wiki project.

	console.log('--> Building the markdown data model documentation...');
	console.log('  > Tables of Contents');
	// TODO: Change this to underscore templates.
	libFS.writeFileSync(tmpTableFile, '##### {DocumentationIndex|Home} > {Model/DataModel|Data Model} > {Model/Dictionary/Dictionary|Data Dictionary}'+"\n");
	libFS.appendFileSync(tmpTableFile, "\n");
	libFS.appendFileSync(tmpTableFile, "Data Dictionary\n");
	libFS.appendFileSync(tmpTableFile, "=========================\n");
	libFS.appendFileSync(tmpTableFile, "\n");
	libFS.appendFileSync(tmpTableFile, "Each entry below describes a single table in the database.\n");
	libFS.appendFileSync(tmpTableFile, "\n");
	libFS.appendFileSync(tmpTableFile, "Table | Column Count \n");
	libFS.appendFileSync(tmpTableFile, "----- | -----------: \n");
	for(var i = 0; i < pFable.Model.Tables.length; i++)
		libFS.appendFileSync(tmpTableFile, "{Model/Dictionary/Model-"+pFable.Model.Tables[i].TableName+"|"+pFable.Model.Tables[i].TableName+"} | "+pFable.Model.Tables[i].Columns.length+"\n");
	libFS.appendFileSync(tmpTableFile, "\n");
	libFS.appendFileSync(tmpTableFile, "- - -\n");
	libFS.appendFileSync(tmpTableFile, "\n");
	libFS.appendFileSync(tmpTableFile, 'Generated on '+formatMarkdownTime()+''+"\n");

	console.log('  > Raw Tables');
	for(var i = 0; i < pFable.Model.Tables.length; i++)
	{
		var tmpTableModelFile = tmpMarkdownFolder+'Model-'+pFable.Model.Tables[i].TableName+'.md';
		libFS.writeFileSync(tmpTableModelFile, '##### {DocumentationIndex|Home} > {Model/DataModel|Data Model} > {Model/Dictionary/Dictionary|Data Dictionary} > {Model/Dictionary/Model-'+pFable.Model.Tables[i].TableName+'|'+pFable.Model.Tables[i].TableName+' Table}'+"\n");
		libFS.appendFileSync(tmpTableModelFile, "\n");
		libFS.appendFileSync(tmpTableModelFile, pFable.Model.Tables[i].TableName+"\n");
		libFS.appendFileSync(tmpTableModelFile, "===\n");
		libFS.appendFileSync(tmpTableModelFile, "\n");
		libFS.appendFileSync(tmpTableModelFile, "Column Name | Size | Data Type | Join \n");
		libFS.appendFileSync(tmpTableModelFile, "----------- | ---: | --------- | ---- \n");
		for (var j = 0; j < pFable.Model.Tables[i].Columns.length; j++)
		{
			// Dump out each column......
			var tmpSize = (pFable.Model.Tables[i].Columns[j].Size == undefined) ? '' : pFable.Model.Tables[i].Columns[j].Size;
			var tmpJoin = (pFable.Model.Tables[i].Columns[j].Join == undefined) ? '' : pFable.ModelIndices[pFable.Model.Tables[i].Columns[j].Join]+'.'+pFable.Model.Tables[i].Columns[j].Join;
			libFS.appendFileSync(tmpTableModelFile, pFable.Model.Tables[i].Columns[j].Column+" | "+tmpSize+" | "+pFable.Model.Tables[i].Columns[j].DataType+" | "+tmpJoin+" \n");
		}
		libFS.appendFileSync(tmpTableModelFile, "- - -\n");
		libFS.appendFileSync(tmpTableModelFile, "\n");
		libFS.appendFileSync(tmpTableModelFile, 'Generated on '+formatMarkdownTime()+''+"\n");
	}

	console.log('  > Table Change Tracking');
	libFS.writeFileSync(tmpChangeTrackingFile, '##### {DocumentationIndex|Home} > {Model/DataModel|Data Model} > {Model/Dictionary/ModelChangeTracking|Table Change Tracking}'+"\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "Table Change Tracking\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "=====================\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "The following table describes which tables have implicit create, update and delete change tracking (provided by the meadow endpoints API architecture).  This does not include any kind of media archival or record longitudinal backups; just timestamps and user stamps for the last action of each type.\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "Table | Create | Update | Delete \n");
	libFS.appendFileSync(tmpChangeTrackingFile, "----- | :----: | :----: | :----: \n");
	for(var i = 0; i < pFable.Model.Tables.length; i++)
	{
		var tmpDelete = '';
		var tmpUpdate = '';
		var tmpCreate = '';

		for (var j = 0; j < pFable.Model.Tables[i].Columns.length; j++)
		{
			switch (pFable.Model.Tables[i].Columns[j].Column)
			{
				case 'UpdateDate':
					tmpUpdate = 'X';
					break;
				case 'Deleted':
					tmpDelete = 'X';
					break;
				case 'CreateDate':
					tmpCreate = 'X';
					break;
			}
		}
		libFS.appendFileSync(tmpChangeTrackingFile, pFable.Model.Tables[i].TableName+" | "+tmpCreate+" | "+tmpUpdate+" | "+tmpDelete+" \n");
	}
	libFS.appendFileSync(tmpChangeTrackingFile, "- - -\n");
	libFS.appendFileSync(tmpChangeTrackingFile, "\n");
	libFS.appendFileSync(tmpChangeTrackingFile, 'Generated on '+formatMarkdownTime()+''+"\n");
};

module.exports = GenerateMarkdownDictionary;
<|endoftext|>