facilmap/server/src/socket.ts

929 wiersze
26 KiB
TypeScript
Czysty Zwykły widok Historia

2020-12-24 17:51:21 +00:00
import { promiseProps, stripObject } from "./utils/utils";
2020-04-14 23:40:00 +00:00
import { streamToArrayPromise } from "./utils/streams";
import { isInBbox } from "./utils/geo";
2020-12-24 17:51:21 +00:00
import { Server, Socket as SocketIO } from "socket.io";
2020-04-14 23:40:00 +00:00
import domain from "domain";
2020-12-24 17:51:21 +00:00
import { exportLineToGpx } from "./export/gpx";
import { find } from "./search";
import { geoipLookup } from "./geoip";
2021-03-25 19:34:48 +00:00
import { isEqual, omit } from "lodash";
2020-12-24 17:51:21 +00:00
import Database, { DatabaseEvents } from "./database/database";
import { Server as HttpServer } from "http";
import { Bbox, BboxWithZoom, EventHandler, EventName, MapEvents, MultipleEvents, PadData, PadId, RequestData, RequestName, ResponseData, Writable } from "facilmap-types";
2020-12-24 17:51:21 +00:00
import { calculateRoute, prepareForBoundingBox } from "./routing/routing";
import { RouteWithId } from "./database/route";
type SocketHandlers = {
[requestName in RequestName]: RequestData<requestName> extends void ? () => any : (data: RequestData<requestName>) => ResponseData<requestName> | PromiseLike<ResponseData<requestName>>;
} & {
error: (data: Error) => void;
disconnect: () => void;
};
type DatabaseHandlers = {
[eventName in EventName<DatabaseEvents>]?: EventHandler<DatabaseEvents, eventName>;
}
type MultipleEventPromises = {
[eventName in keyof MultipleEvents<MapEvents>]: PromiseLike<MultipleEvents<MapEvents>[eventName]> | MultipleEvents<MapEvents>[eventName];
}
function isPadId(padId: PadId | true | undefined): padId is PadId {
return !!(padId && padId !== true);
}
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
export default class Socket {
constructor(server: HttpServer, database: Database) {
const io = new Server(server, {
2021-03-27 17:17:28 +00:00
cors: { origin: true },
2022-02-18 17:46:43 +00:00
allowEIO3: true,
maxHttpBufferSize: 100e6
});
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
io.sockets.on("connection", (socket: SocketIO) => {
2020-12-24 04:57:46 +00:00
const d = domain.create();
2016-10-30 14:01:21 +00:00
d.add(socket);
d.on("error", function(err) {
console.error("Uncaught error in socket:", err.stack);
socket.disconnect();
});
new SocketConnection(socket, database);
});
}
}
class SocketConnection {
2020-12-24 17:51:21 +00:00
socket: SocketIO;
database: Database;
padId: PadId | true | undefined = undefined;
bbox: BboxWithZoom | undefined = undefined;
writable: Writable | undefined = undefined;
2021-03-25 19:34:48 +00:00
route: Omit<RouteWithId, "trackPoints"> | undefined = undefined;
routes: Record<string, Omit<RouteWithId, "trackPoints">> = { };
2020-12-24 17:51:21 +00:00
historyListener: undefined | (() => void) = undefined;
pauseHistoryListener = 0;
constructor(socket: SocketIO, database: Database) {
2016-10-30 14:01:21 +00:00
this.socket = socket;
this.database = database;
this.registerSocketHandlers();
}
registerSocketHandlers() {
2020-12-24 17:51:21 +00:00
for (const i of Object.keys(this.socketHandlers) as Array<keyof SocketHandlers>) {
2023-09-12 11:31:55 +00:00
this.socket.on(i, async (data: any, callback: any): Promise<void> => {
2020-12-24 17:51:21 +00:00
try {
const res = await this.socketHandlers[i](data);
2021-05-06 01:40:59 +00:00
2016-10-30 14:01:21 +00:00
if(!callback && res)
console.trace("No callback available to send result of socket handler " + i);
callback && callback(null, res);
2023-09-12 11:31:55 +00:00
} catch (err: any) {
2016-10-30 14:01:21 +00:00
console.log(err.stack);
2021-03-04 15:45:34 +00:00
callback && callback({ message: err.message, stack: err.stack });
2020-12-24 17:51:21 +00:00
}
2016-10-30 14:01:21 +00:00
});
2020-12-24 17:51:21 +00:00
}
2016-10-30 14:01:21 +00:00
}
2020-12-24 17:51:21 +00:00
registerDatabaseHandler<E extends EventName<DatabaseEvents>>(eventName: E, handler: EventHandler<DatabaseEvents, E>) {
this.database.on(eventName, handler);
2016-10-30 14:01:21 +00:00
return () => {
2020-12-24 17:51:21 +00:00
this.database.off(eventName, handler);
2016-10-30 14:01:21 +00:00
};
}
registerDatabaseHandlers() {
2020-12-24 17:51:21 +00:00
for (const eventName of Object.keys(this.databaseHandlers) as Array<EventName<DatabaseEvents>>) {
this.database.on(eventName as any, this.databaseHandlers[eventName] as any);
}
2016-10-30 14:01:21 +00:00
}
unregisterDatabaseHandlers() {
2020-12-24 17:51:21 +00:00
for (const eventName of Object.keys(this.databaseHandlers) as Array<EventName<DatabaseEvents>>) {
this.database.removeListener(eventName as any, this.databaseHandlers[eventName] as any);
}
2016-10-30 14:01:21 +00:00
}
2020-12-24 17:51:21 +00:00
getPadObjects(padData: PadData) {
const promises: MultipleEventPromises = {
2016-10-30 14:01:21 +00:00
padData: [ padData ],
2020-12-24 17:51:21 +00:00
view: streamToArrayPromise(this.database.views.getViews(padData.id)),
type: streamToArrayPromise(this.database.types.getTypes(padData.id)),
line: streamToArrayPromise(this.database.lines.getPadLines(padData.id))
2016-10-30 14:01:21 +00:00
};
if(this.bbox) { // In case bbox is set while fetching pad data
2020-04-14 23:40:00 +00:00
Object.assign(promises, {
2020-12-24 17:51:21 +00:00
marker: streamToArrayPromise(this.database.markers.getPadMarkers(padData.id, this.bbox)),
linePoints: streamToArrayPromise(this.database.lines.getLinePointsForPad(padData.id, this.bbox))
2016-10-30 14:01:21 +00:00
});
}
2020-04-14 23:40:00 +00:00
return promiseProps(promises);
2016-10-30 14:01:21 +00:00
}
2020-12-24 17:51:21 +00:00
validateConditions(minimumPermissions: Writable, data?: any, structure?: object) {
if(structure && !stripObject(data, structure))
throw new Error("Invalid parameters.");
if (minimumPermissions == Writable.ADMIN && ![Writable.ADMIN].includes(this.writable!))
throw new Error('Only available in admin mode.');
else if (minimumPermissions === Writable.WRITE && ![Writable.ADMIN, Writable.WRITE].includes(this.writable!))
throw new Error('Only available in write mode.');
}
socketHandlers: SocketHandlers = {
error: (err) => {
2016-10-30 14:01:21 +00:00
console.error("Error! Disconnecting client.");
console.error(err.stack);
this.socket.disconnect();
},
2020-12-24 17:51:21 +00:00
setPadId: async (padId) => {
if(typeof padId != "string")
throw new Error("Invalid pad id");
if(this.padId != null)
throw new Error("Pad id already set");
this.padId = true;
const [admin, write, read] = await Promise.all([
this.database.pads.getPadDataByAdminId(padId),
this.database.pads.getPadDataByWriteId(padId),
this.database.pads.getPadData(padId)
]);
let pad;
if(admin)
pad = { ...admin, writable: Writable.ADMIN };
else if(write)
pad = { ...write, writable: Writable.WRITE, adminId: undefined };
else if(read)
pad = { ...read, writable: Writable.READ, writeId: undefined, adminId: undefined };
else {
this.padId = undefined;
throw new Error("This pad does not exist");
}
2021-05-06 01:40:59 +00:00
2020-12-24 17:51:21 +00:00
this.padId = pad.id;
this.writable = pad.writable;
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
this.registerDatabaseHandlers();
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return await this.getPadObjects(pad);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
updateBbox: async (bbox) => {
this.validateConditions(Writable.READ, bbox, {
top: "number",
left: "number",
bottom: "number",
right: "number",
zoom: "number"
});
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
const bboxWithExcept: BboxWithZoom & { except?: Bbox } = { ...bbox };
2016-10-30 14:01:21 +00:00
if(this.bbox && bbox.zoom == this.bbox.zoom)
bboxWithExcept.except = this.bbox;
this.bbox = bbox;
2020-12-24 17:51:21 +00:00
const ret: MultipleEventPromises = {};
2016-10-30 14:01:21 +00:00
if(this.padId && this.padId !== true) {
2020-12-24 17:51:21 +00:00
ret.marker = streamToArrayPromise(this.database.markers.getPadMarkers(this.padId, bboxWithExcept));
ret.linePoints = streamToArrayPromise(this.database.lines.getLinePointsForPad(this.padId, bboxWithExcept));
2016-10-30 14:01:21 +00:00
}
if(this.route)
2020-12-24 17:51:21 +00:00
ret.routePoints = this.database.routes.getRoutePoints(this.route.id, bboxWithExcept, !bboxWithExcept.except).then((points) => ([points]));
2021-03-25 19:34:48 +00:00
if(Object.keys(this.routes).length > 0) {
ret.routePointsWithId = Promise.all(Object.keys(this.routes).map(
(routeId) => this.database.routes.getRoutePoints(this.routes[routeId].id, bboxWithExcept, !bboxWithExcept.except).then((trackPoints) => ({ routeId, trackPoints }))
));
}
2020-12-24 17:51:21 +00:00
return await promiseProps(ret);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
disconnect: () => {
2016-10-30 14:01:21 +00:00
if(this.padId)
this.unregisterDatabaseHandlers();
2020-12-24 17:51:21 +00:00
if (this.historyListener) {
this.historyListener();
this.historyListener = undefined;
}
if(this.route) {
2020-12-24 17:51:21 +00:00
this.database.routes.deleteRoute(this.route.id).catch((err) => {
console.error("Error clearing route", err.stack || err);
});
}
2021-03-25 19:34:48 +00:00
for (const routeId of Object.keys(this.routes)) {
this.database.routes.deleteRoute(this.routes[routeId].id).catch((err) => {
console.error("Error clearing route", err.stack || err);
});
}
2016-10-30 14:01:21 +00:00
},
2021-05-06 01:40:59 +00:00
getPad: async (data) => {
this.validateConditions(Writable.READ, data, {
padId: "string"
});
const padData = await this.database.pads.getPadDataByAnyId(data.padId);
return padData && {
id: padData.id,
name: padData.name,
description: padData.description
};
},
findPads: async (data) => {
this.validateConditions(Writable.READ, data, {
query: "string",
start: "number",
limit: "number"
});
return this.database.pads.findPads(data);
},
2020-12-24 17:51:21 +00:00
createPad: async (data) => {
this.validateConditions(Writable.READ, data, {
name: "string",
defaultViewId: "number",
id: "string",
writeId: "string",
adminId: "string",
searchEngines: "boolean",
description: "string",
clusterMarkers: "boolean",
legend1: "string",
legend2: "string"
});
if(this.padId)
throw new Error("Pad already loaded.");
const padData = await this.database.pads.createPad(data);
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
this.padId = padData.id;
this.writable = Writable.ADMIN;
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
this.registerDatabaseHandlers();
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return await this.getPadObjects(padData);
},
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
editPad: async (data) => {
this.validateConditions(Writable.ADMIN, data, {
name: "string",
defaultViewId: "number",
id: "string",
writeId: "string",
adminId: "string",
searchEngines: "boolean",
description: "string",
clusterMarkers: "boolean",
legend1: "string",
legend2: "string"
2016-10-30 14:01:21 +00:00
});
2021-05-06 01:40:59 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
return await this.database.pads.updatePadData(this.padId, data);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
deletePad: async () => {
this.validateConditions(Writable.ADMIN);
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
await this.database.pads.deletePad(this.padId);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
getMarker: async (data) => {
this.validateConditions(Writable.READ, data, {
id: "number"
} );
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
return await this.database.markers.getMarker(this.padId, data.id);
},
2020-12-24 17:51:21 +00:00
addMarker: async (data) => {
this.validateConditions(Writable.WRITE, data, {
lat: "number",
lon: "number",
name: "string",
colour: "string",
size: "number",
symbol: "string",
shape: "string",
typeId: "number",
data: Object
});
2018-03-18 17:31:38 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2018-03-18 17:31:38 +00:00
2020-12-24 17:51:21 +00:00
return await this.database.markers.createMarker(this.padId, data);
2018-03-18 17:31:38 +00:00
},
2020-12-24 17:51:21 +00:00
editMarker: async (data) => {
this.validateConditions(Writable.WRITE, data, {
id: "number",
lat: "number",
lon: "number",
name: "string",
colour: "string",
size: "number",
symbol: "string",
shape: "string",
typeId: "number",
data: Object
});
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return this.database.markers.updateMarker(this.padId, data.id, data);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
deleteMarker: async (data) => {
this.validateConditions(Writable.WRITE, data, {
id: "number"
});
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return this.database.markers.deleteMarker(this.padId, data.id);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
getLineTemplate: async (data) => {
this.validateConditions(Writable.WRITE, data, {
typeId: "number"
}); // || data.typeId == null)
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return await this.database.lines.getLineTemplate(this.padId, data);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
addLine: async (data) => {
this.validateConditions(Writable.WRITE, data, {
routePoints: [ { lat: "number", lon: "number" } ],
trackPoints: [ { lat: "number", lon: "number" } ],
mode: "string",
colour: "string",
width: "number",
name: "string",
typeId: "number",
data: Object
});
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2021-03-25 19:34:48 +00:00
let fromRoute;
if (data.mode != "track") {
for (const route of [...(this.route ? [this.route] : []), ...Object.values(this.routes)]) {
if(isEqual(route.routePoints, data.routePoints) && data.mode == route.mode) {
fromRoute = { ...route, trackPoints: await this.database.routes.getAllRoutePoints(route.id) };
break;
}
}
}
2021-05-06 01:40:59 +00:00
2021-03-25 19:34:48 +00:00
return await this.database.lines.createLine(this.padId, data, fromRoute);
2020-12-24 17:51:21 +00:00
},
editLine: async (data) => {
this.validateConditions(Writable.WRITE, 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
2016-10-30 14:01:21 +00:00
});
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2016-10-30 14:01:21 +00:00
2021-03-25 19:34:48 +00:00
let fromRoute;
if (data.mode != "track") {
for (const route of [...(this.route ? [this.route] : []), ...Object.values(this.routes)]) {
if(isEqual(route.routePoints, data.routePoints) && data.mode == route.mode) {
fromRoute = { ...route, trackPoints: await this.database.routes.getAllRoutePoints(route.id) };
break;
}
}
}
2016-10-30 14:01:21 +00:00
2021-03-25 19:34:48 +00:00
return await this.database.lines.updateLine(this.padId, data.id, data, undefined, fromRoute);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
deleteLine: async (data) => {
this.validateConditions(Writable.WRITE, data, {
id: "number"
});
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return this.database.lines.deleteLine(this.padId, data.id);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
exportLine: async (data) => {
this.validateConditions(Writable.READ, data, {
id: "string",
format: "string"
});
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
const lineP = this.database.lines.getLine(this.padId, data.id);
lineP.catch(() => null); // Avoid unhandled promise error (https://stackoverflow.com/a/59062117/242365)
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
const [line, trackPoints, type] = await Promise.all([
lineP,
this.database.lines.getAllLinePoints(data.id),
lineP.then((line) => this.database.types.getType(this.padId as string, line.typeId))
]);
2020-12-24 17:51:21 +00:00
const lineWithTrackPoints = { ...line, trackPoints };
2020-12-24 17:51:21 +00:00
switch(data.format) {
case "gpx-trk":
return exportLineToGpx(lineWithTrackPoints, type, true);
case "gpx-rte":
return exportLineToGpx(lineWithTrackPoints, type, false);
default:
throw new Error("Unknown format.");
}
},
2020-12-24 17:51:21 +00:00
addView: async (data) => {
this.validateConditions(Writable.ADMIN, data, {
name: "string",
baseLayer: "string",
layers: [ "string" ],
top: "number",
left: "number",
right: "number",
bottom: "number",
filter: "string"
});
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2021-05-06 01:40:59 +00:00
2020-12-24 17:51:21 +00:00
return await this.database.views.createView(this.padId, data);
},
editView: async (data) => {
this.validateConditions(Writable.ADMIN, data, {
id: "number",
baseLayer: "string",
layers: [ "string" ],
top: "number",
left: "number",
right: "number",
bottom: "number",
filter: "string"
2016-10-30 14:01:21 +00:00
});
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return await this.database.views.updateView(this.padId, data.id, data);
},
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
deleteView: async (data) => {
this.validateConditions(Writable.ADMIN, data, {
id: "number"
2016-10-30 14:01:21 +00:00
});
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
return await this.database.views.deleteView(this.padId, data.id);
},
addType: async (data) => {
this.validateConditions(Writable.ADMIN, data, {
id: "number",
name: "string",
type: "string",
defaultColour: "string", colourFixed: "boolean",
defaultSize: "number", sizeFixed: "boolean",
defaultSymbol: "string", symbolFixed: "boolean",
defaultShape: "string", shapeFixed: "boolean",
defaultWidth: "number", widthFixed: "boolean",
defaultMode: "string", modeFixed: "boolean",
showInLegend: "boolean",
fields: [ {
name: "string",
type: "string",
default: "string",
controlColour: "boolean", controlSize: "boolean", controlSymbol: "boolean", controlShape: "boolean", controlWidth: "boolean",
options: [ { key: "string", value: "string", colour: "string", size: "number", "symbol": "string", shape: "string", width: "number" } ]
}]
});
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return await this.database.types.createType(this.padId, data);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
editType: async (data) => {
this.validateConditions(Writable.ADMIN, data, {
id: "number",
name: "string",
defaultColour: "string", colourFixed: "boolean",
defaultSize: "number", sizeFixed: "boolean",
defaultSymbol: "string", symbolFixed: "boolean",
defaultShape: "string", shapeFixed: "boolean",
defaultWidth: "number", widthFixed: "boolean",
defaultMode: "string", modeFixed: "boolean",
showInLegend: "boolean",
fields: [ {
2016-10-30 14:01:21 +00:00
name: "string",
2020-12-24 17:51:21 +00:00
oldName: "string",
2016-10-30 14:01:21 +00:00
type: "string",
2020-12-24 17:51:21 +00:00
default: "string",
controlColour: "boolean", controlSize: "boolean", controlSymbol: "boolean", controlShape: "boolean", controlWidth: "boolean",
options: [ { key: "string", value: "string", oldValue: "string", colour: "string", size: "number", "symbol": "string", shape: "string", width: "number" } ]
}]
2016-10-30 14:01:21 +00:00
});
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
const rename: Record<string, { name?: string, values?: Record<string, string> }> = {};
for(const field of (data.fields || [])) {
if(field.oldName && field.oldName != field.name)
rename[field.oldName] = { name: field.name };
2021-03-18 11:22:34 +00:00
if(field.options) {
2020-12-24 17:51:21 +00:00
for(const option of field.options) {
if(option.oldValue && option.oldValue != option.value) {
if(!rename[field.oldName || field.name])
rename[field.oldName || field.name] = { };
if(!rename[field.oldName || field.name].values)
rename[field.oldName || field.name].values = { };
rename[field.oldName || field.name].values![option.oldValue] = option.value;
}
2020-12-24 17:51:21 +00:00
delete option.oldValue;
}
}
2020-12-24 17:51:21 +00:00
delete field.oldName;
}
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
// We first update the type (without updating the styles). If that succeeds, we rename the data fields.
// Only then we update the object styles (as they often depend on the field values).
const newType = await this.database.types.updateType(this.padId, data.id, data, false)
2021-05-06 01:40:59 +00:00
2020-12-24 17:51:21 +00:00
if(Object.keys(rename).length > 0)
await this.database.helpers.renameObjectDataField(this.padId, data.id, rename, newType.type == "line");
2021-05-06 01:40:59 +00:00
2020-12-24 17:51:21 +00:00
await this.database.types.recalculateObjectStylesForType(newType.padId, newType.id, newType.type == "line")
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return newType;
},
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
deleteType: async (data) => {
this.validateConditions(Writable.ADMIN, data, {
id: "number"
2016-10-30 14:01:21 +00:00
});
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return await this.database.types.deleteType(this.padId, data.id);
},
find: async (data) => {
this.validateConditions(Writable.READ, data, {
query: "string",
loadUrls: "boolean",
elevation: "boolean"
2016-10-30 14:01:21 +00:00
});
2020-12-24 17:51:21 +00:00
return await find(data.query, data.loadUrls, data.elevation);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
findOnMap: async (data) => {
this.validateConditions(Writable.READ, data, {
query: "string"
});
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2020-12-24 17:51:21 +00:00
return await this.database.search.search(this.padId, data.query);
},
2020-12-24 17:51:21 +00:00
getRoute: async (data) => {
this.validateConditions(Writable.READ, data, {
destinations: [ { lat: "number", lon: "number" } ],
mode: "string"
2016-10-30 14:01:21 +00:00
});
2020-12-24 17:51:21 +00:00
return await calculateRoute(data.destinations, data.mode);
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
setRoute: async (data) => {
2021-03-25 19:34:48 +00:00
this.validateConditions(Writable.READ, data, { routePoints: [ { lat: "number", lon: "number" } ], mode: "string", routeId: "string" });
const existingRoute = data.routeId ? this.routes[data.routeId] : this.route;
2020-12-24 17:51:21 +00:00
let routeInfo;
2021-03-25 19:34:48 +00:00
if(existingRoute)
routeInfo = await this.database.routes.updateRoute(existingRoute.id, data.routePoints, data.mode);
2020-12-24 17:51:21 +00:00
else
routeInfo = await this.database.routes.createRoute(data.routePoints, data.mode);
if(!routeInfo) {
// A newer submitted route has returned in the meantime
console.log("Ignoring outdated route");
return;
}
2021-03-25 19:34:48 +00:00
if (data.routeId)
this.routes[data.routeId] = omit(routeInfo, "trackPoints");
else
this.route = omit(routeInfo, "trackPoints");
2020-12-24 17:51:21 +00:00
if(this.bbox)
routeInfo.trackPoints = prepareForBoundingBox(routeInfo.trackPoints, this.bbox, true);
else
routeInfo.trackPoints = [];
return {
2021-03-25 19:34:48 +00:00
routeId: data.routeId,
2021-04-01 19:31:11 +00:00
top: routeInfo.top,
left: routeInfo.left,
bottom: routeInfo.bottom,
right: routeInfo.right,
2020-12-24 17:51:21 +00:00
routePoints: routeInfo.routePoints,
mode: routeInfo.mode,
time: routeInfo.time,
distance: routeInfo.distance,
ascent: routeInfo.ascent,
descent: routeInfo.descent,
extraInfo: routeInfo.extraInfo,
trackPoints: routeInfo.trackPoints
};
},
2021-03-25 19:34:48 +00:00
clearRoute: async (data) => {
if (data) {
this.validateConditions(Writable.READ, data, {
routeId: "string"
});
}
let route;
if (data?.routeId != null) {
route = this.routes[data.routeId];
delete this.routes[data.routeId];
} else {
route = this.route;
this.route = undefined;
}
if (route)
await this.database.routes.deleteRoute(route.id);
},
2020-12-24 17:51:21 +00:00
lineToRoute: async (data) => {
this.validateConditions(Writable.READ, data, {
2021-03-25 19:34:48 +00:00
id: "number",
routeId: "string"
});
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2021-03-25 19:34:48 +00:00
const existingRoute = data.routeId ? this.routes[data.routeId] : this.route;
const routeInfo = await this.database.routes.lineToRoute(existingRoute?.id, this.padId, data.id);
if (!routeInfo) {
// A newer submitted route has returned in the meantime
console.log("Ignoring outdated route");
return;
}
if (data.routeId)
this.routes[routeInfo.id] = omit(routeInfo, "trackPoints");
else
this.route = omit(routeInfo, "trackPoints");
2020-12-24 17:51:21 +00:00
if(this.bbox)
routeInfo.trackPoints = prepareForBoundingBox(routeInfo.trackPoints, this.bbox, true);
else
routeInfo.trackPoints = [];
2020-12-24 17:51:21 +00:00
return {
2021-03-25 19:34:48 +00:00
routeId: data.routeId,
2021-04-01 19:31:11 +00:00
top: routeInfo.top,
left: routeInfo.left,
bottom: routeInfo.bottom,
right: routeInfo.right,
2020-12-24 17:51:21 +00:00
routePoints: routeInfo.routePoints,
mode: routeInfo.mode,
time: routeInfo.time,
distance: routeInfo.distance,
ascent: routeInfo.ascent,
descent: routeInfo.descent,
trackPoints: routeInfo.trackPoints
};
},
exportRoute: async (data) => {
this.validateConditions(Writable.READ, data, {
2021-03-25 19:34:48 +00:00
format: "string",
routeId: "string"
});
2020-12-24 17:51:21 +00:00
2021-03-25 19:34:48 +00:00
const route = data.routeId ? this.routes[data.routeId] : this.route;
if (!route) {
throw new Error("Route not available.");
2020-12-24 17:51:21 +00:00
}
2021-03-25 19:34:48 +00:00
const trackPoints = await this.database.routes.getAllRoutePoints(route.id);
2020-12-24 17:51:21 +00:00
2021-03-25 19:34:48 +00:00
const routeInfo = { ...this.route, trackPoints };
2020-12-24 17:51:21 +00:00
switch(data.format) {
case "gpx-trk":
2021-03-25 19:34:48 +00:00
return await exportLineToGpx(routeInfo, undefined, true);
2020-12-24 17:51:21 +00:00
case "gpx-rte":
2021-03-25 19:34:48 +00:00
return await exportLineToGpx(routeInfo, undefined, false);
2020-12-24 17:51:21 +00:00
default:
throw new Error("Unknown format.");
}
},
2020-12-24 17:51:21 +00:00
listenToHistory: async () => {
this.validateConditions(Writable.WRITE);
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2021-05-06 01:40:59 +00:00
2020-12-24 17:51:21 +00:00
if(this.historyListener)
throw new Error("Already listening to history.");
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
this.historyListener = this.registerDatabaseHandler("addHistoryEntry", (padId, data) => {
if(padId == this.padId && (this.writable == Writable.ADMIN || ["Marker", "Line"].includes(data.type)) && !this.pauseHistoryListener)
this.socket.emit("history", data);
});
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return promiseProps({
history: streamToArrayPromise(this.database.history.getHistory(this.padId, this.writable == Writable.ADMIN ? undefined : ["Marker", "Line"]))
2016-10-30 14:01:21 +00:00
});
},
2020-12-24 17:51:21 +00:00
stopListeningToHistory: () => {
this.validateConditions(Writable.WRITE);
2016-10-31 12:11:36 +00:00
2020-12-24 17:51:21 +00:00
if(!this.historyListener)
throw new Error("Not listening to history.");
2016-10-31 12:11:36 +00:00
this.historyListener(); // Unregister db listener
2020-12-24 17:51:21 +00:00
this.historyListener = undefined;
2016-10-31 12:11:36 +00:00
},
2020-12-24 17:51:21 +00:00
revertHistoryEntry: async (data) => {
this.validateConditions(Writable.WRITE, data, {
id: "number"
});
2016-10-31 12:11:36 +00:00
2020-12-24 17:51:21 +00:00
if (!isPadId(this.padId))
throw new Error("No map opened.");
2020-12-24 17:51:21 +00:00
const historyEntry = await this.database.history.getHistoryEntry(this.padId, data.id);
2021-05-06 01:40:59 +00:00
2020-12-24 17:51:21 +00:00
if(!["Marker", "Line"].includes(historyEntry.type) && this.writable != Writable.ADMIN)
throw new Error("This kind of change can only be reverted in admin mode.");
2017-03-30 14:01:46 +00:00
2020-12-24 17:51:21 +00:00
this.pauseHistoryListener++;
2021-05-06 01:40:59 +00:00
2020-12-24 17:51:21 +00:00
try {
await this.database.history.revertHistoryEntry(this.padId, data.id);
} finally {
this.pauseHistoryListener--;
}
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
return promiseProps({
history: streamToArrayPromise(this.database.history.getHistory(this.padId, this.writable == Writable.ADMIN ? undefined : ["Marker", "Line"]))
2016-10-30 14:01:21 +00:00
});
},
2020-12-24 17:51:21 +00:00
geoip: async () => {
const ip = (this.socket.handshake.headers as Record<string, string>)["x-forwarded-for"] || this.socket.request.connection.remoteAddress;
return ip && await geoipLookup(ip);
2016-10-31 12:11:36 +00:00
}
2016-10-30 14:01:21 +00:00
/*copyPad : function(data, callback) {
2020-04-14 23:40:00 +00:00
if(!stripObject(data, { toId: "string" }))
2016-10-30 14:01:21 +00:00
return callback("Invalid parameters.");
this.database.copyPad(this.padId, data.toId, callback);
}*/
2020-12-24 17:51:21 +00:00
};
2016-10-30 14:01:21 +00:00
2020-12-24 17:51:21 +00:00
databaseHandlers: DatabaseHandlers = {
line: (padId, data) => {
2016-10-30 14:01:21 +00:00
if(padId == this.padId)
this.socket.emit("line", data);
},
2020-12-24 17:51:21 +00:00
linePoints: (padId, lineId, trackPoints) => {
2016-10-30 14:01:21 +00:00
if(padId == this.padId)
2020-12-24 17:51:21 +00:00
this.socket.emit("linePoints", { reset: true, id: lineId, trackPoints : (this.bbox ? prepareForBoundingBox(trackPoints, this.bbox) : [ ]) });
2016-10-30 14:01:21 +00:00
},
2020-12-24 17:51:21 +00:00
deleteLine: (padId, data) => {
2016-10-30 14:01:21 +00:00
if(padId == this.padId)
this.socket.emit("deleteLine", data);
},
2020-12-24 17:51:21 +00:00
marker: (padId, data) => {
2020-04-14 23:40:00 +00:00
if(padId == this.padId && this.bbox && isInBbox(data, this.bbox))
2016-10-30 14:01:21 +00:00
this.socket.emit("marker", data);
},
2020-12-24 17:51:21 +00:00
deleteMarker: (padId, data) => {
2016-10-30 14:01:21 +00:00
if(padId == this.padId)
this.socket.emit("deleteMarker", data);
},
2020-12-24 17:51:21 +00:00
type: (padId, data) => {
2016-10-30 14:01:21 +00:00
if(padId == this.padId)
this.socket.emit("type", data);
},
2020-12-24 17:51:21 +00:00
deleteType: (padId, data) => {
2016-10-30 14:01:21 +00:00
if(padId == this.padId)
this.socket.emit("deleteType", data);
},
2020-12-24 17:51:21 +00:00
padData: (padId, data) => {
2016-10-30 14:01:21 +00:00
if(padId == this.padId) {
2020-12-24 04:57:46 +00:00
const dataClone = JSON.parse(JSON.stringify(data));
2020-12-24 17:51:21 +00:00
if(this.writable == Writable.READ)
2016-10-30 14:01:21 +00:00
dataClone.writeId = null;
2020-12-24 17:51:21 +00:00
if(this.writable != Writable.ADMIN)
2017-03-30 14:01:46 +00:00
dataClone.adminId = null;
2016-10-30 14:01:21 +00:00
this.padId = data.id;
this.socket.emit("padData", dataClone);
}
},
2020-12-24 17:51:21 +00:00
deletePad: (padId) => {
if (padId == this.padId) {
this.socket.emit("deletePad");
2020-12-24 17:51:21 +00:00
this.writable = Writable.READ;
}
},
2020-12-24 17:51:21 +00:00
view: (padId, data) => {
2016-10-30 14:01:21 +00:00
if(padId == this.padId)
this.socket.emit("view", data);
},
2020-12-24 17:51:21 +00:00
deleteView: (padId, data) => {
2016-10-30 14:01:21 +00:00
if(padId == this.padId)
this.socket.emit("deleteView", data);
}
2020-12-24 17:51:21 +00:00
};
}