diff --git a/server/database.js b/server/database.js deleted file mode 100644 index f029cff6..00000000 --- a/server/database.js +++ /dev/null @@ -1,631 +0,0 @@ -var backend = require("./databaseBackendSequelize"); -var listeners = require("./listeners"); -var routing = require("./routing"); -var utils = require("./utils"); -var underscore = require("underscore"); -var stream = require("stream"); -var Promise = require("promise"); - -var DEFAULT_TYPES = [ - { name: "Marker", type: "marker", fields: [ { name: "Description", type: "textarea" } ] }, - { name: "Line", type: "line", fields: [ { name: "Description", type: "textarea" } ] } -]; - -function padIdExists(padId) { - return backend.padIdExists(padId); -} - -function getPadData(padId) { - return backend.getPadDataByWriteId(padId).then(function(data) { - if(data != null) - return utils.extend(JSON.parse(JSON.stringify(data)), { writable: true }); - - return backend.getPadData(padId).then(function(data) { - if(data != null) - return utils.extend(JSON.parse(JSON.stringify(data)), { writable: false, writeId: null }); - - throw "This pad does not exist."; - }); - }); -} - -function createPad(data) { - return Promise.resolve().then(function() { - if(!data.id || data.id.length == 0) - throw "Invalid read-only ID"; - if(!data.writeId || data.writeId.length == 0) - throw "Invalid write-only ID"; - if(data.id == data.writeId) - throw "Read-only and write-only ID cannot be the same."; - - return Promise.all([ - padIdExists(data.id).then(function(exists) { - if(exists) - throw "ID '" + data.id + "' is already taken."; - }), - padIdExists(data.writeId).then(function(exists) { - if(exists) - throw "ID '" + data.writeId + "' is already taken."; - }) - ]) - }).then(function() { - return backend.createPad(data); - }).then(function(newData) { - data = newData; - - return Promise.all(DEFAULT_TYPES.map(function(it) { - return backend.createType(data.id, it); - })); - }).then(function() { - return utils.extend(JSON.parse(JSON.stringify(data)), { writable: true }); - }); -} - -function updatePadData(padId, data) { - return Promise.resolve().then(function() { - if(data.id != null && data.id != padId && data.id.length == 0) - throw "Invalid read-only ID"; - - var existsPromises = [ ]; - - if(data.id != null && data.id != padId) { - existsPromises.push(padIdExists(data.id).then(function(exists) { - if(exists) - throw "ID '" + data.id + "' is already taken."; - })); - } - - if(data.writeId != null) { - existsPromises.push(backend.getPadData(padId).then(function(padData) { - if(data.writeId != padData.writeId) { - if(data.writeId.length == 0) - throw "Invalid write-only ID"; - if(data.writeId == (data.id != null ? data.id : padId)) - throw "Read-only and write-only ID cannot be the same."; - - return padIdExists(data.writeId).then(function(exists) { - if(exists) - throw "ID '" + data.writeId + "' is already taken."; - }); - } - })); - } - - return Promise.all(existsPromises); - }).then(function() { - return backend.updatePadData(padId, data); - }).then(function(newData) { - listeners.notifyPadListeners(padId, "padData", function(listener) { - var dataClone = JSON.parse(JSON.stringify(newData)); - if(!listener.writable) - dataClone.writeId = null; - - return dataClone; - }); - - if(data.id != null && data.id != padId) - listeners.changePadId(padId, data.id); - - return data; - }); -} - -function getViews(padId) { - return backend.getViews(padId); -} - -function createView(padId, data) { - return Promise.resolve().then(function() { - if(data.name == null || data.name.trim().length == 0) - throw "No name provided."; - - return backend.createView(padId, data); - }).then(function(data) { - listeners.notifyPadListeners(data.padId, "view", data); - - return data; - }); -} - -function updateView(viewId, data) { - return Promise.resolve().then(function() { - if(data.name == null || data.name.trim().length == 0) - throw "No name provided."; - - return backend.updateView(viewId, data); - }).then(function(data) { - listeners.notifyPadListeners(data.padId, "view", data); - - return data; - }); -} - -function deleteView(viewId) { - return backend.deleteView(viewId).then(function(data) { - listeners.notifyPadListeners(data.padId, "deleteView", { id: data.id }); - - return data; - }); -} - -function getTypes(padId) { - return backend.getTypes(padId); -} - -function createType(padId, data) { - return Promise.resolve().then(function() { - if(data.name == null || data.name.trim().length == 0) - throw "No name provided."; - - return backend.createType(padId, data); - }).then(function(data) { - listeners.notifyPadListeners(data.padId, "type", data); - - return data; - }); -} - -function updateType(typeId, data) { - return Promise.resolve().then(function() { - if(data.name == null || data.name.trim().length == 0) - throw "No name provided."; - - return backend.updateType(typeId, data); - }).then(function(data) { - listeners.notifyPadListeners(data.padId, "type", data); - - return _updateObjectStyles(data.type == "line" ? backend.getPadLinesByType(data.padId, typeId) : backend.getPadMarkersByType(data.padId, typeId), data.type == "line").then(function() { - return data; - }); - }); -} - -function _optionsToObj(options, idx) { - var ret = { }; - if(options) { - for(var i=0; i 0) { - utils.extend(object, update); - - if(object.id) // Objects from getLineTemplate() do not have an ID - ret.push((isLine ? _updateLine : _updateMarker)(object.id, update)); - - if(object.id && isLine && "mode" in update) { - ret.push(_calculateRouting(object).then(function(trackPoints) { - return _setLinePoints(object.padId, object.id, trackPoints); - })); - } - } - - return Promise.all(ret); - }); - }); -} - -function getPadLines(padId) { - return backend.getPadLines(padId); -} - -function getPadLinesWithPoints(padId, bboxWithZoom) { - return utils.filterStreamPromise(backend.getPadLines(padId), function(data) { - return _getLinePoints(data.id, bboxWithZoom).then(function(trackPoints) { - data.trackPoints = trackPoints; - return data; - }); - }); -} - -function getLineTemplate(data) { - return utils.promiseAllObject({ - lineTemplate: backend.getLineTemplate(data), - type: backend.getType(data.typeId) - }).then(function(res) { - var line = res.lineTemplate; - - if(res.type.defaultColour) - line.colour = res.type.defaultColour; - if(res.type.defaultWidth) - line.width = res.type.defaultWidth; - if(res.type.defaultMode) - res.mode = res.type.defaultMode; - - return _updateObjectStyles(line, true).then(function() { - return line; - }); - }); -} - -function createLine(padId, data) { - var defaultValsP = backend.getType(data.typeId).then(function(type) { - if(type.defaultColour && !("colour" in data)) - data.colour = type.defaultColour; - if(type.defaultWidth && !("width" in data)) - data.width = type.defaultWidth; - if(type.defaultMode && !("mode" in data)) - data.mode = type.defaultMode; - }); - - var calculateRoutingP = defaultValsP.then(function() { - return _calculateRouting(data); - }); - - var createLineP = calculateRoutingP.then(function() { - return _createLine(padId, data); - }); - - var setLinePointsP = Promise.all([ calculateRoutingP, createLineP ]).then(function(res) { - return _setLinePoints(padId, res[1].id, res[0]); - }); - - var updateLineStyleP = createLineP.then(function(lineData) { - return _updateObjectStyles(lineData, true); - }); - - return Promise.all([ defaultValsP, calculateRoutingP, createLineP, setLinePointsP, updateLineStyleP ]).then(function(res) { - return res[2]; - }); -} - -function updateLine(lineId, data) { - var originalLineP = backend.getLine(lineId); - - var calculateRoutingP = originalLineP.then(function(originalLine) { - if(data.routePoints == null) - data.routePoints = originalLine.routePoints; - - if(data.mode == null) - data.mode = originalLine.mode || ""; - - if((data.mode == "track" && data.trackPoints) || !underscore.isEqual(data.routePoints, originalLine.routePoints) || data.mode != originalLine.mode) - return _calculateRouting(data); // Also sets data.distance and data.time - }); - - var updateLineP = calculateRoutingP.then(function() { - return _updateLine(lineId, data); - }); - - var updateLineStyleP = updateLineP.then(function(newLine) { - return _updateObjectStyles(newLine, true); // Modifies res.updateLine - }); - - var setLinePointsP = Promise.all([ originalLineP, calculateRoutingP ]).then(function(res) { - if(res[1]) - return _setLinePoints(res[0].padId, lineId, res[1]); - }); - - return Promise.all([ originalLineP, calculateRoutingP, updateLineP, updateLineStyleP, setLinePointsP ]).then(function(res) { - return res[2]; - }); -} - -function _createLine(padId, data) { - var dataCopy = utils.extend({ }, data); - delete dataCopy.trackPoints; // They came if mode is track - - return backend.createLine(padId, dataCopy).then(function(newData) { - listeners.notifyPadListeners(newData.padId, "line", newData); - - return newData; - }); -} - -function _updateLine(lineId, data) { - var dataCopy = utils.extend({ }, data); - delete dataCopy.trackPoints; // They came if mode is track - - return backend.updateLine(lineId, dataCopy).then(function(newData) { - listeners.notifyPadListeners(newData.padId, "line", newData); - - return newData; - }); -} - -function _setLinePoints(padId, lineId, trackPoints) { - return backend.setLinePoints(lineId, trackPoints).then(function() { - listeners.notifyPadListeners(padId, "linePoints", function(listener) { - return { reset: true, id: lineId, trackPoints : (listener && listener.bbox ? routing.prepareForBoundingBox(trackPoints, listener.bbox) : [ ]) }; - }); - }); -} - -function deleteLine(lineId) { - return backend.deleteLine(lineId).then(function(data) { - return backend.setLinePoints(lineId, [ ]).then(function() { - return data; - }); - }).then(function(data) { - listeners.notifyPadListeners(data.padId, "deleteLine", { id: data.id }); - - return data; - }); -} - -function getLinePoints(padId, bboxWithZoom) { - return utils.filterStreamPromise(backend.getPadLines(padId, "id"), function(data) { - return _getLinePoints(data.id, bboxWithZoom).then(function(trackPoints) { - if(trackPoints.length >= 2) - return { id: data.id, trackPoints: trackPoints }; - }); - }); -} - -/*function copyPad(fromPadId, toPadId, callback) { - function _handleStream(stream, next, cb) { - stream.on("data", function(data) { - stream.pause(); - cb(data, function() { - stream.resume(); - }); - }); - - stream.on("error", next); - stream.on("end", next); - } - - async.auto({ - fromPadData : function(next) { - backend.getPadData(fromPadId, next); - }, - toPadData : function(next) { - getPadData(toPadId, next); - }, - padsExist : [ "fromPadData", "toPadData", function(r, next) { - if(!r.fromPadData) - return next(new Error("Pad "+fromPadId+" does not exist.")); - if(!r.toPadData.writable) - return next(new Error("Destination pad is read-only.")); - - toPadId = r.toPadData.id; - - next(); - }], - copyMarkers : [ "padsExist", function(r, next) { - _handleStream(getPadMarkers(fromPadId, null), next, function(marker, cb) { - createMarker(toPadId, marker, cb); - }); - }], - copyLines : [ "padsExist", function(r, next) { - _handleStream(getPadLines(fromPadId), next, function(line, cb) { - async.auto({ - createLine : function(next) { - _createLine(toPadId, line, next); - }, - getLinePoints : function(next) { - backend.getLinePoints(line.id, next); - }, - setLinePoints : [ "createLine", "getLinePoints", function(r, next) { - _setLinePoints(toPadId, r.createLine.id, r.getLinePoints, next); - } ] - }, cb); - }); - }], - copyViews : [ "padsExist", function(r, next) { - _handleStream(getViews(fromPadId), next, function(view, cb) { - createView(toPadId, view, function(err, newView) { - if(err) - return cb(err); - - if(r.fromPadData.defaultView && r.fromPadData.defaultView.id == view.id && r.toPadData.defaultView == null) - updatePadData(toPadId, { defaultView: newView.id }, cb); - else - cb(); - }); - }); - }] - }, callback); -}*/ - -function _calculateRouting(line) { - if(line.mode == "track" && line.trackPoints && line.trackPoints.length >= 2) { - line.distance = utils.calculateDistance(line.trackPoints); - line.time = null; - - routing._calculateZoomLevels(line.trackPoints); - - for(var i=0; i= 2 && line.mode && line.mode != "track") { - return routing.calculateRouting(line.routePoints, line.mode).then(function(routeData) { - line.distance = routeData.distance; - line.time = routeData.time; - for(var i=0; i { + return this._conn.sync({ force: !!force }); + }).then(() => { + this._runMigrations() + }); + } +} + +Database.prototype._init = [ ]; +Database.prototype._afterInit = [ ]; + +require("./migrations")(Database); +require("./helpers")(Database); + +require("./pad")(Database); +require("./marker")(Database); +require("./line")(Database); +require("./view")(Database); +require("./type")(Database); + +module.exports = Database; \ No newline at end of file diff --git a/server/database/helpers.js b/server/database/helpers.js new file mode 100644 index 00000000..3fc11c8c --- /dev/null +++ b/server/database/helpers.js @@ -0,0 +1,271 @@ +var Sequelize = require("sequelize"); +var stream = require("stream"); + +var utils = require("../utils"); + +module.exports = function(Database) { + utils.extend(Database.prototype, { + _TYPES: { + get lat() { + return { + type: Sequelize.FLOAT(9, 6), + allowNull: false, + validate: { + min: -90, + max: 90 + } + } + }, + + get lon() { + return { + type: Sequelize.FLOAT(9, 6), + allowNull: false, + validate: { + min: -180, + max: 180 + } + } + }, + + validateColour: { is: /^[a-fA-F0-9]{3}([a-fA-F0-9]{3})?$/ }, + + dataDefinition: { + "name" : { type: Sequelize.TEXT, allowNull: false }, + "value" : { type: Sequelize.TEXT, allowNull: false } + } + }, + + _updateObjectStyles(objectStream, isLine) { + var t = this; + + if(!(objectStream instanceof stream.Readable)) + objectStream = new utils.ArrayStream([ objectStream ]); + + var types = { }; + return utils.streamEachPromise(objectStream, (object) => { + return Promise.resolve().then(() => { + if(!types[object.typeId]) { + return t.getType(object.padId, object.typeId).then((type) => { + if(type == null) + throw "Type "+object.typeId+" does not exist."; + + return types[object.typeId] = type; + }); + } else + return types[object.typeId]; + }).then((type) => { + var update = { }; + + if(type.colourFixed && object.colour != type.defaultColour) + update.colour = type.defaultColour; + if(!isLine && type.sizeFixed && object.size != type.defaultSize) + update.size = type.defaultSize; + if(!isLine && type.symbolFixed && object.symbol != type.defaultSymbol) + update.symbol = type.defaultSymbol; + if(isLine && type.widthFixed && object.width != type.defaultWidth) + update.width = type.defaultWidth; + if(isLine && type.modeFixed && object.mode != "track" && object.mode != type.defaultMode) + update.mode = type.defaultMode; + + types[object.typeId].fields.forEach((field) => { + if(field.type == "dropdown" && (field.controlColour || (!isLine && field.controlSize) || (!isLine && field.controlSymbol) || (isLine && field.controlWidth))) { + var _find = (value) => { + for(var j=0; j<(field.options || []).length; j++) { + if(field.options[j].key == value) + return field.options[j]; + } + return null; + }; + + var option = _find(object.data[field.name]) || _find(field.default) || field.options[0]; + + if(option != null) { + if(field.controlColour && object.colour != option.colour) + update.colour = option.colour; + if(!isLine && field.controlSize && object.size != option.size) + update.size = option.size; + if(!isLine && field.controlSymbol && object.symbol != option.symbol) + update.symbol = option.symbol; + if(isLine && field.controlWidth && object.width != option.width) + update.width = option.width; + } + } + }); + + var ret = [ ]; + + if(Object.keys(update).length > 0) { + utils.extend(object, update); + + if(object.id) // Objects from getLineTemplate() do not have an ID + ret.push((isLine ? t.updateLine : t.updateMarker).call(t, object.padId, object.id, update, true)); + + if(object.id && isLine && "mode" in update) { + ret.push(t._calculateRouting(object).then(function(trackPoints) { + return t._setLinePoints(object.padId, object.id, trackPoints); + })); + } + } + + return Promise.all(ret); + }); + }); + }, + + _makeNotNullForeignKey(type, field, error) { + return { + as: type, + onDelete: error ? "RESTRICT" : "CASCADE", + foreignKey: { name: field, allowNull: false } + } + }, + + _getPadObjects(type, padId, condition) { + var ret = new utils.ArrayStream(); + + var o = this._conn.model("Pad").build({ id: padId }); + this._conn.model("Pad").build({ id: padId })["get"+type+"s"](condition).then((objs) => { + objs.forEach((it) => { + if(it[type+"Data"] != null) { + it.data = this._dataFromArr(it[type+"Data"]); + it.setDataValue("data", it.data); // For JSON.stringify() + it.setDataValue(type+"Data", undefined); + } + }); + + ret.receiveArray(null, objs); + }, (err) => { + ret.receiveArray(err); + }); + return ret; + }, + + _createPadObject(type, padId, data) { + var obj = this._conn.model(type).build(data); + obj.padId = padId; + return obj.save(); + }, + + _createPadObjectWithData(type, padId, data) { + return this._createPadObject(type, padId, data).then((obj) => { + if(data.data != null) { + obj.data = data.data; + obj.setDataValue("data", obj.data); // For JSON.stringify() + return this._setObjectData(type, obj.id, data.data).then(() => { + return obj; + }); + } else { + obj.data = { }; + obj.setDataValue("data", obj.data); // For JSON.stringify() + return obj; + } + }); + }, + + _updatePadObject(type, padId, objId, data) { + return this._conn.model(type).update(data, { where: { id: objId, padId: padId } }).then((res) => { + if(res[0] == 0) + throw new Error(type + " " + objId + " of pad " + padId + "could not be found."); + + return this._conn.model(type).findById(objId); + }); + }, + + _updatePadObjectWithData(type, padId, objId, data) { + return Promise.all([ + this._updatePadObject(type, padId, objId, data), + data.data != null ? this._setObjectData(type, objId, data.data) : this._getObjectData(type, objId) + ]).then((results) => { + var obj = results[0]; + obj.data = (data.data != null ? data.data : results[1]); + obj.setDataValue("data", obj.data); // For JSON.stringify() + return obj; + }); + }, + + _deletePadObject(type, padId, objId) { + return this._conn.model(type).findOne({ where: { id: objId, padId: padId }}).then((obj) => { + if(obj == null) + throw new Error(type + " " + objId + " of pad " + padId + " could not be found."); + + return obj.destroy().then(() => { + return obj; + }); + }); + }, + + _deletePadObjectWithData(type, padId, objId) { + return this._setObjectData(type, objId, { }).then(() => { + return this._deletePadObject(type, padId, objId); // Return the object + }); + }, + + _dataToArr(data, extend) { + var dataArr = [ ]; + for(var i in data) + dataArr.push(utils.extend({ name: i, value: data[i] }, extend)); + return dataArr; + }, + + _dataFromArr(dataArr) { + var data = { }; + for(var i=0; i { + return this._dataFromArr(dataArr); + }); + }, + + _setObjectData(type, objId, data) { + var model = this._conn.model(type+"Data"); + var idObj = { }; + idObj[type.toLowerCase()+"Id"] = objId; + + return model.destroy({ where: idObj}).then(() => { + return model.bulkCreate(this._dataToArr(data, idObj)); + }); + }, + + _makeBboxCondition(bbox, prefix) { + if(!bbox) + return { }; + + prefix = prefix || ""; + + var cond = (key, value) => { + var ret = { }; + ret[prefix+key] = value; + return ret; + }; + + var conditions = [ ]; + conditions.push(cond("lat", { lte: bbox.top, gte: bbox.bottom })); + + if(bbox.right < bbox.left) // Bbox spans over lon=180 + conditions.push(Sequelize.or(cond("lon", { gte: bbox.left }), cond("lon", { lte: bbox.right }))); + else + conditions.push(cond("lon", { gte: bbox.left, lte: bbox.right })); + + if(bbox.except) { + var exceptConditions = [ ]; + exceptConditions.push(Sequelize.or(cond("lat", { gt: bbox.except.top }), cond("lat", { lt: bbox.except.bottom }))); + + if(bbox.except.right < bbox.except.left) + exceptConditions.push(cond("lon", { lt: bbox.except.left, gt: bbox.except.right })); + else + exceptConditions.push(Sequelize.or(cond("lon", { lt: bbox.except.left }), cond("lon", { gt: bbox.except.right }))); + conditions.push(Sequelize.or.apply(Sequelize, exceptConditions)); + } + + return Sequelize.and.apply(Sequelize, conditions); + } + }); +}; \ No newline at end of file diff --git a/server/database/history.js b/server/database/history.js new file mode 100644 index 00000000..1c13af1d --- /dev/null +++ b/server/database/history.js @@ -0,0 +1,44 @@ +var Sequelize = require("sequelize"); + +var utils = require("../utils"); + +module.exports = function(Database) { + Database.prototype._init.push(function() { + this._conn.define("History", { + time: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW }, + type: { type: Sequelize.ENUM("marker", "line", "view", "type", "pad"), allowNull: false }, + action: { type: Sequelize.ENUM("create", "update", "delete"), allowNull: false }, + objectBefore: { + type: Sequelize.TEXT, + allowNull: true, + get: function() { + return JSON.parse(this.getDataValue("objectBefore")); + }, + set: function(v) { + this.setDataValue("objectBefore", JSON.stringify(v)); + } + } + }); + }); + + Database.prototype._afterInit.push(function() { + this._conn.model("Pad").hasMany(this._conn.model("History"), this._makeNotNullForeignKey("History", "padId")); + this._conn.model("History").belongsTo(this._conn.model("Pad"), this._makeNotNullForeignKey("pad", "padId")); + }); + + + // ===================================================================================================================== + + + utils.extend(Database.prototype, { + addHistoryEntry(padId, data) { + return this._createPadObject("History", padId, data).then((historyEntry) => { + this.emit("addHistoryEntry", historyEntry); + }); + }, + + getHistory(padId) { + return this._getPadObjects("History", padId); + } + }); +}; \ No newline at end of file diff --git a/server/database/line.js b/server/database/line.js new file mode 100644 index 00000000..5c3f6268 --- /dev/null +++ b/server/database/line.js @@ -0,0 +1,301 @@ +var Sequelize = require("sequelize"); +var underscore = require("underscore"); + +var utils = require("../utils"); +var routing = require("../routing"); + +module.exports = function(Database) { + Database.prototype._init.push(function() { + this._conn.define("Line", { + routePoints : { + type: Sequelize.TEXT, + allowNull: false, + get: function() { + var routePoints = this.getDataValue("routePoints"); + return routePoints != null ? JSON.parse(routePoints) : routePoints; + }, + set: function(v) { + for(var i=0; i { + return this.getLinePoints(data.id, bboxWithZoom).then((trackPoints) => { + data.trackPoints = trackPoints; + return data; + }); + }); + }, + + getLineTemplate(padId, data) { + return utils.promiseAuto({ + lineTemplate: () => { + return JSON.parse(JSON.stringify(this._conn.model("Line").build(utils.extend({ }, data, { padId: padId, data: data.data || { } })))); + }, + + type: this.getType(padId, data.typeId), + + styles: (lineTemplate, type) => { + if(type.defaultColour) + lineTemplate.colour = type.defaultColour; + if(type.defaultWidth) + lineTemplate.width = type.defaultWidth; + if(type.defaultMode) + lineTemplate.mode = type.defaultMode; + + return this._updateObjectStyles(lineTemplate, true); + } + }).then((res) => { + return res.lineTemplate; + }); + }, + + getLine(padId, lineId) { + return this._conn.model("Line").findOne({ where: { id: lineId, padId: padId }, include: [ this._conn.model("LineData") ] }).then(line => { + if(line == null) + throw new Error("Line " + lineId + " of pad " + padId + " could not be found."); + + return line; + }); + }, + + createLine(padId, data) { + return utils.promiseAuto({ + defaultVals: this.getType(padId, data.typeId).then((type) => { + if(type.defaultColour && !("colour" in data)) + data.colour = type.defaultColour; + if(type.defaultWidth && !("width" in data)) + data.width = type.defaultWidth; + if(type.defaultMode && !("mode" in data)) + data.mode = type.defaultMode; + }), + + routing: (defaultVals) => { + return this._calculateRouting(data); + }, + + createLine: (routing, defaultVals) => { + var dataCopy = utils.extend({ }, data); + delete dataCopy.trackPoints; // They came if mode is track + + return this._createPadObjectWithData("Line", padId, dataCopy); + }, + + lineEvent: (createLine) => { + // We have to emit this before calling _setLinePoints so that this event is sent to the client first + this.emit("line", padId, createLine); + }, + + setLinePoints: (routing, createLine, lineEvent) => { + return this._setLinePoints(padId, createLine.id, routing); + }, + + updateStyle: (createLine) => { + return this._updateObjectStyles(createLine, true); + } + }).then(res => res.createLine); + }, + + updateLine(padId, lineId, data, doNotUpdateStyles) { + return utils.promiseAuto({ + originalLine: this.getLine(padId, lineId), + + routing: (originalLine) => { + if(data.routePoints == null) + data.routePoints = originalLine.routePoints; + + if(data.mode == null) + data.mode = originalLine.mode || ""; + + if((data.mode == "track" && data.trackPoints) || !underscore.isEqual(data.routePoints, originalLine.routePoints) || data.mode != originalLine.mode) + return this._calculateRouting(data); // Also sets data.distance and data.time + }, + + newLine: (routing) => { + var dataCopy = utils.extend({ }, data); + delete dataCopy.trackPoints; // They came if mode is track + + return this._updatePadObjectWithData("Line", padId, lineId, dataCopy); + }, + + updateStyle: (newLine) => { + if(!doNotUpdateStyles) + return this._updateObjectStyles(newLine, true); // Modifies newLine + }, + + linePoints: (newLine, routing) => { + if(routing) + return this._setLinePoints(newLine.padId, lineId, routing); + } + }).then((res) => { + this.emit("line", padId, res.newLine); + + return res.newLine; + }); + }, + + _setLinePoints(padId, lineId, trackPoints, _noEvent) { + return this._conn.model("LinePoint").destroy({ where: { lineId: lineId } }).then(() => { + var create = [ ]; + for(var i=0; i { + if(!_noEvent) + this.emit("linePoints", padId, lineId, points); + }); + }, + + deleteLine(padId, lineId) { + return utils.promiseAuto({ + line: this._deletePadObjectWithData("Line", padId, lineId), + points: this._setLinePoints(padId, lineId, [ ], true) + }).then((res) => { + this.emit("deleteLine", padId, { id: lineId }); + + return data; + }); + }, + + getLinePointsForPad(padId, bboxWithZoom) { + return utils.filterStreamPromise(this.getPadLines(padId, "id"), (line) => { + return this.getLinePoints(line.id, bboxWithZoom).then((trackPoints) => { + if(trackPoints.length >= 2) + return { id: line.id, trackPoints: trackPoints }; + }); + }); + }, + + getLinePoints(lineId, bboxWithZoom) { + return Promise.resolve().then(() => { + return this._conn.model("Line").build({ id: lineId }).getLinePoints({ + where: Sequelize.and(this._makeBboxCondition(bboxWithZoom), bboxWithZoom ? { zoom: { lte: bboxWithZoom.zoom } } : null), + attributes: [ "idx" ], + order: "idx" + }); + }).then((data) => { + // Get one more point outside of the bbox for each segment + var indexes = [ ]; + for(var i=0; i= 2) { + line.distance = utils.calculateDistance(line.trackPoints); + line.time = null; + + routing._calculateZoomLevels(line.trackPoints); + + for(var i=0; i= 2 && line.mode && line.mode != "track") { + return routing.calculateRouting(line.routePoints, line.mode).then((routeData) => { + line.distance = routeData.distance; + line.time = routeData.time; + for(var i=0; i { + if(type.defaultColour) + data.colour = type.defaultColour; + if(type.defaultSize) + data.size = type.defaultSize; + if(type.defaultSymbol) + data.symbol = type.defaultSymbol; + + return this._createPadObjectWithData("Marker", padId, data); + }, + styles: (create) => { + return this._updateObjectStyles(create, false) + } + }).then((res) => { + this.emit("marker", padId, res.create); + + return res.create; + }); + }, + + updateMarker(padId, markerId, data, doNotUpdateStyles) { + return utils.promiseAuto({ + update: this._updatePadObjectWithData("Marker", padId, markerId, data), + updateStyles: (update) => { + if(!doNotUpdateStyles) + return this._updateObjectStyles(update, false); + } + }).then((res) => { + this.emit("marker", padId, res.update); + + return res.update; + }); + }, + + deleteMarker(padId, markerId) { + this._deletePadObjectWithData("Marker", padId, markerId).then(del => { + this.emit("deleteMarker", padId, { id: del.id }); + + return del; + }); + } + }); +}; \ No newline at end of file diff --git a/server/database/migrations.js b/server/database/migrations.js new file mode 100644 index 00000000..b608d92a --- /dev/null +++ b/server/database/migrations.js @@ -0,0 +1,89 @@ +var Sequelize = require("sequelize"); + +var utils = require("../utils"); + +module.exports = function(Database) { + utils.extend(Database.prototype, { + _runMigrations() { + var queryInterface = this._conn.getQueryInterface(); + + var renameColMigrations = Promise.all([ + queryInterface.describeTable('Lines').then((attributes) => { + var promises = [ ]; + + // Rename Line.points to Line.routePoints + if(attributes.points) { + promises.push(queryInterface.renameColumn('Lines', 'points', 'routePoints')); + } + + // Change routing type "shortest" / "fastest" to "car", add type "track" + if(attributes.mode.type.indexOf("shortest") != -1) { + promises.push( + Promise.resolve().then(() => { + return queryInterface.changeColumn('Lines', 'mode', { + type: Sequelize.ENUM("", "shortest", "fastest", "car", "bicycle", "pedestrian"), allowNull: false, defaultValue: "" + }); + }).then(() => { + return this._conn.model("Line").update({ mode: "car" }, { where: { mode: { $in: [ "fastest", "shortest" ] } } }); + }).then(() => { + return queryInterface.changeColumn('Lines', 'mode', { + type: Sequelize.ENUM("", "car", "bicycle", "pedestrian", "track"), allowNull: false, defaultValue: "" + }); + }) + ); + } + + return Promise.all(promises); + }) + ]); + + var changeColMigrations = Promise.all([ 'Pads', 'Markers', 'Lines' ].map((table) => { + // allow null on Pad.name, Marker.name, Line.name + return queryInterface.describeTable(table).then((attributes) => { + if(!attributes.name.allowNull) + return queryInterface.changeColumn(table, 'name', { type: Sequelize.TEXT, allowNull: true }); + }); + })); + + var addColMigrations = renameColMigrations.then(() => { + return Promise.all([ 'Marker', 'Type' ].map((table) => { + queryInterface.describeTable(table+"s").then((attributes) => { + var promises = [ ]; + var model = this._conn.model(table); + for(var attribute in model.attributes) { + if(!attributes[attribute]) + promises.push(queryInterface.addColumn(table+"s", attribute, model.attributes[attribute])); + } + return Promise.all(promises); + }); + })); + + /*queryInterface.describeTable('Markers').then(function(attributes) { + var promises = [ ]; + + // Add size and symbol columns + if(!attributes.size) + promises.push(queryInterface.addColumn('Markers', 'size', Marker.attributes.size)); + if(!attributes.symbol) + promises.push(queryInterface.addColumn('Markers', 'symbol', Marker.attributes.symbol)); + + return Promise.all(promises); + }), + + queryInterface.describeTable('Types').then(function(attributes) { + return Promise.all([ 'defaultColour', 'colourFixed', 'defaultSize', 'sizeFixed', 'defaultSymbol', 'symbolFixed', 'defaultWidth', 'widthFixed', 'defaultMode', 'modeFixed' ].map(function(col) { + if(!attributes[col]) + return queryInterface.addColumn('Types', col, Type.attributes[col]); + })); + }), + + queryInterface.describeTable('Views').then(function(attributes) { + if(!attributes.filter) + return queryInterface.addColumn('Views', 'filter', View.attributes.filter); + })*/ + }); + + return Promise.all([ renameColMigrations, changeColMigrations, addColMigrations ]); + } + }); +}; \ No newline at end of file diff --git a/server/database/pad.js b/server/database/pad.js new file mode 100644 index 00000000..ff64887e --- /dev/null +++ b/server/database/pad.js @@ -0,0 +1,193 @@ +var Sequelize = require("sequelize"); + +var utils = require("../utils"); + +module.exports = function(Database) { + Database.prototype._init.push(function() { + this._conn.define("Pad", { + id : { type: Sequelize.STRING, allowNull: false, primaryKey: true, validate: { is: /^.+$/ } }, + name: { type: Sequelize.TEXT, allowNull: true, get: function() { return this.getDataValue("name") || "New FacilMap"; } }, + writeId: { type: Sequelize.STRING, allowNull: false, validate: { is: /^.+$/ } } + }); + }); + + Database.prototype._afterInit.push(function() { + this._conn.model("Pad").belongsTo(this._conn.model("View"), { as: "defaultView", foreignKey: "defaultViewId", constraints: false }); + }); + + // ===================================================================================================================== + + utils.extend(Database.prototype, { + padIdExists(padId) { + return this._conn.model("Pad").count({ where: { $or: [ { id: padId }, { writeId: padId } ] } }).then(function(num) { + return num > 0; + }); + }, + + getPadData(padId) { + return this._conn.model("Pad").findOne({ where: { id: padId }, include: [ { model: this._conn.model("View"), as: "defaultView" } ]}); + }, + + getPadDataByWriteId(writeId) { + return this._conn.model("Pad").findOne({ where: { writeId: writeId }, include: [ { model: this._conn.model("View"), as: "defaultView" } ] }); + }, + + createPad(data) { + return utils.promiseAuto({ + validate: () => { + if(!data.id || data.id.length == 0) + throw "Invalid read-only ID"; + if(!data.writeId || data.writeId.length == 0) + throw "Invalid write-only ID"; + if(data.id == data.writeId) + throw "Read-only and write-only ID cannot be the same."; + + return Promise.all([ + this.padIdExists(data.id).then((exists) => { + if(exists) + throw "ID '" + data.id + "' is already taken."; + }), + this.padIdExists(data.writeId).then((exists) => { + if(exists) + throw "ID '" + data.writeId + "' is already taken."; + }) + ]); + }, + + create: (validate) => { + return this._conn.model("Pad").create(data); + }, + + types: (create) => { + return Promise.all(this.DEFAULT_TYPES.map((it) => { + return this.createType(data.id, it); + })); + } + }).then(res => { + return res.create; + }); + }, + + updatePadData(padId, data) { + return utils.promiseAuto({ + oldData: this.getPadData(padId), + + validateRead: () => { + if(data.id != null && data.id != padId && data.id.length == 0) + throw "Invalid read-only ID"; + + var existsPromises = [ ]; + + if(data.id != null && data.id != padId) { + return this.padIdExists(data.id).then((exists) => { + if(exists) + throw "ID '" + data.id + "' is already taken."; + }); + } + }, + + validateWrite: (oldData) => { + if(data.writeId != null && data.writeId != oldData.writeId) { + if(data.writeId.length == 0) + throw "Invalid write-only ID"; + if(data.writeId == (data.id != null ? data.id : padId)) + throw "Read-only and write-only ID cannot be the same."; + + return this.padIdExists(data.writeId).then((exists) => { + if(exists) + throw "ID '" + data.writeId + "' is already taken."; + }); + } + }, + + update: (validateRead, validateWrite) => { + return this._conn.model("Pad").update(data, { where: { id: padId } }).then(res => { + if(res[0] == 0) + throw "Pad " + padId + " could not be found."; + return res; + }); + }, + + newData: (update) => this.getPadData(data.id || padId), + + /*history: (oldData, update) => { + return this.addHistoryEntry(data.id || padId, { + type: "pad", + action: "update", + objectBefore: oldData + }); + }*/ + }).then((res) => { + this.emit("padData", padId, res.newData); + + return res.newData; + }); + } + + /*function copyPad(fromPadId, toPadId, callback) { + function _handleStream(stream, next, cb) { + stream.on("data", function(data) { + stream.pause(); + cb(data, function() { + stream.resume(); + }); + }); + + stream.on("error", next); + stream.on("end", next); + } + + async.auto({ + fromPadData : function(next) { + backend.getPadData(fromPadId, next); + }, + toPadData : function(next) { + getPadData(toPadId, next); + }, + padsExist : [ "fromPadData", "toPadData", function(r, next) { + if(!r.fromPadData) + return next(new Error("Pad "+fromPadId+" does not exist.")); + if(!r.toPadData.writable) + return next(new Error("Destination pad is read-only.")); + + toPadId = r.toPadData.id; + + next(); + }], + copyMarkers : [ "padsExist", function(r, next) { + _handleStream(getPadMarkers(fromPadId, null), next, function(marker, cb) { + createMarker(toPadId, marker, cb); + }); + }], + copyLines : [ "padsExist", function(r, next) { + _handleStream(getPadLines(fromPadId), next, function(line, cb) { + async.auto({ + createLine : function(next) { + _createLine(toPadId, line, next); + }, + getLinePoints : function(next) { + backend.getLinePoints(line.id, next); + }, + setLinePoints : [ "createLine", "getLinePoints", function(r, next) { + _setLinePoints(toPadId, r.createLine.id, r.getLinePoints, next); + } ] + }, cb); + }); + }], + copyViews : [ "padsExist", function(r, next) { + _handleStream(getViews(fromPadId), next, function(view, cb) { + createView(toPadId, view, function(err, newView) { + if(err) + return cb(err); + + if(r.fromPadData.defaultView && r.fromPadData.defaultView.id == view.id && r.toPadData.defaultView == null) + updatePadData(toPadId, { defaultView: newView.id }, cb); + else + cb(); + }); + }); + }] + }, callback); + }*/ + }); +}; \ No newline at end of file diff --git a/server/database/type.js b/server/database/type.js new file mode 100644 index 00000000..eb25e0f9 --- /dev/null +++ b/server/database/type.js @@ -0,0 +1,175 @@ +var Sequelize = require("sequelize"); + +var utils = require("../utils"); + +module.exports = function(Database) { + Database.prototype._init.push(function() { + var Type = this._conn.define("Type", { + name: { type: Sequelize.TEXT, allowNull: false }, + type: { type: Sequelize.ENUM("marker", "line"), allowNull: false }, + defaultColour: { type: Sequelize.STRING(6), allowNull: true, validate: this._TYPES.validateColour }, + colourFixed: { type: Sequelize.BOOLEAN, allowNull: true }, + defaultSize: { type: Sequelize.INTEGER.UNSIGNED, allowNull: true, validate: { min: 15 } }, + sizeFixed: { type: Sequelize.BOOLEAN, allowNull: true }, + defaultSymbol: { type: Sequelize.TEXT, allowNull: true}, + symbolFixed: { type: Sequelize.BOOLEAN, allowNull: true}, + defaultWidth: { type: Sequelize.INTEGER.UNSIGNED, allowNull: true, validate: { min: 1 } }, + widthFixed: { type: Sequelize.BOOLEAN, allowNull: true }, + defaultMode: { type: Sequelize.ENUM("", "car", "bicycle", "pedestrian", "track"), allowNull: true }, + modeFixed: { type: Sequelize.BOOLEAN, allowNull: true }, + + fields: { + type: Sequelize.TEXT, + allowNull: false, + get: function() { + return JSON.parse(this.getDataValue("fields")); + }, + set: function(v) { + return this.setDataValue("fields", JSON.stringify(v)); + }, + validate: { + checkUniqueFieldName: (obj) => { + obj = JSON.parse(obj); + var fields = { }; + for(var i=0; i= 1)) + throw new Error("Invalid width "+obj[i].options[j].width+" in field "+obj[i].name+"."); + } + } + } + } + } + } + }, { + validate: { + defaultValsNotNull: function() { + if(this.colourFixed && this.defaultColour == null) + throw "Fixed colour cannot be undefined."; + if(this.sizeFixed && this.defaultSize == null) + throw "Fixed size cannot be undefined."; + if(this.widthFixed && this.defaultWidth == null) + throw "Fixed width cannot be undefined."; + } + } + }); + }); + + Database.prototype._afterInit.push(function() { + this._conn.model("Type").belongsTo(this._conn.model("Pad"), this._makeNotNullForeignKey("pad", "padId")); + this._conn.model("Pad").hasMany(this._conn.model("Type"), { foreignKey: "padId" }); + }); + + // ===================================================================================================================== + + utils.extend(Database.prototype, { + DEFAULT_TYPES: [ + { name: "Marker", type: "marker", fields: [ { name: "Description", type: "textarea" } ] }, + { name: "Line", type: "line", fields: [ { name: "Description", type: "textarea" } ] } + ], + + getTypes(padId) { + return this._getPadObjects("Type", padId); + }, + + getType(padId, typeId) { + return this._conn.model("Type").findOne({ where: { id: typeId, padId: padId } }).then(res => { + if(res == null) + throw new Error("Type " + typeId + " of pad " + padId + " could not be found."); + + return res; + }); + }, + + createType(padId, data) { + return Promise.resolve().then(() => { + if(data.name == null || data.name.trim().length == 0) + throw "No name provided."; + + return this._createPadObject("Type", padId, data); + }).then((data) => { + this.emit("type", data.padId, data); + + return data; + }); + }, + + updateType(padId, typeId, data) { + return Promise.resolve().then(() => { + if(data.name == null || data.name.trim().length == 0) + throw "No name provided."; + + return this._updatePadObject("Type", padId, typeId, data); + }).then((data) => { + this.emit("type", data.padId, data); + + return this._updateObjectStyles(data.type == "line" ? this.getPadLinesByType(data.padId, typeId) : this.getPadMarkersByType(data.padId, typeId), data.type == "line").then(() => data); + }); + }, + + _optionsToObj(options, idx) { + var ret = { }; + if(options) { + for(var i=0; i { + return !!res[0] || !!res[1]; + }); + }, + + deleteType(padId, typeId) { + return utils.promiseAuto({ + isUsed: this.isTypeUsed(padId, typeId), + del: (isUsed) => { + if(isUsed) + throw "This type is in use."; + + return this._deletePadObject("Type", padId, typeId); + } + }).then((res) => { + this.emit("deleteType", padId, { id: res.del.id }); + + return res.del; + }); + } + }); +}; \ No newline at end of file diff --git a/server/database/view.js b/server/database/view.js new file mode 100644 index 00000000..8f65db31 --- /dev/null +++ b/server/database/view.js @@ -0,0 +1,78 @@ +var Sequelize = require("sequelize"); + +var utils = require("../utils"); + +module.exports = function(Database) { + Database.prototype._init.push(function() { + this._conn.define("View", { + name : { type: Sequelize.TEXT, allowNull: false }, + baseLayer : { type: Sequelize.TEXT, allowNull: false }, + layers : { + type: Sequelize.TEXT, + allowNull: false, + get: function() { + return JSON.parse(this.getDataValue("layers")); + }, + set: function(v) { + this.setDataValue("layers", JSON.stringify(v)); + } + }, + top : this._TYPES.lat, + bottom : this._TYPES.lat, + left : this._TYPES.lon, + right : this._TYPES.lon, + filter: { type: Sequelize.TEXT, allowNull: true } + }); + }); + + Database.prototype._afterInit.push(function() { + this._conn.model("View").belongsTo(this._conn.model("Pad"), this._makeNotNullForeignKey("pad", "padId")); + this._conn.model("Pad").hasMany(this._conn.model("View"), { foreignKey: "padId" }); + }); + + // ===================================================================================================================== + + utils.extend(Database.prototype, { + getViews(padId) { + return this._getPadObjects("View", padId); + }, + + createView(padId, data) { + return utils.promiseAuto({ + create: () => { + if(data.name == null || data.name.trim().length == 0) + throw "No name provided."; + + return this._createPadObject("View", padId, data); + }, + + /*history: (create) => { + return this.addHistoryEntry(padId, { + type: "view", + action: "create" + }); + }*/ + }).then((res) => { + this.emit("view", padId, res.create); + + return res.create; + }); + }, + + updateView(padId, viewId, data) { + return this._updatePadObject("View", padId, viewId, data).then((newData) => { + this.emit("view", padId, newData); + + return newData; + }); + }, + + deleteView(padId, viewId) { + return this._deletePadObject("View", padId, viewId).then((data) => { + this.emit("deleteView", padId, { id: data.id }); + + return data; + }); + } + }); +}; \ No newline at end of file diff --git a/server/databaseBackendSequelize.js b/server/databaseBackendSequelize.js deleted file mode 100644 index 10d74849..00000000 --- a/server/databaseBackendSequelize.js +++ /dev/null @@ -1,666 +0,0 @@ -var Sequelize = require("sequelize"); -var config = require("../config"); -var utils = require("./utils"); -var Promise = require("promise"); - -var conn = new Sequelize(config.db.database, config.db.user, config.db.password, { - dialect: config.db.type, - host: config.db.host, - port: config.db.port, - define: { - timestamps: false - } -}); - - -/*********************/ -/* Types and Helpers */ -/*********************/ - -function getLatType() { - return { - type: Sequelize.FLOAT(9, 6), - allowNull: false, - validate: { - min: -90, - max: 90 - } - }; -} - -function getLonType() { - return { - type: Sequelize.FLOAT(9, 6), - allowNull: false, - validate: { - min: -180, - max: 180 - } - }; -} - -var validateColour = { is: /^[a-fA-F0-9]{3}([a-fA-F0-9]{3})?$/ }; - -var dataDefinition = { - "name" : { type: Sequelize.TEXT, allowNull: false }, - "value" : { type: Sequelize.TEXT, allowNull: false } -}; - -function _makeNotNullForeignKey(type, field, error) { - return { - as: type, - onDelete: error ? "RESTRICT" : "CASCADE", - foreignKey: { name: field, allowNull: false } - } -} - - -/**********/ -/* Tables */ -/**********/ - -/* Pads */ - -var Pad = conn.define("Pad", { - id : { type: Sequelize.STRING, allowNull: false, primaryKey: true, validate: { is: /^.+$/ } }, - name: { type: Sequelize.TEXT, allowNull: true, get: function() { return this.getDataValue("name") || "New FacilMap"; } }, - writeId: { type: Sequelize.STRING, allowNull: false, validate: { is: /^.+$/ } } -}); - - -/* Markers */ - -var Marker = conn.define("Marker", { - "lat" : getLatType(), - "lon" : getLonType(), - name : { type: Sequelize.TEXT, allowNull: true, get: function() { return this.getDataValue("name") || "Untitled marker"; } }, - colour : { type: Sequelize.STRING(6), allowNull: false, defaultValue: "ff0000", validate: validateColour }, - size : { type: Sequelize.INTEGER.UNSIGNED, allowNull: false, defaultValue: 25, validate: { min: 15 } }, - symbol : { type: Sequelize.TEXT, allogNull: true } -}); - -Pad.hasMany(Marker, { foreignKey: "padId" }); -Marker.belongsTo(Pad, _makeNotNullForeignKey("pad", "padId")); - -var MarkerData = conn.define("MarkerData", dataDefinition); - -MarkerData.belongsTo(Marker, _makeNotNullForeignKey("marker", "markerId")); -Marker.hasMany(MarkerData, { foreignKey: "markerId" }); - - -/* Lines */ - -var Line = conn.define("Line", { - routePoints : { - type: Sequelize.TEXT, - allowNull: false, - get: function() { - var routePoints = this.getDataValue("routePoints"); - return routePoints != null ? JSON.parse(routePoints) : routePoints; - }, - set: function(v) { - for(var i=0; i= 1)) - throw new Error("Invalid width "+obj[i].options[j].width+" in field "+obj[i].name+"."); - } - } - } - } - } - } -}, { - validate: { - defaultValsNotNull: function() { - if(this.colourFixed && this.defaultColour == null) - throw "Fixed colour cannot be undefined."; - if(this.sizeFixed && this.defaultSize == null) - throw "Fixed size cannot be undefined."; - if(this.widthFixed && this.defaultWidth == null) - throw "Fixed width cannot be undefined."; - } - } -}); - -Pad.hasMany(Type, { foreignKey: "padId" }); -Type.belongsTo(Pad, _makeNotNullForeignKey("pad", "padId")); - -Marker.belongsTo(Type, _makeNotNullForeignKey("type", "typeId", true)); -Line.belongsTo(Type, _makeNotNullForeignKey("type", "typeId", true)); - - -function connect(force) { - conn.authenticate().then(function() { - return conn.sync({ force: !!force }); - }).then(function() { - // Migrations - - var queryInterface = conn.getQueryInterface(); - return Promise.all([ - queryInterface.describeTable('Lines').then(function(attributes) { - var promises = [ ]; - - // Rename Line.points to Line.routePoints - if(attributes.points) { - promises.push(queryInterface.renameColumn('Lines', 'points', 'routePoints')); - } - - // Change routing type "shortest" / "fastest" to "car", add type "track" - if(attributes.mode.type.indexOf("shortest") != -1) { - promises.push( - Promise.resolve().then(function() { - return queryInterface.changeColumn('Lines', 'mode', { - type: Sequelize.ENUM("", "shortest", "fastest", "car", "bicycle", "pedestrian"), allowNull: false, defaultValue: "" - }); - }).then(function() { - return Line.update({ mode: "car" }, { where: { mode: { $in: [ "fastest", "shortest" ] } } }); - }).then(function() { - return queryInterface.changeColumn('Lines', 'mode', { - type: Sequelize.ENUM("", "car", "bicycle", "pedestrian", "track"), allowNull: false, defaultValue: "" - }); - }) - ); - } - - return Promise.all(promises); - }), - queryInterface.describeTable('Markers').then(function(attributes) { - var promises = [ ]; - - // Add size and symbol columns - if(!attributes.size) - promises.push(queryInterface.addColumn('Markers', 'size', Marker.attributes.size)); - if(!attributes.symbol) - promises.push(queryInterface.addColumn('Markers', 'symbol', Marker.attributes.symbol)); - - return Promise.all(promises); - }), - queryInterface.describeTable('Types').then(function(attributes) { - return Promise.all([ 'defaultColour', 'colourFixed', 'defaultSize', 'sizeFixed', 'defaultSymbol', 'symbolFixed', 'defaultWidth', 'widthFixed', 'defaultMode', 'modeFixed' ].map(function(col) { - if(!attributes[col]) - return queryInterface.addColumn('Types', col, Type.attributes[col]); - })); - }), - queryInterface.describeTable('Views').then(function(attributes) { - if(!attributes.filter) - return queryInterface.addColumn('Views', 'filter', View.attributes.filter); - }) - ].concat([ 'Pads', 'Markers', 'Lines' ].map(function(table) { - // allow null on Pad.name, Marker.name, Line.name - return queryInterface.describeTable(table).then(function(attributes) { - if(!attributes.name.allowNull) - return queryInterface.changeColumn(table, 'name', { type: Sequelize.TEXT, allowNull: true }); - }); - }))); - }); -} - -function padIdExists(padId) { - return Pad.count({ where: { $or: [ { id: padId }, { writeId: padId } ] } }).then(function(num) { - return num > 0; - }); -} - -function getPadData(padId) { - return Pad.findOne({ where: { id: padId }, include: [ { model: View, as: "defaultView" } ]}); -} - -function getPadDataByWriteId(writeId) { - return Pad.findOne({ where: { writeId: writeId }, include: [ { model: View, as: "defaultView" } ] }); -} - -function createPad(data) { - return Pad.create(data); -} - -function updatePadData(padId, data) { - return Pad.update(data, { where: { id: padId } }).then(function() { - return getPadData(data.id || padId); - }); -} - -function _getPadObjects(type, padId, condition) { - var ret = new utils.ArrayStream(); - - Pad.build({ id: padId })["get"+type+"s"](condition).then(function(objs) { - objs.forEach(function(it) { - if(it[type+"Data"] != null) { - it.data = _dataFromArr(it[type+"Data"]); - it.setDataValue("data", it.data); // For JSON.stringify() - it.setDataValue(type+"Data", undefined); - } - }); - - ret.receiveArray(null, objs); - }, function(err) { - ret.receiveArray(err); - }); - return ret; -} - -function _createPadObject(type, padId, data) { - var obj = conn.model(type).build(data); - obj.padId = padId; - return obj.save(); -} - -function _createPadObjectWithData(type, padId, data) { - return _createPadObject(type, padId, data).then(function(obj) { - if(data.data != null) { - obj.data = data.data; - obj.setDataValue("data", obj.data); // For JSON.stringify() - return _setObjectData(type, obj.id, data.data).then(function() { - return obj; - }); - } else { - obj.data = { }; - obj.setDataValue("data", obj.data); // For JSON.stringify() - return obj; - } - }); -} - -function _updatePadObject(type, objId, data) { - return conn.model(type).update(data, { where: { id: objId } }).then(function() { - return conn.model(type).findById(objId); - }); -} - -function _updatePadObjectWithData(type, objId, data) { - return Promise.all([ - _updatePadObject(type, objId, data), - data.data != null ? _setObjectData(type, objId, data.data) : _getObjectData(type, objId) - ]).then(function(results) { - var obj = results[0]; - obj.data = (data.data != null ? data.data : results[1]); - obj.setDataValue("data", obj.data); // For JSON.stringify() - return obj; - }); -} - -function _deletePadObject(type, objId) { - return conn.model(type).findById(objId).then(function(obj) { - return obj.destroy().then(function() { - return obj; - }); - }); -} - -function _deletePadObjectWithData(type, objId) { - return _setObjectData(type, objId, { }).then(function() { - return _deletePadObject(type, objId); // Return the object - }); -} - -function _dataToArr(data, extend) { - var dataArr = [ ]; - for(var i in data) - dataArr.push(utils.extend({ name: i, value: data[i] }, extend)); - return dataArr; -} - -function _dataFromArr(dataArr) { - var data = { }; - for(var i=0; i { + var views = ''; + return utils.streamEachPromise(database.getViews(padId), (view) => { + views += '\n'; + }).then(() => views); + }, - var viewsP = utils.streamEachPromise(database.getViews(padId), function(view) { - views += '\n'; - }); + typesObj: () => { + var typesObj = { }; + return utils.streamEachPromise(database.getTypes(padId), function(type) { + typesObj[type.id] = type; + }).then(() => typesObj); + }, - var typesObj = { }; - var typesObjP = utils.streamEachPromise(database.getTypes(padId), function(type) { - typesObj[type.id] = type; - }); + types: (typesObj) => { + var types = ''; + for(var i in typesObj) { + var type = typesObj[i]; + types += '\n'; + } + return types; + }, - var typesMarkersLinesP = typesObjP.then(function() { - for(var i in typesObj) { - var type = typesObj[i]; - types += '\n'; - } - - return Promise.all([ - utils.streamEachPromise(database.getPadMarkers(padId), function(marker) { + markers: (typesObj) => { + var markers = ''; + return utils.streamEachPromise(database.getPadMarkers(padId), function(marker) { markers += '\n' + '\t' + _e(marker.name) + '\n' + '\t' + _e(_dataToText(typesObj[marker.typeId].fields, marker.data)) + '\n' + @@ -74,8 +80,12 @@ function exportGpx(padId, useTracks) { '\t\t' + _e(marker.colour) + '\n' + '\t\n' + '\n'; - }), - utils.streamEachPromise(database.getPadLinesWithPoints(padId), function(line) { + }).then(() => markers); + }, + + lines: (typesObj) => { + var lines = ''; + return utils.streamEachPromise(database.getPadLinesWithPoints(padId), function(line) { var t = (useTracks || line.mode == "track"); lines += '<' + (t ? 'trk' : 'rte') + '>\n' + @@ -101,25 +111,22 @@ function exportGpx(padId, useTracks) { } lines += '\n'; - }) - ]); - }); - - return Promise.all([ padDataP, viewsP, typesMarkersLinesP ]).then(function(res) { - return '\n' + + }).then(() => lines); + } + }).then((res) => '\n' + '\n' + - '\t\n' + - '\t\t' + _e(res[0].name) + '\n' + - '\t\t\n' + - '\t\n' + - '\t\n' + - views.replace(/^(.)/gm, '\t\t$1') + - types.replace(/^(.)/gm, '\t\t$1') + - '\t\n' + - markers.replace(/^(.)/gm, '\t$1') + - lines.replace(/^(.)/gm, '\t$1') + - ''; - }); + '\t\n' + + '\t\t' + _e(res.padData.name) + '\n' + + '\t\t\n' + + '\t\n' + + '\t\n' + + res.views.replace(/^(.)/gm, '\t\t$1') + + res.types.replace(/^(.)/gm, '\t\t$1') + + '\t\n' + + res.markers.replace(/^(.)/gm, '\t$1') + + res.lines.replace(/^(.)/gm, '\t$1') + + '' + ); } module.exports = { diff --git a/server/routing.js b/server/routing.js index 92e4bf8b..9bace2f3 100644 --- a/server/routing.js +++ b/server/routing.js @@ -1,7 +1,7 @@ var request = require("request-promise"); + var utils = require("./utils"); var config = require("../config"); -var Promise = require("promise"); var ROUTING_URL = "https://api.mapbox.com/directions/v5/mapbox"; diff --git a/server/search.js b/server/search.js index 867e6fbf..c3b24013 100644 --- a/server/search.js +++ b/server/search.js @@ -1,9 +1,10 @@ var request = require("request-promise"); -var config = require("../config"); var Promise = require("promise"); var cheerio = require("cheerio"); var zlib = require("zlib"); var compressjs = require("compressjs"); + +var config = require("../config"); var utils = require("./utils"); request = request.defaults({ diff --git a/server/server.js b/server/server.js index 72137399..c21db25d 100644 --- a/server/server.js +++ b/server/server.js @@ -1,17 +1,14 @@ var http = require("http"); -var socketIo = require("socket.io"); -var config = require("../config"); -var listeners = require("./listeners"); -var database = require("./database"); +var compression = require("compression"); var domain = require("domain"); -var utils = require("./utils"); -var routing = require("./routing"); -var gpx = require("./gpx"); -var search = require("./search"); var Promise = require("promise"); var express = require("express"); var path = require("path"); -var compression = require("compression"); + +var config = require("../config"); +var Database = require("./database/database"); +var utils = require("./utils"); +var Socket = require("./socket"); var frontendPath = path.resolve(__dirname + "/../frontend"); @@ -28,370 +25,33 @@ Object.defineProperty(Error.prototype, "toJSON", { configurable: true }); -var dbP = database.connect(); - -var app = express(); - -app.use(compression()); - -app.use(express.static(frontendPath + "/build/")); - -app.get("/:padId", function(req, res) { - res.sendFile(frontendPath + "/build/index.html"); +process.on('unhandledRejection', (reason, promise) => { + console.trace("Unhandled rejection", reason); }); -var server = http.createServer(app); +utils.promiseAuto({ + database: () => new Database(), -var serverP = Promise.denodeify(server.listen.bind(server))(config.port, config.host).then(function() { - var io = socketIo.listen(server); + databaseConnect: database => database.connect(), - io.sockets.on("connection", function(socket) { - var d = domain.create(); - d.add(socket); - - d.on("error", function(err) { - console.error("Uncaught error in socket:", err.stack); - socket.disconnect(); + server: () => { + var app = express(); + app.use(compression()); + app.use(express.static(frontendPath + "/build/")); + app.get("/:padId", function(req, res) { + res.sendFile(frontendPath + "/build/index.html"); }); - var handlers = { - error : function(err) { - console.error("Error! Disconnecting client."); - console.error(err.stack); - socket.disconnect(); - }, + var server = http.createServer(app); + return Promise.denodeify(server.listen.bind(server))(config.port, config.host).then(() => server); + }, - setPadId : function(padId) { - return Promise.resolve().then(function() { - if(typeof padId != "string") - throw "Invalid pad id"; - if(socket.padId != null) - throw "Pad id already set"; - - socket.padId = true; - - return database.getPadData(padId); - }).then(function(data) { - return _setPadId(socket, data); - }); - }, - - updateBbox : function(bbox) { - if(!utils.stripObject(bbox, { top: "number", left: "number", bottom: "number", right: "number", zoom: "number" })) - return; - - var bboxWithExcept = utils.extend({ }, bbox); - if(socket.bbox && bbox.zoom == socket.bbox.zoom) - bboxWithExcept.except = socket.bbox; - - socket.bbox = bbox; - - if(socket.padId && socket.padId !== true) { - return utils.promiseAllObject({ - marker: utils.streamToArrayPromise(database.getPadMarkers(socket.padId, bboxWithExcept)), - linePoints: utils.streamToArrayPromise(database.getLinePoints(socket.padId, bboxWithExcept)) - }); - } - }, - - disconnect : function() { - if(socket.padId) - listeners.removePadListener(socket); - }, - - createPad : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { name: "string", defaultViewId: "number", id: "string", writeId: "string" })) - throw "Invalid parameters."; - - if(socket.padId) - throw "Pad already loaded."; - - return database.createPad(data); - }).then(function(padData) { - return _setPadId(socket, padData); - }); - }, - - editPad : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { name: "string", defaultViewId: "number", id: "string", writeId: "string" })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.updatePadData(socket.padId, data); - }); - }, - - addMarker : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { lat: "number", lon: "number", name: "string", colour: "string", size: "number", symbol: "string", typeId: "number", data: Object } )) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.createMarker(socket.padId, data); - }); - }, - - editMarker : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { id: "number", lat: "number", lon: "number", name: "string", colour: "string", size: "number", symbol: "string", typeId: "number", data: Object })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.updateMarker(data.id, data); - }); - }, - - deleteMarker : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { id: "number" })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.deleteMarker(data.id); - }); - }, - - getLineTemplate : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { typeId: "number" }) || data.typeId == null) - throw "Invalid parameters."; - - return database.getLineTemplate(data); - }); - }, - - addLine : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { routePoints: [ { lat: "number", lon: "number" } ], trackPoints: [ { lat: "number", lon: "number" } ], mode: "string", colour: "string", width: "number", name: "string", typeId: "number", data: Object })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.createLine(socket.padId, data); - }); - }, - - editLine : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { id: "number", routePoints: [ { lat: "number", lon: "number" } ], trackPoints: [ { lat: "number", lon: "number" } ], mode: "string", colour: "string", width: "number", name: "string", typeId: "number", data: Object })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.updateLine(data.id, data); - }); - }, - - deleteLine : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { id: "number" })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.deleteLine(data.id); - }); - }, - - addView : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { name: "string", baseLayer: "string", layers: [ "string" ], top: "number", left: "number", right: "number", bottom: "number", filter: "string" })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.createView(socket.padId, data); - }); - }, - - editView : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { id: "number", baseLayer: "string", layers: [ "string" ], top: "number", left: "number", right: "number", bottom: "number", filter: "string" })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.updateView(data.id, data); - }); - }, - - deleteView : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { id: "number" })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.deleteView(data.id); - }); - }, - - addType : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { - id: "number", - name: "string", - type: "string", - defaultColour: "string", colourFixed: "boolean", - defaultSize: "number", sizeFixed: "boolean", - defaultSymbol: "string", symbolFixed: "boolean", - defaultWidth: "number", widthFixed: "boolean", - defaultMode: "string", modeFixed: "boolean", - fields: [ { - name: "string", - type: "string", - default: "string", - controlColour: "boolean", controlSize: "boolean", controlSymbol: "boolean", controlWidth: "boolean", - options: [ { key: "string", value: "string", colour: "string", size: "number", "symbol": "string", width: "number" } ] - }] - })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.createType(socket.padId, data); - }); - }, - - editType : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { - id: "number", - name: "string", - defaultColour: "string", colourFixed: "boolean", - defaultSize: "number", sizeFixed: "boolean", - defaultSymbol: "string", symbolFixed: "boolean", - defaultWidth: "number", widthFixed: "boolean", - defaultMode: "string", modeFixed: "boolean", - fields: [ { - name: "string", - type: "string", - default: "string", - controlColour: "boolean", controlSize: "boolean", controlSymbol: "boolean", controlWidth: "boolean", - options: [ { key: "string", value: "string", colour: "string", size: "number", "symbol": "string", width: "number" } ] - }] - })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.updateType(data.id, data); - }); - }, - - deleteType : function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { id: "number" })) - throw "Invalid parameters."; - - if(!socket.writable) - throw "In read-only mode."; - - return database.deleteType(data.id); - }); - }, - - exportGpx : function(data) { - return Promise.resolve().then(function() { - if(socket.padId == null) - throw "No pad ID set."; - - return gpx.exportGpx(socket.padId, data.useTracks); - }); - }, - - find: function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { query: "string", loadUrls: "boolean" })) - throw "Invalid parameters."; - - return search.find(data.query, data.loadUrls); - }); - }, - - getRoute: function(data) { - return Promise.resolve().then(function() { - if(!utils.stripObject(data, { destinations: [ { lat: "number", lon: "number" } ], mode: "string" })) - throw "Invalid parameters."; - - return routing.calculateRouting(data.destinations, data.mode, true); - }); - } - - /*copyPad : function(data, callback) { - if(!utils.stripObject(data, { toId: "string" })) - return callback("Invalid parameters."); - - database.copyPad(socket.padId, data.toId, callback); - }*/ - }; - - for(var i in handlers) { (function(i) { - socket.on(i, function(data, callback) { - Promise.resolve(data).then(handlers[i]).then(function(res) { // nodeify(callback); - callback(null, res); - }, function(err) { - console.log(err.stack); - callback(err); - }); - }); - })(i); } - }); -}); - -Promise.all([ dbP, serverP ]).then(function() { + socket: (server, database) => { + return new Socket(server, database); + } +}).then(res => { console.log("Server started on " + (config.host || "*" ) + ":" + config.port); -}).catch(function(err) { +}).catch(err => { console.error(err); process.exit(1); -}); - -function _sendStreamData(socket, eventName, stream) { - stream.on("data", function(data) { - if(data != null) - socket.emit(eventName, data); - }).on("error", function(err) { - console.warn("_sendStreamData", err, err.stack); - socket.emit("error", err); - }) -} - -function _setPadId(socket, data) { - socket.padId = data.id; - socket.writable = data.writable; - listeners.addPadListener(socket); - - var promises = { - padData: [ data ], - view: utils.streamToArrayPromise(database.getViews(socket.padId)), - type: utils.streamToArrayPromise(database.getTypes(socket.padId)), - line: utils.streamToArrayPromise(database.getPadLines(socket.padId)) - }; - - if(socket.bbox) { // In case bbox is set while fetching pad data - utils.extend(promises, { - marker: utils.streamToArrayPromise(database.getPadMarkers(socket.padId, socket.bbox)), - linePoints: utils.streamToArrayPromise(database.getLinePoints(socket.padId, socket.bbox)) - }); - } - - return utils.promiseAllObject(promises); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/server/socket.js b/server/socket.js new file mode 100644 index 00000000..4fdb2989 --- /dev/null +++ b/server/socket.js @@ -0,0 +1,526 @@ +var socketIo = require("socket.io"); +var domain = require("domain"); + +var utils = require("./utils"); +var routing = require("./routing"); +var search = require("./search"); +var gpx = require("./gpx"); + +class Socket { + constructor(server, database) { + var io = socketIo.listen(server); + + io.sockets.on("connection", (socket) => { + var d = domain.create(); + d.add(socket); + + d.on("error", function(err) { + console.error("Uncaught error in socket:", err.stack); + socket.disconnect(); + }); + + new SocketConnection(socket, database); + }); + } +} + +class SocketConnection { + constructor(socket, database) { + this.socket = socket; + this.database = database; + + this.padId = null; + this.bbox = null; + this.writable = null; + + this._dbHandlers = [ ]; + + this.registerSocketHandlers(); + } + + registerSocketHandlers() { + Object.keys(this.socketHandlers).forEach((i) => { + this.socket.on(i, (data, callback) => { + Promise.resolve(data).then(this.socketHandlers[i].bind(this)).then((res) => { // nodeify(callback); + if(!callback && res) + console.trace("No callback available to send result of socket handler " + i); + + callback && callback(null, res); + }, (err) => { + console.log(err.stack); + + callback && callback(err); + }).catch(err => { + console.error("Error in socket handler for "+i, err.stack); + }); + }); + }); + } + + registerDatabaseHandler(eventName, handler) { + var func = handler.bind(this); + + this.database.on(eventName, func); + + if(!this._dbHandlers[eventName]) + this._dbHandlers[eventName] = [ ]; + + this._dbHandlers[eventName].push(func); + + return () => { + this.database.removeListener(eventName, func); + this._dbHandlers[eventName] = this._dbHandlers[eventName].filter(function(it) { return it !== func; }); + }; + } + + registerDatabaseHandlers() { + Object.keys(this.databaseHandlers).forEach((eventName) => { + this.registerDatabaseHandler(eventName, this.databaseHandlers[eventName]); + }); + } + + unregisterDatabaseHandlers() { + Object.keys(this._dbHandlers).forEach((eventName) => { + this._dbHandlers[eventName].forEach((it) => { + this.database.removeListener(eventName, it); + }); + }); + this._dbHandlers = { }; + } + + sendStreamData(eventName, stream) { + stream.on("data", (data) => { + if(data != null) + this.socket.emit(eventName, data); + }).on("error", (err) => { + console.warn("SocketConnection.sendStreamData", err.stack); + this.socket.emit("error", err); + }); + } + + getPadObjects(padData) { + var promises = { + padData: [ padData ], + view: utils.streamToArrayPromise(this.database.getViews(padData.id)), + type: utils.streamToArrayPromise(this.database.getTypes(padData.id)), + line: utils.streamToArrayPromise(this.database.getPadLines(padData.id)) + }; + + if(this.bbox) { // In case bbox is set while fetching pad data + utils.extend(promises, { + marker: utils.streamToArrayPromise(this.database.getPadMarkers(padData.id, this.bbox)), + linePoints: utils.streamToArrayPromise(this.database.getLinePointsForPad(padData.id, this.bbox)) + }); + } + + return utils.promiseAllObject(promises); + } +} + +utils.extend(SocketConnection.prototype, { + socketHandlers: { + error : function(err) { + console.error("Error! Disconnecting client."); + console.error(err.stack); + this.socket.disconnect(); + }, + + setPadId : function(padId) { + return utils.promiseAuto({ + validate: () => { + if(typeof padId != "string") + throw "Invalid pad id"; + if(this.padId != null) + throw "Pad id already set"; + + this.padId = true; + }, + + write: (validate) => { + return this.database.getPadDataByWriteId(padId); + }, + + read: (validate) => { + return this.database.getPadData(padId); + }, + + pad: (write, read) => { + if(write) + return utils.extend(JSON.parse(JSON.stringify(write)), { writable: true }); + else if(read) + return utils.extend(JSON.parse(JSON.stringify(read)), { writable: false, writeId: null }); + else + throw "This pad does not exist"; + } + }).then(res => { + this.padId = res.pad.id; + this.writable = res.pad.writable; + + this.registerDatabaseHandlers(); + + return this.getPadObjects(res.pad); + }); + }, + + updateBbox : function(bbox) { + if(!utils.stripObject(bbox, { top: "number", left: "number", bottom: "number", right: "number", zoom: "number" })) + return; + + var bboxWithExcept = utils.extend({ }, bbox); + if(this.bbox && bbox.zoom == this.bbox.zoom) + bboxWithExcept.except = this.bbox; + + this.bbox = bbox; + + if(this.padId && this.padId !== true) { + return utils.promiseAllObject({ + marker: utils.streamToArrayPromise(this.database.getPadMarkers(this.padId, bboxWithExcept)), + linePoints: utils.streamToArrayPromise(this.database.getLinePointsForPad(this.padId, bboxWithExcept)) + }); + } + }, + + disconnect : function() { + if(this.padId) + this.unregisterDatabaseHandlers(); + }, + + createPad : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { name: "string", defaultViewId: "number", id: "string", writeId: "string" })) + throw "Invalid parameters."; + + if(this.padId) + throw "Pad already loaded."; + + return this.database.createPad(data); + }).then((padData) => { + this.padId = padData.id; + this.writable = true; + + this.registerDatabaseHandlers(); + + return this.getPadObjects(padData); + }); + }, + + editPad : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { name: "string", defaultViewId: "number", id: "string", writeId: "string" })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.updatePadData(this.padId, data); + }); + }, + + addMarker : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { lat: "number", lon: "number", name: "string", colour: "string", size: "number", symbol: "string", typeId: "number", data: Object } )) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.createMarker(this.padId, data); + }); + }, + + editMarker : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { id: "number", lat: "number", lon: "number", name: "string", colour: "string", size: "number", symbol: "string", typeId: "number", data: Object })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.updateMarker(this.padId, data.id, data); + }); + }, + + deleteMarker : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { id: "number" })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.deleteMarker(this.padId, data.id); + }); + }, + + getLineTemplate : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { typeId: "number" }) || data.typeId == null) + throw "Invalid parameters."; + + return this.database.getLineTemplate(this.padId, data); + }); + }, + + addLine : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { routePoints: [ { lat: "number", lon: "number" } ], trackPoints: [ { lat: "number", lon: "number" } ], mode: "string", colour: "string", width: "number", name: "string", typeId: "number", data: Object })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.createLine(this.padId, data); + }); + }, + + editLine : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { id: "number", routePoints: [ { lat: "number", lon: "number" } ], trackPoints: [ { lat: "number", lon: "number" } ], mode: "string", colour: "string", width: "number", name: "string", typeId: "number", data: Object })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.updateLine(this.padId, data.id, data); + }); + }, + + deleteLine : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { id: "number" })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.deleteLine(this.padId, data.id); + }); + }, + + addView : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { name: "string", baseLayer: "string", layers: [ "string" ], top: "number", left: "number", right: "number", bottom: "number", filter: "string" })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.createView(this.padId, data); + }); + }, + + editView : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { id: "number", baseLayer: "string", layers: [ "string" ], top: "number", left: "number", right: "number", bottom: "number", filter: "string" })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.updateView(this.padId, data.id, data); + }); + }, + + deleteView : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { id: "number" })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.deleteView(this.padId, data.id); + }); + }, + + addType : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { + id: "number", + name: "string", + type: "string", + defaultColour: "string", colourFixed: "boolean", + defaultSize: "number", sizeFixed: "boolean", + defaultSymbol: "string", symbolFixed: "boolean", + defaultWidth: "number", widthFixed: "boolean", + defaultMode: "string", modeFixed: "boolean", + fields: [ { + name: "string", + type: "string", + default: "string", + controlColour: "boolean", controlSize: "boolean", controlSymbol: "boolean", controlWidth: "boolean", + options: [ { key: "string", value: "string", colour: "string", size: "number", "symbol": "string", width: "number" } ] + }] + })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.createType(this.padId, data); + }); + }, + + editType : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { + id: "number", + name: "string", + defaultColour: "string", colourFixed: "boolean", + defaultSize: "number", sizeFixed: "boolean", + defaultSymbol: "string", symbolFixed: "boolean", + defaultWidth: "number", widthFixed: "boolean", + defaultMode: "string", modeFixed: "boolean", + fields: [ { + name: "string", + type: "string", + default: "string", + controlColour: "boolean", controlSize: "boolean", controlSymbol: "boolean", controlWidth: "boolean", + options: [ { key: "string", value: "string", colour: "string", size: "number", "symbol": "string", width: "number" } ] + }] + })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.updateType(this.padId, data.id, data); + }); + }, + + deleteType : function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { id: "number" })) + throw "Invalid parameters."; + + if(!this.writable) + throw "In read-only mode."; + + return this.database.deleteType(this.padId, data.id); + }); + }, + + exportGpx : function(data) { + return Promise.resolve().then(() => { + if(this.padId == null) + throw "No pad ID set."; + + return gpx.exportGpx(this.database, this.padId, data.useTracks); + }); + }, + + find: function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { query: "string", loadUrls: "boolean" })) + throw "Invalid parameters."; + + return search.find(data.query, data.loadUrls); + }); + }, + + getRoute: function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { destinations: [ { lat: "number", lon: "number" } ], mode: "string" })) + throw "Invalid parameters."; + + return routing.calculateRouting(data.destinations, data.mode, true); + }); + }, + + /*listenToHistory: function() { + return Promise.resolve().then(() => { + if(this.padId == null) + throw "No pad ID set."; + + if(this.historyListener) + throw "Already listening to history."; + + this.historyListener = this.registerDatabaseHandler("addHistoryEntry", function(data) { + this.socket.emit("history", data); + }); + + return utils.promiseAllObject({ + history: utils.streamToArrayPromise(this.database.getHistory(this.padId)) + }); + }); + }, + + stopListeningToHistory: function() { + return Promise.resolve().then(() => { + if(!this.historyListener) + throw "Not listening to history."; + + this.historyListener(); // Unregister db listener + this.historyListener = null; + }); + }*/ + + /*copyPad : function(data, callback) { + if(!utils.stripObject(data, { toId: "string" })) + return callback("Invalid parameters."); + + this.database.copyPad(this.padId, data.toId, callback); + }*/ + }, + + databaseHandlers: { + line: function(padId, data) { + if(padId == this.padId) + this.socket.emit("line", data); + }, + + linePoints: function(padId, lineId, trackPoints) { + if(padId == this.padId) + this.socket.emit("linePoints", { reset: true, id: lineId, trackPoints : (this.bbox ? routing.prepareForBoundingBox(trackPoints, this.bbox) : [ ]) }); + }, + + deleteLine: function(padId, data) { + if(padId == this.padId) + this.socket.emit("deleteLine", data); + }, + + marker: function(padId, data) { + if(padId == this.padId && this.bbox && utils.isInBbox(data, this.bbox)) + this.socket.emit("marker", data); + }, + + deleteMarker: function(padId, data) { + if(padId == this.padId) + this.socket.emit("deleteMarker", data); + }, + + type: function(padId, data) { + if(padId == this.padId) + this.socket.emit("type", data); + }, + + deleteType: function(padId, data) { + if(padId == this.padId) + this.socket.emit("deleteType", data); + }, + + padData: function(padId, data) { + if(padId == this.padId) { + var dataClone = JSON.parse(JSON.stringify(data)); + if(!this.writable) + dataClone.writeId = null; + + this.padId = data.id; + + this.socket.emit("padData", dataClone); + } + }, + + view: function(padId, data) { + if(padId == this.padId) + this.socket.emit("view", data); + }, + + deleteView: function(padId, data) { + if(padId == this.padId) + this.socket.emit("deleteView", data); + } + } +}); + +module.exports = Socket; \ No newline at end of file diff --git a/server/utils.js b/server/utils.js index 86f4294e..26690b30 100644 --- a/server/utils.js +++ b/server/utils.js @@ -235,6 +235,59 @@ function promiseAllObject(obj) { }); } +function promiseAuto(obj) { + var promises = { }; + + function _get(str) { + if(!obj[str]) + throw new Error("Invalid dependency '" + str + "' in promiseAuto()."); + + if(promises[str]) + return promises[str]; + + if(obj[str].then) + return obj[str]; + + var params = getFuncParams(obj[str]); + return promises[str] = _getDeps(params).then(function(res) { + return obj[str].apply(null, params.map(function(param) { return res[param]; })); + }); + } + + function _getDeps(arr) { + var deps = { }; + arr.forEach(function(it) { + deps[it] = _get(it); + }); + return promiseAllObject(deps); + } + + return _getDeps(Object.keys(obj)); +} + +function getFuncParams(func) { + // Taken from angular injector code + + var ARROW_ARG = /^([^\(]+?)\s*=>/; + var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; + var FN_ARG_SPLIT = /\s*,\s*/; + var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; + + var fnText = (Function.prototype.toString.call(func) + ' ').replace(STRIP_COMMENTS, ''); + var params = (fnText.match(ARROW_ARG) || fnText.match(FN_ARGS))[1]; + return params == "" ? [ ] : params.split(FN_ARG_SPLIT); +} + +function modifyFunction(obj, prop, before, after) { + var bkp = obj[prop]; + obj[prop] = function() { + before && before.apply(this, arguments); + var ret = bkp.apply(this, arguments); + after && after.apply(this, arguments); + return ret; + }; +} + module.exports = { isInBbox : isInBbox, filterStreamPromise : filterStreamPromise, @@ -248,5 +301,7 @@ module.exports = { isoDate : isoDate, round: round, streamToArrayPromise: streamToArrayPromise, - promiseAllObject: promiseAllObject + promiseAllObject: promiseAllObject, + promiseAuto: promiseAuto, + modifyFunction: modifyFunction }; \ No newline at end of file