kopia lustrzana https://github.com/FacilMap/facilmap
301 wiersze
9.9 KiB
TypeScript
301 wiersze
9.9 KiB
TypeScript
import { DataTypes, HasManyGetAssociationsMixin, Model, Op } from "sequelize";
|
|
import { BboxWithZoom, ID, Latitude, Line, LineCreate, ExtraInfo, LineUpdate, Longitude, PadId, Point, Route, TrackPoint } from "../../../types/src";
|
|
import Database from "./database";
|
|
import { BboxWithExcept, dataDefinition, DataModel, getLatType, getLonType, makeBboxCondition, makeNotNullForeignKey, validateColour } from "./helpers";
|
|
import { isEqual } from "lodash";
|
|
import { wrapAsync } from "../utils/streams";
|
|
import { calculateRouteForLine } from "../routing/routing";
|
|
|
|
export type LineWithTrackPoints = Line & {
|
|
trackPoints: TrackPoint[];
|
|
}
|
|
|
|
function createLineModel() {
|
|
return class LineModel extends Model {
|
|
id!: ID;
|
|
padId!: PadId;
|
|
routePoints!: string;
|
|
mode!: string;
|
|
colour!: string;
|
|
width!: number;
|
|
name!: string | null;
|
|
distance!: number | null;
|
|
time!: number | null;
|
|
ascent!: number | null;
|
|
descent!: number | null;
|
|
top!: Latitude;
|
|
bottom!: Latitude;
|
|
left!: Longitude;
|
|
right!: Longitude;
|
|
extraInfo!: string | null;
|
|
|
|
getLinePoints!: HasManyGetAssociationsMixin<LinePointModel>;
|
|
toJSON!: () => Line;
|
|
}
|
|
}
|
|
|
|
function createLinePointModel() {
|
|
return class LinePointModel extends Model {
|
|
id!: ID;
|
|
lat!: Latitude;
|
|
lon!: Longitude;
|
|
zoom!: number;
|
|
idx!: number;
|
|
ele!: number | null;
|
|
toJSON!: () => TrackPoint;
|
|
};
|
|
}
|
|
|
|
function createLineDataModel() {
|
|
return class LineData extends DataModel {};
|
|
}
|
|
|
|
export type LineModel = InstanceType<ReturnType<typeof createLineModel>>;
|
|
export type LinePointModel = InstanceType<ReturnType<typeof createLinePointModel>>;
|
|
|
|
export default class DatabaseLines {
|
|
|
|
LineModel = createLineModel();
|
|
LinePointModel = createLinePointModel();
|
|
LineDataModel = createLineDataModel();
|
|
|
|
_db: Database;
|
|
|
|
constructor(database: Database) {
|
|
this._db = database;
|
|
|
|
this.LineModel.init({
|
|
routePoints : {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false,
|
|
get: function(this: LineModel) {
|
|
const routePoints = this.getDataValue("routePoints");
|
|
return routePoints != null ? JSON.parse(routePoints) : routePoints;
|
|
},
|
|
set: function(this: LineModel, v: Point[]) {
|
|
for(let i=0; i<v.length; i++) {
|
|
v[i].lat = Number(v[i].lat.toFixed(6));
|
|
v[i].lon = Number(v[i].lon.toFixed(6));
|
|
}
|
|
this.setDataValue("routePoints", JSON.stringify(v));
|
|
},
|
|
validate: {
|
|
minTwo: function(val: string) {
|
|
const 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: DataTypes.TEXT, allowNull: false, defaultValue: "" },
|
|
colour : { type: DataTypes.STRING(6), allowNull: false, defaultValue: "0000ff", validate: validateColour },
|
|
width : { type: DataTypes.INTEGER.UNSIGNED, allowNull: false, defaultValue: 4, validate: { min: 1 } },
|
|
name : { type: DataTypes.TEXT, allowNull: true, get: function(this: LineModel) { return this.getDataValue("name") || "Untitled line"; } },
|
|
distance : { type: DataTypes.FLOAT(24, 2).UNSIGNED, allowNull: true },
|
|
time : { type: DataTypes.INTEGER.UNSIGNED, allowNull: true },
|
|
ascent : { type: DataTypes.INTEGER.UNSIGNED, allowNull: true },
|
|
descent : { type: DataTypes.INTEGER.UNSIGNED, allowNull: true },
|
|
top: getLatType(),
|
|
bottom: getLatType(),
|
|
left: getLonType(),
|
|
right: getLonType(),
|
|
extraInfo: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: true,
|
|
get: function(this: LineModel) {
|
|
const extraInfo = this.getDataValue("extraInfo");
|
|
return extraInfo != null ? JSON.parse(extraInfo) : extraInfo;
|
|
},
|
|
set: function(this: LineModel, v: ExtraInfo) {
|
|
this.setDataValue("extraInfo", JSON.stringify(v));
|
|
}
|
|
}
|
|
}, {
|
|
sequelize: this._db._conn,
|
|
modelName: "Line"
|
|
});
|
|
|
|
this.LinePointModel.init({
|
|
lat: getLatType(),
|
|
lon: getLonType(),
|
|
zoom: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false, validate: { min: 1, max: 20 } },
|
|
idx: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false },
|
|
ele: { type: DataTypes.INTEGER, allowNull: true }
|
|
}, {
|
|
sequelize: this._db._conn,
|
|
indexes: [
|
|
{ fields: [ "lineId", "zoom" ] }
|
|
],
|
|
modelName: "LinePoint"
|
|
});
|
|
|
|
this.LineDataModel.init(dataDefinition, {
|
|
sequelize: this._db._conn,
|
|
modelName: "LineData"
|
|
});
|
|
}
|
|
|
|
afterInit(): void {
|
|
this.LineModel.belongsTo(this._db.pads.PadModel, makeNotNullForeignKey("pad", "padId"));
|
|
this._db.pads.PadModel.hasMany(this.LineModel, { foreignKey: "padId" });
|
|
|
|
// TODO: Cascade
|
|
this.LineModel.belongsTo(this._db.types.TypeModel, makeNotNullForeignKey("type", "typeId", true));
|
|
|
|
this.LinePointModel.belongsTo(this.LineModel, makeNotNullForeignKey("line", "lineId"));
|
|
this.LineModel.hasMany(this.LinePointModel, { foreignKey: "lineId" });
|
|
|
|
this.LineDataModel.belongsTo(this.LineModel, makeNotNullForeignKey("line", "lineId"));
|
|
this.LineModel.hasMany(this.LineDataModel, { foreignKey: "lineId" });
|
|
}
|
|
|
|
getPadLines(padId: PadId, fields?: Array<keyof Line>): Highland.Stream<Line> {
|
|
const cond = fields ? { attributes: fields } : { };
|
|
return this._db.helpers._getPadObjects<Line>("Line", padId, cond);
|
|
}
|
|
|
|
getPadLinesByType(padId: PadId, typeId: ID): Highland.Stream<Line> {
|
|
return this._db.helpers._getPadObjects<Line>("Line", padId, { where: { typeId: typeId } });
|
|
}
|
|
|
|
getPadLinesWithPoints(padId: PadId): Highland.Stream<LineWithTrackPoints> {
|
|
return this.getPadLines(padId)
|
|
.flatMap(wrapAsync(async (line): Promise<LineWithTrackPoints> => {
|
|
const trackPoints = await this.getAllLinePoints(line.id);
|
|
return { ...line, trackPoints };
|
|
}));
|
|
}
|
|
|
|
async getLineTemplate(padId: PadId, data: { typeId: ID }): Promise<Line> {
|
|
const lineTemplate = {
|
|
...this.LineModel.build({ ...data, padId: padId }).toJSON(),
|
|
data: { }
|
|
} as Line;
|
|
|
|
const type = await this._db.types.getType(padId, data.typeId);
|
|
|
|
if(type.defaultColour)
|
|
lineTemplate.colour = type.defaultColour;
|
|
if(type.defaultWidth)
|
|
lineTemplate.width = type.defaultWidth;
|
|
if(type.defaultMode)
|
|
lineTemplate.mode = type.defaultMode;
|
|
|
|
await this._db.helpers._updateObjectStyles(lineTemplate);
|
|
|
|
return lineTemplate;
|
|
}
|
|
|
|
getLine(padId: PadId, lineId: ID): Promise<Line> {
|
|
return this._db.helpers._getPadObject<Line>("Line", padId, lineId);
|
|
}
|
|
|
|
async createLine(padId: PadId, data: LineCreate, trackPointsFromRoute?: Route): Promise<Line> {
|
|
const type = await this._db.types.getType(padId, data.typeId);
|
|
|
|
if(type.defaultColour && !data.colour)
|
|
data.colour = type.defaultColour;
|
|
if(type.defaultWidth && !data.width)
|
|
data.width = type.defaultWidth;
|
|
if(type.defaultMode && !data.mode)
|
|
data.mode = type.defaultMode;
|
|
|
|
const { trackPoints, ...routeInfo } = await calculateRouteForLine(data, trackPointsFromRoute);
|
|
|
|
const dataCopy = { ...data, ...routeInfo };
|
|
delete dataCopy.trackPoints; // They came if mode is track
|
|
|
|
const createdLine = await this._db.helpers._createPadObject<Line>("Line", padId, dataCopy);
|
|
await this._db.helpers._updateObjectStyles(createdLine);
|
|
|
|
// We have to emit this before calling _setLinePoints so that this event is sent to the client first
|
|
this._db.emit("line", padId, createdLine);
|
|
|
|
await this._setLinePoints(padId, createdLine.id, trackPoints);
|
|
|
|
return createdLine;
|
|
}
|
|
|
|
async updateLine(padId: PadId, lineId: ID, data: LineUpdate, doNotUpdateStyles?: boolean, trackPointsFromRoute?: Route): Promise<Line> {
|
|
const originalLine = await this.getLine(padId, lineId);
|
|
const update = {
|
|
...data,
|
|
routePoints: data.routePoints || originalLine.routePoints,
|
|
mode: data.mode || originalLine.mode || ""
|
|
};
|
|
|
|
let routeInfo;
|
|
if((update.mode == "track" && update.trackPoints) || !isEqual(update.routePoints, originalLine.routePoints) || update.mode != originalLine.mode)
|
|
routeInfo = await calculateRouteForLine(update, trackPointsFromRoute);
|
|
|
|
Object.assign(update, routeInfo);
|
|
delete update.trackPoints; // They came if mode is track
|
|
|
|
const newLine = await this._db.helpers._updatePadObject<Line>("Line", padId, lineId, update, doNotUpdateStyles);
|
|
|
|
if(!doNotUpdateStyles)
|
|
await this._db.helpers._updateObjectStyles(newLine); // Modifies newLine
|
|
|
|
this._db.emit("line", padId, newLine);
|
|
|
|
if(routeInfo)
|
|
await this._setLinePoints(padId, lineId, routeInfo.trackPoints);
|
|
|
|
return newLine;
|
|
}
|
|
|
|
async _setLinePoints(padId: PadId, lineId: ID, trackPoints: Point[], _noEvent?: boolean): Promise<void> {
|
|
// First get elevation, so that if that fails, we don't update anything
|
|
await this.LinePointModel.destroy({ where: { lineId: lineId } });
|
|
|
|
const create = [ ];
|
|
for(let i=0; i<trackPoints.length; i++) {
|
|
create.push(Object.assign(JSON.parse(JSON.stringify(trackPoints[i])), { lineId: lineId }));
|
|
}
|
|
|
|
const points = await this._db.helpers._bulkCreateInBatches<TrackPoint>(this.LinePointModel, create);
|
|
|
|
if(!_noEvent)
|
|
this._db.emit("linePoints", padId, lineId, points);
|
|
}
|
|
|
|
async deleteLine(padId: PadId, lineId: ID): Promise<Line> {
|
|
await this._setLinePoints(padId, lineId, [ ], true);
|
|
const oldLine = await this._db.helpers._deletePadObject<Line>("Line", padId, lineId);
|
|
this._db.emit("deleteLine", padId, { id: lineId });
|
|
return oldLine;
|
|
}
|
|
|
|
getLinePointsForPad(padId: PadId, bboxWithZoom: BboxWithZoom & BboxWithExcept): Highland.Stream<{ id: ID; trackPoints: TrackPoint[] }> {
|
|
return this._db.helpers._toStream(async () => {
|
|
const results = await this.LineModel.findAll({
|
|
attributes: ["id"],
|
|
where: {
|
|
[Op.and]: [
|
|
{
|
|
padId,
|
|
"$LinePoints.zoom$": { [Op.lte]: bboxWithZoom.zoom }
|
|
},
|
|
makeBboxCondition(bboxWithZoom, "$LinePoints.", "$")
|
|
]
|
|
},
|
|
include: this.LinePointModel
|
|
});
|
|
|
|
return results.map((res) => {
|
|
const val = res.toJSON() as any;
|
|
return { id: val.id, trackPoints: val.LinePoints };
|
|
});
|
|
});
|
|
}
|
|
|
|
async getAllLinePoints(lineId: ID): Promise<TrackPoint[]> {
|
|
const points = await this.LineModel.build({ id: lineId }).getLinePoints({
|
|
attributes: [ "lat", "lon", "ele", "zoom", "idx" ]
|
|
});
|
|
return points.map((point) => point.toJSON() as TrackPoint);
|
|
}
|
|
|
|
} |