Restructure server code

pull/54/merge
Candid Dauth 2016-10-30 17:01:21 +03:00
rodzic 6d191703f2
commit 3369ace90d
18 zmienionych plików z 1944 dodań i 1979 usunięć

Wyświetl plik

@ -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<options.length; i++) {
ret[options[i].key] = options[i][idx];
}
}
return ret;
}
function deleteType(typeId) {
return backend.isTypeUsed(typeId).then(function(isUsed) {
if(isUsed)
throw "This type is in use.";
return backend.deleteType(typeId);
}).then(function(data) {
listeners.notifyPadListeners(data.padId, "deleteType", { id: data.id });
return data;
});
}
function getPadMarkers(padId, bbox) {
return backend.getPadMarkers(padId, bbox);
}
function createMarker(padId, data) {
return Promise.resolve().then(function() {
return backend.getType(data.typeId);
}).then(function(type) {
if(type.defaultColour)
data.colour = type.defaultColour;
if(type.defaultSize)
data.size = type.defaultSize;
if(type.defaultSymbol)
data.symbol = type.defaultSymbol;
return backend.createMarker(padId, data)
}).then(function(data) {
listeners.notifyPadListeners(padId, "marker", _getMarkerDataFunc(data));
return _updateObjectStyles(data, false).then(function() {
return data;
});
});
}
function updateMarker(markerId, data) {
return _updateMarker(markerId, data).then(function(data) {
return _updateObjectStyles(data, false).then(function() {
return data;
});
});
}
function _updateMarker(markerId, data) {
return backend.updateMarker(markerId, data).then(function(data) {
listeners.notifyPadListeners(data.padId, "marker", _getMarkerDataFunc(data));
return data;
});
}
function deleteMarker(markerId) {
return backend.deleteMarker(markerId).then(function(data) {
listeners.notifyPadListeners(data.padId, "deleteMarker", { id: data.id });
return data;
});
}
function _updateObjectStyles(objectStream, isLine) {
if(!(objectStream instanceof stream.Readable))
objectStream = new utils.ArrayStream([ objectStream ]);
var types = { };
return utils.streamEachPromise(objectStream, function(object) {
return Promise.resolve().then(function() {
if(!types[object.typeId]) {
return backend.getType(object.typeId).then(function(type) {
if(type == null)
throw "Type "+object.typeId+" does not exist.";
return types[object.typeId] = type;
});
} else
return types[object.typeId];
}).then(function(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(function(field) {
if(field.type == "dropdown" && (field.controlColour || (!isLine && field.controlSize) || (!isLine && field.controlSymbol) || (isLine && field.controlWidth))) {
var _find = function(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 ? _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<line.trackPoints.length; i++)
line.trackPoints[i].idx = i;
return Promise.resolve(line.trackPoints);
} else if(line.routePoints && line.routePoints.length >= 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<routeData.trackPoints.length; i++)
routeData.trackPoints[i].idx = i;
return routeData.trackPoints;
});
} else {
line.distance = utils.calculateDistance(line.routePoints);
line.time = null;
var trackPoints = [ ];
for(var i=0; i<line.routePoints.length; i++) {
trackPoints.push(utils.extend({ }, line.routePoints[i], { zoom: 1, idx: i }));
}
return Promise.resolve(trackPoints);
}
}
function _getMarkerDataFunc(marker) {
return function(listener) {
if(!listener || !listener.bbox || !utils.isInBbox(marker, listener.bbox))
return null;
return marker;
};
}
function _getLinePoints(lineId, bboxWithZoom) {
return backend.getLinePointsByBbox(lineId, bboxWithZoom).then(function(data) {
// Get one more point outside of the bbox for each segment
var indexes = [ ];
for(var i=0; i<data.length; i++) {
if(i == 0 || data[i-1].idx != data[i].idx-1) // Beginning of segment
indexes.push(data[i].idx-1);
indexes.push(data[i].idx);
if(i == data.length-1 || data[i+1].idx != data[i].idx+1) // End of segment
indexes.push(data[i].idx+1);
}
if(indexes.length == 0)
return [ ];
return backend.getLinePointsByIdx(lineId, indexes);
});
}
module.exports = {
connect : backend.connect,
getPadData : getPadData,
padIdExists : padIdExists,
createPad : createPad,
updatePadData : updatePadData,
getViews : getViews,
createView : createView,
updateView : updateView,
deleteView : deleteView,
getTypes : getTypes,
createType : createType,
updateType : updateType,
deleteType : deleteType,
getPadMarkers : getPadMarkers,
createMarker : createMarker,
updateMarker : updateMarker,
deleteMarker : deleteMarker,
getPadLines : getPadLines,
getPadLinesWithPoints : getPadLinesWithPoints,
getLineTemplate : getLineTemplate,
createLine : createLine,
updateLine : updateLine,
deleteLine : deleteLine,
getLinePoints : getLinePoints,
//copyPad : copyPad,
_defaultTypes : DEFAULT_TYPES
};

Wyświetl plik

@ -0,0 +1,50 @@
var util = require("util");
var events = require("events");
var Sequelize = require("sequelize");
var utils = require("../utils");
var config = require("../../config");
class Database extends events.EventEmitter {
constructor() {
super();
this._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
}
});
for(let func of this._init)
func.call(this);
for(let func of this._afterInit)
func.call(this);
}
connect(force) {
return this._conn.authenticate().then(() => {
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;

Wyświetl plik

@ -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<dataArr.length; i++)
data[dataArr[i].name] = dataArr[i].value;
return data;
},
_getObjectData(type, objId) {
var filter = { };
filter[type.toLowerCase()+"Id"] = objId;
return this._conn.model(type+"Data").findAll({ where: filter}).then((dataArr) => {
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);
}
});
};

