2017-04-28 00:17:30 +00:00
|
|
|
var highland = require("highland");
|
2017-03-04 19:35:26 +00:00
|
|
|
var Promise = require("bluebird");
|
2016-10-30 14:01:21 +00:00
|
|
|
var Sequelize = require("sequelize");
|
|
|
|
var underscore = require("underscore");
|
|
|
|
|
2017-04-28 00:17:30 +00:00
|
|
|
var elevation = require("../elevation");
|
2016-10-30 14:01:21 +00:00
|
|
|
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 },
|
2017-04-28 00:17:30 +00:00
|
|
|
time : { type: Sequelize.INTEGER.UNSIGNED, allowNull: true },
|
|
|
|
ascent : { type: Sequelize.INTEGER.UNSIGNED, allowNull: true },
|
|
|
|
descent : { type: Sequelize.INTEGER.UNSIGNED, allowNull: true }
|
2016-10-30 14:01:21 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this._conn.define("LinePoint", {
|
|
|
|
lat: this._TYPES.lat,
|
|
|
|
lon: this._TYPES.lon,
|
|
|
|
zoom: { type: Sequelize.INTEGER.UNSIGNED, allowNull: false, validate: { min: 1, max: 20 } },
|
2017-04-28 00:17:30 +00:00
|
|
|
idx: { type: Sequelize.INTEGER.UNSIGNED, allowNull: false },
|
2017-04-28 01:39:44 +00:00
|
|
|
ele: { type: Sequelize.INTEGER, allowNull: true }
|
2016-10-30 14:01:21 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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) {
|
2016-10-31 12:08:22 +00:00
|
|
|
var cond = fields ? { attributes: (typeof fields == "string" ? fields.split(/\s+/) : fields) } : { };
|
2016-10-30 14:01:21 +00:00
|
|
|
return this._getPadObjects("Line", padId, cond);
|
|
|
|
},
|
|
|
|
|
|
|
|
getPadLinesByType(padId, typeId) {
|
2016-10-31 12:08:22 +00:00
|
|
|
return this._getPadObjects("Line", padId, { where: { typeId: typeId } });
|
2016-10-30 14:01:21 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
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: () => {
|
2016-10-30 15:05:23 +00:00
|
|
|
var ret = JSON.parse(JSON.stringify(this._conn.model("Line").build(utils.extend({ }, data, { padId: padId }))));
|
|
|
|
ret.data = data.data || { };
|
|
|
|
return ret;
|
2016-10-30 14:01:21 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
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) {
|
2016-10-31 12:08:22 +00:00
|
|
|
return this._getPadObject("Line", padId, lineId);
|
2016-10-30 14:01:21 +00:00
|
|
|
},
|
|
|
|
|
2017-05-25 20:58:29 +00:00
|
|
|
createLine(padId, data, trackPointsFromRoute) {
|
2016-10-30 14:01:21 +00:00
|
|
|
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) => {
|
2017-05-25 20:58:29 +00:00
|
|
|
return this._calculateRouting(data, trackPointsFromRoute);
|
2016-10-30 14:01:21 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
createLine: (routing, defaultVals) => {
|
|
|
|
var dataCopy = utils.extend({ }, data);
|
|
|
|
delete dataCopy.trackPoints; // They came if mode is track
|
|
|
|
|
2016-10-31 12:08:22 +00:00
|
|
|
return this._createPadObject("Line", padId, dataCopy);
|
2016-10-30 14:01:21 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2017-05-17 17:33:07 +00:00
|
|
|
}).then((res) => {
|
|
|
|
this.emit("line", padId, res.setLinePoints); // res.linePoints returns the line with updated ascent and descent
|
|
|
|
return res.setLinePoints;
|
|
|
|
});
|
2016-10-30 14:01:21 +00:00
|
|
|
},
|
|
|
|
|
2017-05-25 20:58:29 +00:00
|
|
|
updateLine(padId, lineId, data, doNotUpdateStyles, trackPointsFromRoute) {
|
2016-10-30 14:01:21 +00:00
|
|
|
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)
|
2017-05-25 20:58:29 +00:00
|
|
|
return this._calculateRouting(data, trackPointsFromRoute); // Also sets data.distance and data.time
|
2016-10-30 14:01:21 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
newLine: (routing) => {
|
|
|
|
var dataCopy = utils.extend({ }, data);
|
|
|
|
delete dataCopy.trackPoints; // They came if mode is track
|
|
|
|
|
2016-10-31 12:11:36 +00:00
|
|
|
return this._updatePadObject("Line", padId, lineId, dataCopy, doNotUpdateStyles);
|
2016-10-30 14:01:21 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
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) => {
|
2017-05-23 22:41:26 +00:00
|
|
|
this.emit("line", padId, res.linePoints || res.newLine); // res.linePoints returns the line with updated ascent and descent
|
2016-10-30 14:01:21 +00:00
|
|
|
|
2017-05-11 00:54:23 +00:00
|
|
|
return res.linePoints;
|
2016-10-30 14:01:21 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-05-11 00:53:56 +00:00
|
|
|
_setLinePoints(padId, lineId, trackPoints, _noEvent, _noUpdateLine) {
|
2017-04-28 00:17:30 +00:00
|
|
|
// First get elevation, so that if that fails, we don't update anything
|
|
|
|
let ascentDescent;
|
|
|
|
return this._updateElevation(trackPoints).then((a) => {
|
|
|
|
ascentDescent = a;
|
|
|
|
|
|
|
|
return this._conn.model("LinePoint").destroy({ where: { lineId: lineId } });
|
|
|
|
}).then(() => {
|
2016-10-30 14:01:21 +00:00
|
|
|
var create = [ ];
|
|
|
|
for(var i=0; i<trackPoints.length; i++) {
|
2017-04-28 00:17:30 +00:00
|
|
|
create.push(Object.assign(JSON.parse(JSON.stringify(trackPoints[i])), { lineId: lineId }));
|
2016-10-30 14:01:21 +00:00
|
|
|
}
|
|
|
|
|
2017-02-10 15:49:25 +00:00
|
|
|
return this._bulkCreateInBatches(this._conn.model("LinePoint"), create);
|
2016-10-30 14:01:21 +00:00
|
|
|
}).then((points) => {
|
|
|
|
if(!_noEvent)
|
|
|
|
this.emit("linePoints", padId, lineId, points);
|
2017-04-28 00:17:30 +00:00
|
|
|
|
2017-05-11 00:53:56 +00:00
|
|
|
if(!_noUpdateLine) // When deleting the line, we don't want to set this
|
|
|
|
return this._updatePadObject("Line", padId, lineId, ascentDescent, true);
|
2016-10-30 14:01:21 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
deleteLine(padId, lineId) {
|
|
|
|
return utils.promiseAuto({
|
2016-10-31 12:08:22 +00:00
|
|
|
line: this._deletePadObject("Line", padId, lineId),
|
2017-05-11 00:53:56 +00:00
|
|
|
points: this._setLinePoints(padId, lineId, [ ], true, true)
|
2016-10-30 14:01:21 +00:00
|
|
|
}).then((res) => {
|
|
|
|
this.emit("deleteLine", padId, { id: lineId });
|
|
|
|
|
2016-10-30 15:05:23 +00:00
|
|
|
return res.line;
|
2016-10-30 14:01:21 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
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 },
|
2017-04-28 00:17:30 +00:00
|
|
|
attributes: [ "lon", "lat", "idx", "ele" ],
|
2016-10-30 14:01:21 +00:00
|
|
|
order: "idx"
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-05-25 20:58:29 +00:00
|
|
|
getAllLinePoints(lineId) {
|
|
|
|
return this._conn.model("Line").build({ id: lineId }).getLinePoints({
|
|
|
|
attributes: [ "lat", "lon", "ele", "zoom", "idx" ]
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
_calculateRouting(line, trackPointsFromRoute) {
|
|
|
|
if(trackPointsFromRoute) {
|
|
|
|
line.distance = trackPointsFromRoute.distance;
|
|
|
|
line.time = trackPointsFromRoute.time;
|
|
|
|
line.ascent = trackPointsFromRoute.ascent;
|
|
|
|
line.descent = trackPointsFromRoute.descent;
|
|
|
|
|
|
|
|
return Promise.resolve(trackPointsFromRoute.trackPoints);
|
|
|
|
} else if(line.mode == "track" && line.trackPoints && line.trackPoints.length >= 2) {
|
2016-10-30 14:01:21 +00:00
|
|
|
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);
|
|
|
|
}
|
2017-04-28 00:17:30 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_updateElevation(trackPoints) {
|
|
|
|
let pointsToUpdate = trackPoints.filter((point) => (point.zoom < 12 && point.ele == null));
|
|
|
|
|
|
|
|
return elevation.getElevationForPoints(pointsToUpdate).then((elevations) => {
|
|
|
|
elevations.forEach((elevation, i) => {
|
|
|
|
if(pointsToUpdate[i].setDataValue)
|
|
|
|
pointsToUpdate[i].setDataValue("ele", elevation);
|
|
|
|
else
|
|
|
|
pointsToUpdate[i].ele = elevation;
|
|
|
|
});
|
|
|
|
|
|
|
|
return elevation.getAscentDescent(trackPoints.filter((point) => (point.ele != null)).map((point) => (point.ele)));
|
|
|
|
});
|
2016-10-30 14:01:21 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|