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; 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>; export type LinePointModel = InstanceType>; 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): Highland.Stream { const cond = fields ? { attributes: fields } : { }; return this._db.helpers._getPadObjects("Line", padId, cond); } getPadLinesByType(padId: PadId, typeId: ID): Highland.Stream { return this._db.helpers._getPadObjects("Line", padId, { where: { typeId: typeId } }); } getPadLinesWithPoints(padId: PadId): Highland.Stream { return this.getPadLines(padId) .flatMap(wrapAsync(async (line): Promise => { const trackPoints = await this.getAllLinePoints(line.id); return { ...line, trackPoints }; })); } async getLineTemplate(padId: PadId, data: { typeId: ID }): Promise { 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 { return this._db.helpers._getPadObject("Line", padId, lineId); } async createLine(padId: PadId, data: LineCreate, trackPointsFromRoute?: Route): Promise { 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", 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 { 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", 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 { // 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(this.LinePointModel, create); if(!_noEvent) this._db.emit("linePoints", padId, lineId, points); } async deleteLine(padId: PadId, lineId: ID): Promise { await this._setLinePoints(padId, lineId, [ ], true); const oldLine = await this._db.helpers._deletePadObject("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 { const points = await this.LineModel.build({ id: lineId }).getLinePoints({ attributes: [ "lat", "lon", "ele", "zoom", "idx" ] }); return points.map((point) => point.toJSON() as TrackPoint); } }