Wyświetl plik

@ -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);
}
});
};

Wyświetl plik

@ -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<v.length; i++) {
v[i].lat = 1*v[i].lat.toFixed(6);
v[i].lon = 1*v[i].lon.toFixed(6);
}
this.setDataValue("routePoints", JSON.stringify(v));
},
validate: {
minTwo: function(val) {
var routePoints = JSON.parse(val);
if(!Array.isArray(routePoints))
throw new Error("routePoints is not an array");
if(routePoints.length < 2)
throw new Error("A line cannot have less than two route points.");
}
}
},
mode : { type: Sequelize.ENUM("", "car", "bicycle", "pedestrian", "track"), allowNull: false, defaultValue: "" },
colour : { type: Sequelize.STRING(6), allowNull: false, defaultValue: "0000ff", validate: this._TYPES.validateColour },
width : { type: Sequelize.INTEGER.UNSIGNED, allowNull: false, defaultValue: 4, validate: { min: 1 } },
name : { type: Sequelize.TEXT, allowNull: true, get: function() { return this.getDataValue("name") || "Untitled line"; } },
distance : { type: Sequelize.FLOAT(24, 2).UNSIGNED, allowNull: true },
time : { type: Sequelize.INTEGER.UNSIGNED, allowNull: true }
});
this._conn.define("LinePoint", {
lat: this._TYPES.lat,
lon: this._TYPES.lon,
zoom: { type: Sequelize.INTEGER.UNSIGNED, allowNull: false, validate: { min: 1, max: 20 } },
idx: { type: Sequelize.INTEGER.UNSIGNED, allowNull: false }
});
this._conn.define("LineData", this._TYPES.dataDefinition);
});
Database.prototype._afterInit.push(function() {
var Line = this._conn.model("Line");
var LinePoint = this._conn.model("LinePoint");
var LineData = this._conn.model("LineData");
Line.belongsTo(this._conn.model("Pad"), this._makeNotNullForeignKey("pad", "padId"));
this._conn.model("Pad").hasMany(Line, { foreignKey: "padId" });
// TODO: Cascade
Line.belongsTo(this._conn.model("Type"), this._makeNotNullForeignKey("type", "typeId", true));
LinePoint.belongsTo(Line, this._makeNotNullForeignKey("line", "lineId"));
Line.hasMany(LinePoint, { foreignKey: "lineId" });
LineData.belongsTo(Line, this._makeNotNullForeignKey("line", "lineId"));
Line.hasMany(LineData, { foreignKey: "lineId" });
});
// =====================================================================================================================
utils.extend(Database.prototype, {
getPadLines(padId, fields) {
var cond = { include: [ this._conn.model("LineData") ] };
if(fields)
cond.attributes = (typeof fields == "string" ? fields.split(/\s+/) : fields);
return this._getPadObjects("Line", padId, cond);
},
getPadLinesByType(padId, typeId) {
return this._getPadObjects("Line", padId, { where: { typeId: typeId }, include: [ this._conn.model("LineData") ] });
},
getPadLinesWithPoints(padId, bboxWithZoom) {
return utils.filterStreamPromise(this.getPadLines(padId), (data) => {
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<trackPoints.length; i++) {
create.push(utils.extend({ }, trackPoints[i], { lineId: lineId }));
}
return this._conn.model("LinePoint").bulkCreate(create);
}).then((points) => {
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<data.length; i++) {
if(i == 0 || data[i-1].idx != data[i].idx-1) // Beginning of segment
indexes.push(data[i].idx-1);
indexes.push(data[i].idx);
if(i == data.length-1 || data[i+1].idx != data[i].idx+1) // End of segment
indexes.push(data[i].idx+1);
}
if(indexes.length == 0)
return [ ];
return this.getLinePointsByIdx(lineId, indexes);
});
},
getLinePointsByIdx(lineId, indexes) {
return this._conn.model("Line").build({ id: lineId }).getLinePoints({
where: { idx: indexes },
attributes: [ "lon", "lat", "idx" ],
order: "idx"
});
},
_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<line.trackPoints.length; i++)
line.trackPoints[i].idx = i;
return Promise.resolve(line.trackPoints);
} else if(line.routePoints && line.routePoints.length >= 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<routeData.trackPoints.length; i++)
routeData.trackPoints[i].idx = i;
return routeData.trackPoints;
});
} else {
line.distance = utils.calculateDistance(line.routePoints);
line.time = null;
var trackPoints = [ ];
for(var i=0; i<line.routePoints.length; i++) {
trackPoints.push(utils.extend({ }, line.routePoints[i], { zoom: 1, idx: i }));
}
return Promise.resolve(trackPoints);
}
}
});
};

