import { CreationAttributes, CreationOptional, DataTypes, ForeignKey, HasManyGetAssociationsMixin, InferAttributes, InferCreationAttributes, Model, Op } from "sequelize"; import { BboxWithZoom, ID, Latitude, Line, LineCreate, ExtraInfo, LineUpdate, Longitude, PadId, Point, Route, TrackPoint } from "facilmap-types"; import Database from "./database"; import { BboxWithExcept, createModel, dataDefinition, DataModel, getDefaultIdType, getLatType, getLonType, getPosType, getVirtualLatType, getVirtualLonType, makeBboxCondition, makeNotNullForeignKey, validateColour } from "./helpers"; import { groupBy, isEqual, mapValues, omit } from "lodash"; import { wrapAsync } from "../utils/streams"; import { calculateRouteForLine } from "../routing/routing"; import { PadModel } from "./pad"; import { Point as GeoJsonPoint } from "geojson"; import { TypeModel } from "./type"; export type LineWithTrackPoints = Line & { trackPoints: TrackPoint[]; } export interface LineModel extends Model, InferCreationAttributes> { id: CreationOptional; padId: ForeignKey; routePoints: string; typeId: ForeignKey; mode: CreationOptional; colour: CreationOptional; width: CreationOptional; name: CreationOptional; distance: CreationOptional; time: CreationOptional; ascent: CreationOptional; descent: CreationOptional; top: Latitude; bottom: Latitude; left: Longitude; right: Longitude; extraInfo: CreationOptional; getLinePoints: HasManyGetAssociationsMixin; toJSON: () => Line; } export interface LinePointModel extends Model, InferCreationAttributes> { id: CreationOptional; lineId: ForeignKey; pos: GeoJsonPoint; lat: Latitude; lon: Longitude; zoom: number; idx: number; ele: number | null; toJSON: () => TrackPoint; } export default class DatabaseLines { LineModel = createModel(); LinePointModel = createModel(); LineDataModel = createModel(); _db: Database; constructor(database: Database) { this._db = database; this.LineModel.init({ id: getDefaultIdType(), 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 } satisfies Partial> as any).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, mapValues(routeInfo, (val) => val == null ? null : val)); // Use null instead of undefined 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.map((point) => omit(point, ["lineId", "pos"]) as TrackPoint)); } 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 () => await this.LineModel.findAll({ attributes: ["id"], where: { padId } })) .map((line) => line.id) .batch(50000) .flatMap(wrapAsync(async (lineIds) => { const linePoints = await this.LinePointModel.findAll({ where: { [Op.and]: [ { zoom: { [Op.lte]: bboxWithZoom.zoom }, lineId: { [Op.in]: lineIds } }, makeBboxCondition(bboxWithZoom) ] }, attributes: ["pos", "lat", "lon", "ele", "zoom", "idx", "lineId"] }); return Object.entries(groupBy(linePoints, "lineId")).map(([key, val]) => ({ id: Number(key), trackPoints: val.map((p) => omit(p.toJSON(), ["lineId", "pos"])) })); })).flatten(); } async getAllLinePoints(lineId: ID): Promise { const points = await this.LineModel.build({ id: lineId } satisfies Partial> as any).getLinePoints({ attributes: [ "pos", "lat", "lon", "ele", "zoom", "idx" ], order: [["idx", "ASC"]] }); return points.map((point) => omit(point.toJSON(), ["pos"]) as TrackPoint); } }