Wyświetl plik

@ -0,0 +1,85 @@
var Sequelize = require("sequelize");
var utils = require("../utils");
module.exports = function(Database) {
Database.prototype._init.push(function() {
this._conn.define("Marker", {
"lat" : this._TYPES.lat,
"lon" : this._TYPES.lon,
name : { type: Sequelize.TEXT, allowNull: true, get: function() { return this.getDataValue("name") || "Untitled marker"; } },
colour : { type: Sequelize.STRING(6), allowNull: false, defaultValue: "ff0000", validate: this._TYPES.validateColour },
size : { type: Sequelize.INTEGER.UNSIGNED, allowNull: false, defaultValue: 25, validate: { min: 15 } },
symbol : { type: Sequelize.TEXT, allowNull: true }
});
this._conn.define("MarkerData", this._TYPES.dataDefinition);
});
Database.prototype._afterInit.push(function() {
var Marker = this._conn.model("Marker");
this._conn.model("Pad").hasMany(Marker, this._makeNotNullForeignKey("Markers", "padId"));
Marker.belongsTo(this._conn.model("Pad"), this._makeNotNullForeignKey("pad", "padId"));
Marker.belongsTo(this._conn.model("Type"), this._makeNotNullForeignKey("type", "typeId", true));
this._conn.model("MarkerData").belongsTo(Marker, this._makeNotNullForeignKey("marker", "markerId"));
Marker.hasMany(this._conn.model("MarkerData"), { foreignKey: "markerId" });
});
// =====================================================================================================================
utils.extend(Database.prototype, {
getPadMarkers(padId, bbox) {
return this._getPadObjects("Marker", padId, { where: this._makeBboxCondition(bbox), include: [ this._conn.model("MarkerData") ] });
},
getPadMarkersByType(padId, typeId) {
return this._getPadObjects("Marker", padId, { where: { padId: padId, typeId: typeId }, include: [ this._conn.model("MarkerData") ] });
},
createMarker(padId, data) {
return utils.promiseAuto({
type: this.getType(padId, data.typeId),
create: (type) => {
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;
});
}
});
};

Wyświetl plik

@ -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 ]);
}
});
};

Wyświetl plik

@ -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);
}*/
});
};

Wyświetl plik

@ -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<obj.length; i++) {
if(obj[i].name.trim().length == 0)
throw new Error("Empty field name.");
if(fields[obj[i].name])
throw new Error("field name "+obj[i].name+" is not unique.");
fields[obj[i].name] = true;
if([ "textarea", "dropdown", "checkbox", "input" ].indexOf(obj[i].type) == -1)
throw new Error("Invalid field type "+obj[i].type+" for field "+obj[i].name+".");
if(obj[i].controlColour) {
if(!obj[i].options || obj[i].options.length < 1)
throw new Error("No options specified for colour-controlling field "+obj[i].name+".");
for(var j=0; j<obj[i].options.length; j++) {
if(!obj[i].options[j].colour || !obj[i].options[j].colour.match(this._TYPES.validateColour.is))
throw new Error("Invalid colour "+obj[i].options[j].colour+" in field "+obj[i].name+".");
}
}
if(obj[i].controlSize) {
if(!obj[i].options || obj[i].options.length < 1)
throw new Error("No options specified for size-controlling field "+obj[i].name+".");
for(var j=0; j<obj[i].options.length; j++) {
if(!obj[i].options[j].size || !isFinite(obj[i].options[j].size) || obj[i].options[j].size < 15)
throw new Error("Invalid size "+obj[i].options[j].size+" in field "+obj[i].name+".");
}
}
if(obj[i].controlSymbol) {
if(!obj[i].options || obj[i].options.length < 1)
throw new Error("No options specified for icon-controlling field "+obj[i].name+".");
}
if(obj[i].controlWidth) {
if(!obj[i].options || obj[i].options.length < 1)
throw new Error("No options specified for width-controlling field "+obj[i].name+".");
for(var j=0; j<obj[i].options.length; j++) {
if(!obj[i].options[j].width || !(1*obj[i].options[j].width >= 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<options.length; i++) {
ret[options[i].key] = options[i][idx];
}
}
return ret;
},
isTypeUsed(padId, typeId) {
return Promise.all([
this._conn.model("Marker").findOne({ where: { padId: padId, typeId: typeId } }),
this._conn.model("Line").findOne({ where: { padId: padId, typeId: typeId } })
]).then(res => {
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;
});
}
});
};

Wyświetl plik

@ -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;
});
}
});
};

Wyświetl plik

@ -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<v.length; i++) {
v[i].lat = 1*v[i].lat.toFixed(6);
v[i].lon = 1*v[i].lon.toFixed(6);
}
this.setDataValue("routePoints", JSON.stringify(v));
},
validate: {
minTwo: function(val) {
var routePoints = JSON.parse(val);
if(!Array.isArray(routePoints))
throw new Error("routePoints is not an array");
if(routePoints.length < 2)
throw new Error("A line cannot have less than two route points.");
}
}
},
mode : { type: Sequelize.ENUM("", "car", "bicycle", "pedestrian", "track"), allowNull: false, defaultValue: "" },
colour : { type: Sequelize.STRING(6), allowNull: false, defaultValue: "0000ff", validate: validateColour },
width : { type: Sequelize.INTEGER.UNSIGNED, allowNull: false, defaultValue: 4, validate: { min: 1 } },
name : { type: Sequelize.TEXT, allowNull: true, get: function() { return this.getDataValue("name") || "Untitled line"; } },
distance : { type: Sequelize.FLOAT(24, 2).UNSIGNED, allowNull: true },
time : { type: Sequelize.INTEGER.UNSIGNED, allowNull: true }
});
Pad.hasMany(Line, { foreignKey: "padId" });
Line.belongsTo(Pad, _makeNotNullForeignKey("pad", "padId"));
var LinePoint = conn.define("LinePoint", {
lat: getLatType(),
lon: getLonType(),
zoom: { type: Sequelize.INTEGER.UNSIGNED, allowNull: false, validate: { min: 1, max: 20 } },
idx: { type: Sequelize.INTEGER.UNSIGNED, allowNull: false }
});
LinePoint.belongsTo(Line, _makeNotNullForeignKey("line", "lineId"));
Line.hasMany(LinePoint, { foreignKey: "lineId" });
var LineData = conn.define("LineData", dataDefinition);
LineData.belongsTo(Line, _makeNotNullForeignKey("line", "lineId"));
Line.hasMany(LineData, { foreignKey: "lineId" });
/* Views */
var View = 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 : getLatType(),
bottom : getLatType(),
left : getLonType(),
right : getLonType(),
filter: { type: Sequelize.TEXT, allowNull: true }
});
Pad.hasMany(View, { foreignKey: "padId" });
View.belongsTo(Pad, _makeNotNullForeignKey("pad", "padId"));
Pad.belongsTo(View, { as: "defaultView", foreignKey: "defaultViewId", constraints: false });
/* Types */
var Type = 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: 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: function(obj) {
obj = JSON.parse(obj);
var fields = { };
for(var i=0; i<obj.length; i++) {
if(obj[i].name.trim().length == 0)
throw new Error("Empty field name.");
if(fields[obj[i].name])
throw new Error("field name "+obj[i].name+" is not unique.");
fields[obj[i].name] = true;
if([ "textarea", "dropdown", "checkbox", "input" ].indexOf(obj[i].type) == -1)
throw new Error("Invalid field type "+obj[i].type+" for field "+obj[i].name+".");
if(obj[i].controlColour) {
if(!obj[i].options || obj[i].options.length < 1)
throw new Error("No options specified for colour-controlling field "+obj[i].name+".");
for(var j=0; j<obj[i].options.length; j++) {
if(!obj[i].options[j].colour || !obj[i].options[j].colour.match(validateColour.is))
throw new Error("Invalid colour "+obj[i].options[j].colour+" in field "+obj[i].name+".");
}
}
if(obj[i].controlSize) {
if(!obj[i].options || obj[i].options.length < 1)
throw new Error("No options specified for size-controlling field "+obj[i].name+".");
for(var j=0; j<obj[i].options.length; j++) {
if(!obj[i].options[j].size || !isFinite(obj[i].options[j].size) || obj[i].options[j].size < 15)
throw new Error("Invalid size "+obj[i].options[j].size+" in field "+obj[i].name+".");
}
}
if(obj[i].controlSymbol) {
if(!obj[i].options || obj[i].options.length < 1)
throw new Error("No options specified for icon-controlling field "+obj[i].name+".");
}
if(obj[i].controlWidth) {
if(!obj[i].options || obj[i].options.length < 1)
throw new Error("No options specified for width-controlling field "+obj[i].name+".");
for(var j=0; j<obj[i].options.length; j++) {
if(!obj[i].options[j].width || !(1*obj[i].options[j].width >= 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<dataArr.length; i++)
data[dataArr[i].name] = dataArr[i].value;
return data;
}
function _getObjectData(type, objId) {
var filter = { };
filter[type.toLowerCase()+"Id"] = objId;
return conn.model(type+"Data").findAll({ where: filter}).then(function(dataArr) {
return _dataFromArr(dataArr);
});
}
function _setObjectData(type, objId, data) {
var model = conn.model(type+"Data");
var idObj = { };
idObj[type.toLowerCase()+"Id"] = objId;
return model.destroy({ where: idObj}).then(function() {
return model.bulkCreate(_dataToArr(data, idObj));
});
}
function getViews(padId) {
return _getPadObjects("View", padId);
}
function createView(padId, data) {
return _createPadObject("View", padId, data);
}
function updateView(viewId, data) {
return _updatePadObject("View", viewId, data);
}
function deleteView(viewId) {
return _deletePadObject("View", viewId);
}
function getType(typeId) {
return Type.findById(typeId);
}
function getTypes(padId) {
return _getPadObjects("Type", padId);
}
function createType(padId, data) {
return _createPadObject("Type", padId, data);
}
function updateType(typeId, data) {
return _updatePadObject("Type", typeId, data);
}
function deleteType(typeId) {
return _deletePadObject("Type", typeId);
}
function isTypeUsed(typeId) {
return Promise.all([
Marker.findOne({ where: { typeId: typeId } }),
Line.findOne({ where: { typeId: typeId } })
]).then(function(res) {
return res[0] != null || res[1] != null;
});
}
function getPadMarkers(padId, bbox) {
return _getPadObjects("Marker", padId, { where: _makeBboxCondition(bbox), include: [ MarkerData ] });
}
function getPadMarkersByType(padId, typeId) {
return _getPadObjects("Marker", padId, { where: { typeId: typeId }, include: [ MarkerData ] });
}
function createMarker(padId, data) {
return _createPadObjectWithData("Marker", padId, data);
}
function updateMarker(markerId, data) {
return _updatePadObjectWithData("Marker", markerId, data);
}
function deleteMarker(markerId) {
return _deletePadObjectWithData("Marker", markerId);
}
function getPadLines(padId, fields) {
var cond = { include: [ LineData ] };
if(fields)
cond.attributes = (typeof fields == "string" ? fields.split(/\s+/) : fields);
return _getPadObjects("Line", padId, cond);
}
function getPadLinesByType(padId, typeId) {
return _getPadObjects("Line", padId, { where: { typeId: typeId }, include: [ LineData ] });
}
function getLineTemplate(data) {
var line = JSON.parse(JSON.stringify(Line.build(data)));
line.data = data.data || { };
return Promise.resolve(line);
}
function getLine(lineId) {
return Line.findOne({ where: { id: lineId }, include: [ LineData ] });
}
function createLine(padId, data) {
return _createPadObjectWithData("Line", padId, data);
}
function updateLine(lineId, data) {
return _updatePadObjectWithData("Line", lineId, data);
}
function deleteLine(lineId) {
return _deletePadObjectWithData("Line", lineId);
}
function getLinePoints(lineId) {
return Line.build({ id: lineId }).getLinePoints();
}
function getLinePointsByBbox(lineId, bboxWithZoom) {
return Line.build({ id: lineId }).getLinePoints({
where: Sequelize.and(_makeBboxCondition(bboxWithZoom), bboxWithZoom ? { zoom: { lte: bboxWithZoom.zoom } } : null),
attributes: [ "idx" ],
order: "idx"
});
}
function getLinePointsByIdx(lineId, indexes) {
return Line.build({ id: lineId }).getLinePoints({
where: { idx: indexes },
attributes: [ "lon", "lat", "idx" ],
order: "idx"
});
}
function setLinePoints(lineId, trackPoints) {
return LinePoint.destroy({ where: { lineId: lineId } }).then(function() {
var create = [ ];
for(var i=0; i<trackPoints.length; i++) {
create.push(utils.extend({ }, trackPoints[i], { lineId: lineId }));
}
return LinePoint.bulkCreate(create);
});
}
function _makeBboxCondition(bbox, prefix) {
if(!bbox)
return { };
prefix = prefix || "";
function 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);
}
module.exports = {
connect : connect,
padIdExists : padIdExists,
getPadData : getPadData,
getPadDataByWriteId : getPadDataByWriteId,
createPad : createPad,
updatePadData : updatePadData,
getViews : getViews,
createView : createView,
updateView : updateView,
deleteView : deleteView,
getType : getType,
getTypes : getTypes,
createType : createType,
updateType : updateType,
deleteType : deleteType,
isTypeUsed : isTypeUsed,
getPadMarkers : getPadMarkers,
getPadMarkersByType : getPadMarkersByType,
createMarker : createMarker,
updateMarker : updateMarker,
deleteMarker : deleteMarker,
getPadLines : getPadLines,
getPadLinesByType : getPadLinesByType,
getLine : getLine,
getLineTemplate : getLineTemplate,
createLine : createLine,
updateLine : updateLine,
deleteLine : deleteLine,
getLinePoints : getLinePoints,
getLinePointsByBbox : getLinePointsByBbox,
getLinePointsByIdx : getLinePointsByIdx,
setLinePoints : setLinePoints,
_models : {
Pad: Pad,
Marker: Marker,
MarkerData: MarkerData,
Line: Line,
LineData: LineData,
LinePoint: LinePoint,
View: View,
Type: Type
},
_conn : conn
};

Wyświetl plik

@ -1,273 +0,0 @@
var readline = require("readline");
var mongoose = require("mongoose");
var db = require("./databaseBackendSequelize");
var db2 = require("./database");
var utils = require("./utils");
var async = require("async");
var OLD_MARKER_COLOURS = { blue: "8da8f6", green: "90ee90", gold: "ffd700", red: "ff0000" };
var ObjectId = mongoose.Schema.Types.ObjectId;
var positionType = {
lon: Number,
lat: Number
};
var bboxType = {
top: Number,
right: Number,
bottom: Number,
left: Number
};
var markerSchema = mongoose.Schema({
_pad : { type: String, ref: "Pad" },
position : positionType,
name : { type: String, default: "Untitled marker" },
description : String,
colour : { type: String, default: "ff0000" }
});
var lineSchema = mongoose.Schema({
_pad : { type: String, ref: "Pad" },
points : [positionType],
mode : { type: String, default: "" },
colour : { type: String, default: "0000ff" },
width : { type: Number, default: 4 },
description : String,
name : { type: String, default: "Untitled line" },
distance : Number,
time : Number
});
var linePointsSchema = mongoose.Schema(utils.extend({ }, positionType, {
zoom : Number,
idx : Number,
_line : { type: ObjectId, ref: "Line" }
}));
var viewSchema = mongoose.Schema({
_pad : { type: String, ref: "Pad" },
name : String,
baseLayer : String,
layers : [String],
view : bboxType
});
var padSchema = mongoose.Schema({
_id : String,
defaultView : { type: ObjectId, ref: "View" },
name: { type: String, default: "New FacilMap" },
writeId : String
});
var typeSchema = mongoose.Schema({
_pad : { type: String, ref: "Pad" },
name : String,
type : { type: String, enum: [ "marker", "line" ] },
fields : [ Object ]
});
var Marker = mongoose.model("Marker", markerSchema);
var Line = mongoose.model("Line", lineSchema);
var LinePoints = mongoose.model("LinePoints", linePointsSchema);
var View = mongoose.model("View", viewSchema);
var Pad = mongoose.model("Pad", padSchema);
var Type = mongoose.model("Type", typeSchema);
function migrateData(title, stream, deal, callback) {
console.log();
console.log();
console.log("Migrating "+title);
console.log();
var queue = async.queue(function(data, next) {
data = JSON.parse(JSON.stringify(data));
data.id = data._id;
delete data._id;
var queries = deal(data);
async.each(queries, function(it, next) {
it.complete(function(err) {
err && console.error(err);
next();
});
}, next);
}, 5);
var startStop = function() {
if(queue.length() == 0)
stream.resume();
else
stream.pause();
};
stream.on("data", function(data) {
queue.push(data, startStop);
startStop();
});
stream.on("end", function() {
if(queue.running() == 0 && queue.length() == 0)
callback();
else
queue.drain = callback;
});
stream.on("error", function(err) {
queue.kill();
callback(err);
});
}
var DEFAULT_MARKER_TYPE = db2._defaultTypes[0];
var DEFAULT_LINE_TYPE = db2._defaultTypes[1];
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log("Target SQL database will be cleared!");
rl.question("Please enter the MongoDB connection string [mongodb://localhost/facilpad]: ", function(connectionString) {
rl.close();
mongoose.connect(connectionString || "mongodb://localhost/facilpad");
var markerTypes = { };
var lineTypes = { };
var markers = { };
var lines = { };
var views = { };
async.series([
function(next) {
db.connect(next, true);
},
function(next) {
migrateData("Pads", Pad.find().stream(), function(data) {
delete data.defaultView;
var ret = [ db._models.Pad.create(data) ];
markerTypes[data.id] = db._models.Type.build(DEFAULT_MARKER_TYPE);
markerTypes[data.id].padId = data.id;
ret.push(markerTypes[data.id].save());
lineTypes[data.id] = db._models.Type.build(DEFAULT_LINE_TYPE);
lineTypes[data.id].padId = data.id;
ret.push(lineTypes[data.id].save());
return ret;
}, next);
},
function(next) {
migrateData("Markers", Marker.find().stream(), function(data) {
data.lat = (data.position && data.position.lat);
data.lon = (data.position && data.position.lon);
delete data.position;
data.padId = data._pad;
delete data._pad;
if(data.style) {
data.colour = OLD_MARKER_COLOURS[data.style];
delete data.style;
}
data.typeId = (markerTypes[data.padId] && markerTypes[data.padId].id);
var id = data.id;
delete data.id;
markers[id] = db._models.Marker.build(data);
return [ markers[id].save() ];
}, next);
},
function(next) {
migrateData("MarkerData", Marker.find().stream(), function(data) {
if(!markers[data.id].id)
return [ ]; // Saving of marker failed
var markerData = db._models.MarkerData.build({ name: "Description", value: data.description || "" });
markerData.markerId = markers[data.id].id;
return [ markerData.save() ];
}, next);
},
function(next) {
migrateData("Line", Line.find().stream(), function(data) {
data.padId = data._pad;
delete data._pad;
for(var i=0; i<data.points.length; i++) {
delete data.points[i]._id;
}
data.typeId = (lineTypes[data.padId] && lineTypes[data.padId].id);
var id = data.id;
delete data.id;
lines[id] = db._models.Line.build(data);
return [ lines[id].save() ];
}, next);
},
function(next) {
migrateData("LineData", Line.find().stream(), function(data) {
if(!lines[data.id].id)
return [ ]; // Saving of line failed
var lineData = db._models.LineData.build({ name: "Description", value: data.description || "" });
lineData.lineId = lines[data.id].id;
return [ lineData.save() ];
}, next);
},
function(next) {
migrateData("LinePoints", LinePoints.find().stream(), function(data) {
delete data.id;
var linePoint = db._models.LinePoint.build(data);
linePoint.lineId = lines[data._line].id;
return [ linePoint.save() ];
}, next);
},
function(next) {
migrateData("Views", View.find().stream(), function(data) {
var id = data.id;
delete data.id;
data.top = data.view.top;
data.left = data.view.left;
data.bottom = data.view.bottom;
data.right = data.view.right;
delete data.view;
data.padId = data._pad;
delete data._pad;
views[id] = db._models.View.build(data);
return [ views[id].save() ];
}, next);
},
function(next) {
migrateData("defaultViews", Pad.find().stream(), function(data) {
if(!data.defaultView)
return [ ];
return [ db._models.Pad.update({ defaultViewId: views[data.defaultView] && views[data.defaultView].id }, { where: { id: data.id } }) ];
}, next);
}
], function(err) {
if(err)
console.error(err);
mongoose.disconnect();
})
});

Wyświetl plik

@ -1,6 +1,4 @@
var database = require("./database");
var utils = require("./utils"); var utils = require("./utils");
var Promise = require("promise");
var _e = utils.escapeXml; var _e = utils.escapeXml;
@ -45,28 +43,36 @@ function _textToData(fields, text) {
return ret; return ret;
} }
function exportGpx(padId, useTracks) { function exportGpx(database, padId, useTracks) {
var padDataP = database.getPadData(padId); return utils.promiseAuto({
padData: database.getPadData(padId),
var views = '', markers = '', lines = '', types = ''; views: () => {
var views = '';
return utils.streamEachPromise(database.getViews(padId), (view) => {
views += '<fm:view name="' + _e(view.name) + '" baselayer="' + _e(view.baseLayer) + '" layers="' + _e(JSON.stringify(view.layers)) + '" bbox="' + _e([ view.left, view.top, view.right, view.bottom].join(',')) + '" />\n';
}).then(() => views);
},
var viewsP = utils.streamEachPromise(database.getViews(padId), function(view) { typesObj: () => {
views += '<fm:view name="' + _e(view.name) + '" baselayer="' + _e(view.baseLayer) + '" layers="' + _e(JSON.stringify(view.layers)) + '" bbox="' + _e([ view.left, view.top, view.right, view.bottom].join(',')) + '" />\n'; var typesObj = { };
}); return utils.streamEachPromise(database.getTypes(padId), function(type) {
typesObj[type.id] = type;
}).then(() => typesObj);
},
var typesObj = { }; types: (typesObj) => {
var typesObjP = utils.streamEachPromise(database.getTypes(padId), function(type) { var types = '';
typesObj[type.id] = type; for(var i in typesObj) {
}); var type = typesObj[i];
types += '<fm:type name="' + _e(type.name) + '" type="' + _e(type.type) + '" fields="' + _e(JSON.stringify(type.fields)) + '" />\n';
}
return types;
},
var typesMarkersLinesP = typesObjP.then(function() { markers: (typesObj) => {
for(var i in typesObj) { var markers = '';
var type = typesObj[i]; return utils.streamEachPromise(database.getPadMarkers(padId), function(marker) {
types += '<fm:type name="' + _e(type.name) + '" type="' + _e(type.type) + '" fields="' + _e(JSON.stringify(type.fields)) + '" />\n';
}
return Promise.all([
utils.streamEachPromise(database.getPadMarkers(padId), function(marker) {
markers += '<wpt lat="' + _e(marker.lat) + '" lon="' + _e(marker.lon) + '">\n' + markers += '<wpt lat="' + _e(marker.lat) + '" lon="' + _e(marker.lon) + '">\n' +
'\t<name>' + _e(marker.name) + '</name>\n' + '\t<name>' + _e(marker.name) + '</name>\n' +
'\t<desc>' + _e(_dataToText(typesObj[marker.typeId].fields, marker.data)) + '</desc>\n' + '\t<desc>' + _e(_dataToText(typesObj[marker.typeId].fields, marker.data)) + '</desc>\n' +
@ -74,8 +80,12 @@ function exportGpx(padId, useTracks) {
'\t\t<fm:colour>' + _e(marker.colour) + '</fm:colour>\n' + '\t\t<fm:colour>' + _e(marker.colour) + '</fm:colour>\n' +
'\t</extensions>\n' + '\t</extensions>\n' +
'</wpt>\n'; '</wpt>\n';
}), }).then(() => markers);
utils.streamEachPromise(database.getPadLinesWithPoints(padId), function(line) { },
lines: (typesObj) => {
var lines = '';
return utils.streamEachPromise(database.getPadLinesWithPoints(padId), function(line) {
var t = (useTracks || line.mode == "track"); var t = (useTracks || line.mode == "track");
lines += '<' + (t ? 'trk' : 'rte') + '>\n' + lines += '<' + (t ? 'trk' : 'rte') + '>\n' +
@ -101,25 +111,22 @@ function exportGpx(padId, useTracks) {
} }
lines += '</' + (t ? 'trk' : 'rte') + '>\n'; lines += '</' + (t ? 'trk' : 'rte') + '>\n';
}) }).then(() => lines);
]); }
}); }).then((res) => '<?xml version="1.0" encoding="UTF-8"?>\n' +
return Promise.all([ padDataP, viewsP, typesMarkersLinesP ]).then(function(res) {
return '<?xml version="1.0" encoding="UTF-8"?>\n' +
'<gpx xmlns="http://www.topografix.com/GPX/1/1" creator="FacilMap" version="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns:fm="https://facilmap.org/">\n' + '<gpx xmlns="http://www.topografix.com/GPX/1/1" creator="FacilMap" version="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns:fm="https://facilmap.org/">\n' +
'\t<metadata>\n' + '\t<metadata>\n' +
'\t\t<name>' + _e(res[0].name) + '</name>\n' + '\t\t<name>' + _e(res.padData.name) + '</name>\n' +
'\t\t<time>' + _e(utils.isoDate()) + '</time>\n' + '\t\t<time>' + _e(utils.isoDate()) + '</time>\n' +
'\t</metadata>\n' + '\t</metadata>\n' +
'\t<extensions>\n' + '\t<extensions>\n' +
views.replace(/^(.)/gm, '\t\t$1') + res.views.replace(/^(.)/gm, '\t\t$1') +
types.replace(/^(.)/gm, '\t\t$1') + res.types.replace(/^(.)/gm, '\t\t$1') +
'\t</extensions>\n' + '\t</extensions>\n' +
markers.replace(/^(.)/gm, '\t$1') + res.markers.replace(/^(.)/gm, '\t$1') +
lines.replace(/^(.)/gm, '\t$1') + res.lines.replace(/^(.)/gm, '\t$1') +
'</gpx>'; '</gpx>'
}); );
} }
module.exports = { module.exports = {

Wyświetl plik

@ -1,7 +1,7 @@
var request = require("request-promise"); var request = require("request-promise");
var utils = require("./utils"); var utils = require("./utils");
var config = require("../config"); var config = require("../config");
var Promise = require("promise");
var ROUTING_URL = "https://api.mapbox.com/directions/v5/mapbox"; var ROUTING_URL = "https://api.mapbox.com/directions/v5/mapbox";

Wyświetl plik

@ -1,9 +1,10 @@
var request = require("request-promise"); var request = require("request-promise");
var config = require("../config");
var Promise = require("promise"); var Promise = require("promise");
var cheerio = require("cheerio"); var cheerio = require("cheerio");
var zlib = require("zlib"); var zlib = require("zlib");
var compressjs = require("compressjs"); var compressjs = require("compressjs");
var config = require("../config");
var utils = require("./utils"); var utils = require("./utils");
request = request.defaults({ request = request.defaults({

Wyświetl plik

@ -1,17 +1,14 @@
var http = require("http"); var http = require("http");
var socketIo = require("socket.io"); var compression = require("compression");
var config = require("../config");
var listeners = require("./listeners");
var database = require("./database");
var domain = require("domain"); 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 Promise = require("promise");
var express = require("express"); var express = require("express");
var path = require("path"); 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"); var frontendPath = path.resolve(__dirname + "/../frontend");
@ -28,370 +25,33 @@ Object.defineProperty(Error.prototype, "toJSON", {
configurable: true configurable: true
}); });
var dbP = database.connect(); process.on('unhandledRejection', (reason, promise) => {
console.trace("Unhandled rejection", reason);
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 server = http.createServer(app); utils.promiseAuto({
database: () => new Database(),
var serverP = Promise.denodeify(server.listen.bind(server))(config.port, config.host).then(function() { databaseConnect: database => database.connect(),
var io = socketIo.listen(server);
io.sockets.on("connection", function(socket) { server: () => {
var d = domain.create(); var app = express();
d.add(socket); app.use(compression());
app.use(express.static(frontendPath + "/build/"));
d.on("error", function(err) { app.get("/:padId", function(req, res) {
console.error("Uncaught error in socket:", err.stack); res.sendFile(frontendPath + "/build/index.html");
socket.disconnect();
}); });
var handlers = { var server = http.createServer(app);
error : function(err) { return Promise.denodeify(server.listen.bind(server))(config.port, config.host).then(() => server);
console.error("Error! Disconnecting client."); },
console.error(err.stack);
socket.disconnect();
},
setPadId : function(padId) { socket: (server, database) => {
return Promise.resolve().then(function() { return new Socket(server, database);
if(typeof padId != "string") }
throw "Invalid pad id"; }).then(res => {
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() {
console.log("Server started on " + (config.host || "*" ) + ":" + config.port); console.log("Server started on " + (config.host || "*" ) + ":" + config.port);
}).catch(function(err) { }).catch(err => {
console.error(err); console.error(err);
process.exit(1); 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);
}

526
server/socket.js 100644
Wyświetl plik

@ -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;

Wyświetl plik

@ -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 = { module.exports = {
isInBbox : isInBbox, isInBbox : isInBbox,
filterStreamPromise : filterStreamPromise, filterStreamPromise : filterStreamPromise,
@ -248,5 +301,7 @@ module.exports = {
isoDate : isoDate, isoDate : isoDate,
round: round, round: round,
streamToArrayPromise: streamToArrayPromise, streamToArrayPromise: streamToArrayPromise,
promiseAllObject: promiseAllObject promiseAllObject: promiseAllObject,
promiseAuto: promiseAuto,
modifyFunction: modifyFunction
}; };