kopia lustrzana https://github.com/FacilMap/facilmap
Rename "symbol" to "marker", introduce v3 socket version
rodzic
e080834696
commit
3589ec3336
|
@ -0,0 +1,11 @@
|
|||
diff --git a/dist/client.d.ts b/dist/client.d.ts
|
||||
index f441770687db57140a4c50b4a78a15b696f44177..3dbea34c932e79d6e3dbbf49b5dcafe2fb54cccd 100644
|
||||
--- a/dist/client.d.ts
|
||||
+++ b/dist/client.d.ts
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Socket as SocketIO } from "socket.io-client";
|
||||
-import { Bbox, BboxWithZoom, EventHandler, EventName, FindOnMapQuery, FindPadsQuery, FindPadsResult, FindQuery, GetPadQuery, HistoryEntry, ID, Line, LineCreate, LineExportRequest, LineTemplateRequest, LineToRouteCreate, LineUpdate, MapEvents, Marker, MarkerCreate, MarkerUpdate, MultipleEvents, ObjectWithId, PadData, PadDataCreate, PadDataUpdate, PadId, PagedResults, RequestData, RequestName, ResponseData, Route, RouteClear, RouteCreate, RouteExportRequest, RouteInfo, RouteRequest, SearchResult, TrackPoint, Type, TypeCreate, TypeUpdate, View, ViewCreate, ViewUpdate, Writable } from "facilmap-types";
|
||||
+import { Bbox, BboxWithZoom, EventHandler, EventName, FindOnMapQuery, FindPadsQuery, FindPadsResult, FindQuery, GetPadQuery, HistoryEntry, ID, Line, LineCreate, LineExportRequest, LineTemplateRequest, LineToRouteCreate, LineUpdate, MapEvents, Marker, MarkerCreate, MarkerUpdate, MultipleEvents, ObjectWithId, PadData, PadDataCreate, PadDataUpdate, PadId, PagedResults, RequestData, RequestName, ResponseData, Route, RouteClear, RouteCreate, RouteExportRequest, RouteInfo, RouteRequest, SearchResult, TrackPoint, Type, TypeCreate, TypeUpdate, View, ViewCreate, ViewUpdate, Writable } from "facilmap-types-v3";
|
||||
export interface ClientEvents<DataType = Record<string, string>> extends MapEvents<DataType> {
|
||||
connect: [];
|
||||
disconnect: [string];
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "facilmap-client",
|
||||
"version": "4.1.1",
|
||||
"version": "5.0.0",
|
||||
"description": "A library that acts as a client to FacilMap and makes it possible to retrieve and modify objects on a collaborative map.",
|
||||
"keywords": [
|
||||
"maps",
|
||||
|
|
|
@ -2,7 +2,7 @@ import { io, type ManagerOptions, type Socket as SocketIO, type SocketOptions }
|
|||
import { type Bbox, type BboxWithZoom, type CRU, type EventHandler, type EventName, type FindOnMapQuery, type FindPadsQuery, type FindPadsResult, type FindQuery, type GetPadQuery, type HistoryEntry, type ID, type Line, type LineExportRequest, type LineTemplateRequest, type LineToRouteCreate, type SocketEvents, type Marker, type MultipleEvents, type ObjectWithId, type PadData, type PadId, type PagedResults, type SocketRequest, type SocketRequestName, type SocketResponse, type Route, type RouteClear, type RouteCreate, type RouteExportRequest, type RouteInfo, type RouteRequest, type SearchResult, type SocketVersion, type TrackPoint, type Type, type View, type Writable, type SocketClientToServerEvents, type SocketServerToClientEvents, type LineTemplate, type LinePointsEvent, PadNotFoundError, type SetLanguageRequest } from "facilmap-types";
|
||||
import { deserializeError, errorConstructors, serializeError } from "serialize-error";
|
||||
|
||||
export interface ClientEvents extends SocketEvents<SocketVersion.V2> {
|
||||
export interface ClientEventsInterface extends SocketEvents<SocketVersion.V3> {
|
||||
connect: [];
|
||||
disconnect: [string];
|
||||
connect_error: [Error];
|
||||
|
@ -21,11 +21,13 @@ export interface ClientEvents extends SocketEvents<SocketVersion.V2> {
|
|||
route: [RouteWithTrackPoints];
|
||||
clearRoute: [RouteClear];
|
||||
|
||||
emit: { [eventName in SocketRequestName<SocketVersion.V2>]: [eventName, SocketRequest<SocketVersion.V2, eventName>] }[SocketRequestName<SocketVersion.V2>];
|
||||
emitResolve: { [eventName in SocketRequestName<SocketVersion.V2>]: [eventName, SocketResponse<SocketVersion.V2, eventName>] }[SocketRequestName<SocketVersion.V2>];
|
||||
emitReject: [SocketRequestName<SocketVersion.V2>, Error];
|
||||
emit: { [eventName in SocketRequestName<SocketVersion.V3>]: [eventName, SocketRequest<SocketVersion.V3, eventName>] }[SocketRequestName<SocketVersion.V3>];
|
||||
emitResolve: { [eventName in SocketRequestName<SocketVersion.V3>]: [eventName, SocketResponse<SocketVersion.V3, eventName>] }[SocketRequestName<SocketVersion.V3>];
|
||||
emitReject: [SocketRequestName<SocketVersion.V3>, Error];
|
||||
}
|
||||
|
||||
export type ClientEvents = Pick<ClientEventsInterface, keyof ClientEventsInterface>; // Workaround for https://github.com/microsoft/TypeScript/issues/15300
|
||||
|
||||
const MANAGER_EVENTS: Array<EventName<ClientEvents>> = ['error', 'reconnect', 'reconnect_attempt', 'reconnect_error', 'reconnect_failed'];
|
||||
|
||||
export interface TrackPoints {
|
||||
|
@ -69,7 +71,7 @@ interface ClientData {
|
|||
errorConstructors.set("PadNotFoundError", PadNotFoundError as any);
|
||||
|
||||
class Client {
|
||||
private socket: SocketIO<SocketServerToClientEvents<SocketVersion.V2>, SocketClientToServerEvents<SocketVersion.V2>>;
|
||||
private socket: SocketIO<SocketServerToClientEvents<SocketVersion.V3>, SocketClientToServerEvents<SocketVersion.V3>>;
|
||||
private state: ClientState;
|
||||
private data: ClientData;
|
||||
|
||||
|
@ -103,7 +105,7 @@ class Client {
|
|||
});
|
||||
|
||||
const serverUrl = typeof location != "undefined" ? new URL(this.state.server, location.href) : new URL(this.state.server);
|
||||
const socket = io(`${serverUrl.origin}/v2`, {
|
||||
const socket = io(`${serverUrl.origin}/v3`, {
|
||||
forceNew: true,
|
||||
path: serverUrl.pathname.replace(/\/$/, "") + "/socket.io",
|
||||
...socketOptions
|
||||
|
@ -141,7 +143,7 @@ class Client {
|
|||
return result;
|
||||
}
|
||||
|
||||
private _fixResponseObject<T>(requestName: SocketRequestName<SocketVersion.V2>, obj: T): T {
|
||||
private _fixResponseObject<T>(requestName: SocketRequestName<SocketVersion.V3>, obj: T): T {
|
||||
if (typeof obj != "object" || !(obj as any)?.data || !["getMarker", "addMarker", "editMarker", "deleteMarker", "getLineTemplate", "addLine", "editLine", "deleteLine"].includes(requestName))
|
||||
return obj;
|
||||
|
||||
|
@ -188,7 +190,7 @@ class Client {
|
|||
}
|
||||
}
|
||||
|
||||
private async _emit<R extends SocketRequestName<SocketVersion.V2>>(eventName: R, ...[data]: SocketRequest<SocketVersion.V2, R> extends undefined | null ? [ ] : [ SocketRequest<SocketVersion.V2, R> ]): Promise<SocketResponse<SocketVersion.V2, R>> {
|
||||
private async _emit<R extends SocketRequestName<SocketVersion.V3>>(eventName: R, ...[data]: SocketRequest<SocketVersion.V3, R> extends undefined | null ? [ ] : [ SocketRequest<SocketVersion.V3, R> ]): Promise<SocketResponse<SocketVersion.V3, R>> {
|
||||
try {
|
||||
this._simulateEvent("loadStart");
|
||||
|
||||
|
@ -196,7 +198,7 @@ class Client {
|
|||
|
||||
const outerError = new Error();
|
||||
return await new Promise((resolve, reject) => {
|
||||
this.socket.emit(eventName as any, data, (err: any, data: SocketResponse<SocketVersion.V2, R>) => {
|
||||
this.socket.emit(eventName as any, data, (err: any, data: SocketResponse<SocketVersion.V3, R>) => {
|
||||
if(err) {
|
||||
const cause = deserializeError(err);
|
||||
reject(deserializeError({ ...serializeError(outerError), message: cause.message, cause }));
|
||||
|
@ -350,7 +352,7 @@ class Client {
|
|||
}
|
||||
};
|
||||
|
||||
async setPadId(padId: PadId): Promise<MultipleEvents<SocketEvents<SocketVersion.V2>>> {
|
||||
async setPadId(padId: PadId): Promise<MultipleEvents<SocketEvents<SocketVersion.V3>>> {
|
||||
if(this.state.padId != null)
|
||||
throw new Error("Pad ID already set.");
|
||||
|
||||
|
@ -361,7 +363,7 @@ class Client {
|
|||
await this._emit("setLanguage", language);
|
||||
}
|
||||
|
||||
async updateBbox(bbox: BboxWithZoom): Promise<MultipleEvents<SocketEvents<SocketVersion.V2>>> {
|
||||
async updateBbox(bbox: BboxWithZoom): Promise<MultipleEvents<SocketEvents<SocketVersion.V3>>> {
|
||||
const isZoomChange = this.bbox && bbox.zoom !== this.bbox.zoom;
|
||||
|
||||
this._set(this.state, 'bbox', bbox);
|
||||
|
@ -400,7 +402,7 @@ class Client {
|
|||
return await this._emit("findPads", data);
|
||||
}
|
||||
|
||||
async createPad(data: PadData<CRU.CREATE>): Promise<MultipleEvents<SocketEvents<SocketVersion.V2>>> {
|
||||
async createPad(data: PadData<CRU.CREATE>): Promise<MultipleEvents<SocketEvents<SocketVersion.V3>>> {
|
||||
const obj = await this._emit("createPad", data);
|
||||
this._set(this.state, 'serverError', undefined);
|
||||
this._set(this.state, 'readonly', false);
|
||||
|
@ -417,7 +419,7 @@ class Client {
|
|||
await this._emit("deletePad");
|
||||
}
|
||||
|
||||
async listenToHistory(): Promise<MultipleEvents<SocketEvents<SocketVersion.V2>>> {
|
||||
async listenToHistory(): Promise<MultipleEvents<SocketEvents<SocketVersion.V3>>> {
|
||||
const obj = await this._emit("listenToHistory");
|
||||
this._set(this.state, 'listeningToHistory', true);
|
||||
this._receiveMultiple(obj);
|
||||
|
@ -429,7 +431,7 @@ class Client {
|
|||
await this._emit("stopListeningToHistory");
|
||||
}
|
||||
|
||||
async revertHistoryEntry(data: ObjectWithId): Promise<MultipleEvents<SocketEvents<SocketVersion.V2>>> {
|
||||
async revertHistoryEntry(data: ObjectWithId): Promise<MultipleEvents<SocketEvents<SocketVersion.V3>>> {
|
||||
const obj = await this._emit("revertHistoryEntry", data);
|
||||
this._set(this.data, 'history', {});
|
||||
this._receiveMultiple(obj);
|
||||
|
@ -483,7 +485,7 @@ class Client {
|
|||
return await this._emit("find", data);
|
||||
}
|
||||
|
||||
async findOnMap(data: FindOnMapQuery): Promise<SocketResponse<SocketVersion.V2, 'findOnMap'>> {
|
||||
async findOnMap(data: FindOnMapQuery): Promise<SocketResponse<SocketVersion.V3, 'findOnMap'>> {
|
||||
return await this._emit("findOnMap", data);
|
||||
}
|
||||
|
||||
|
@ -580,7 +582,7 @@ class Client {
|
|||
this.socket.disconnect();
|
||||
}
|
||||
|
||||
private async _setPadId(padId: string): Promise<MultipleEvents<SocketEvents<SocketVersion.V2>>> {
|
||||
private async _setPadId(padId: string): Promise<MultipleEvents<SocketEvents<SocketVersion.V3>>> {
|
||||
this._set(this.state, 'serverError', undefined);
|
||||
this._set(this.state, 'padId', padId);
|
||||
try {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "facilmap-docs",
|
||||
"version": "4.1.1",
|
||||
"version": "5.0.0",
|
||||
"description": "Documentation for FacilMap.",
|
||||
"author": "Candid Dauth <cdauth@cdauth.eu>",
|
||||
"repository": {
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
The websocket on the FacilMap server provides different API versions (implemented as socket.io namespaces such as `/` for version 1, `/v2` for version 2, etc.) in order to stay backwards compatible with older clients. Each release of facilmap-client is adapted to a particular API version of the server. When upgrading to a new version of the client, have a look at this page to find out what has changed.
|
||||
|
||||
## v5.0.0 (API v3)
|
||||
|
||||
* “symbol” was renamed to “icon” everywhere. This applies to `Marker.symbol`, `Type.defaultSymbol`, `Type.symbolFixed`, `Type.fields[].controlSymbol` and `Type.fields[].options[].symbol`.
|
||||
|
||||
## v4.0.0 (API v2)
|
||||
|
||||
* Before, creating a map with an empty name resulted in `padData.name` set to `"Unnamed map"`. Now, an empty name will result in `""` and the UI is responsible for displaying that in an appropriate way.
|
||||
|
|
|
@ -20,7 +20,7 @@ A bounding box that describes which part of the map the user is currently viewin
|
|||
* `name` (string): The name of this marker
|
||||
* `colour` (string): The colour of this marker as a 6-digit hex value, for example `ff0000`
|
||||
* `size` (number, min: 15): The height of the marker in pixels
|
||||
* `symbol` (string): The symbol name for the marker. Default is an empty string.
|
||||
* `icon` (string): The icon name for the marker. Default is an empty string.
|
||||
* `shape` (string): The shape name for the marker. Default is an empty string (equivalent to `"drop"`).
|
||||
* `ele` (number or null): The elevation of this marker in metres (set by the server)
|
||||
* `typeId` (number): The ID of the type of this marker
|
||||
|
@ -110,20 +110,20 @@ their `idx` property.
|
|||
* `name` (string): The name of this type. Note that the if the name is "Marker" or "Line", the FacilMap UI will translate the name to other languages even though the underlying name is in English.
|
||||
* `type` (string): `marker` or `line`
|
||||
* `idx` (number): The sorting position of this type. When a list of types is shown to the user, it must be ordered by this value. If types were deleted or reordered, there may be gaps in the sequence of indexes, but no two types on the same map can ever have the same index. When setting this as part of a type creation/update, other types with a same/higher index will have their index increased to be moved down the list.
|
||||
* `defaultColour`, `defaultSize`, `defaultSymbol`, `defaultShape`, `defaultWidth`, `defaultStroke`, `defaultMode` (string/number): Default values for the
|
||||
* `defaultColour`, `defaultSize`, `defaultIcon`, `defaultShape`, `defaultWidth`, `defaultStroke`, `defaultMode` (string/number): Default values for the
|
||||
different object properties
|
||||
* `colourFixed`, `sizeFixed`, `symbolFixed`, `shapeFixed`, `widthFixed`, `strokeFixed`, `modeFixed` (boolean): Whether those values are fixed and
|
||||
* `colourFixed`, `sizeFixed`, `iconFixed`, `shapeFixed`, `widthFixed`, `strokeFixed`, `modeFixed` (boolean): Whether those values are fixed and
|
||||
cannot be changed for an individual object
|
||||
* `fields` ([object]): The form fields for this type. Each field has the following properties:
|
||||
* `name` (string): The name of the field. This is at the same time the key in the `data` properties of markers and lines. Note that the if the name is "Description", the FacilMap UI will translate the name to other languages even though the underlying name is in English.
|
||||
* `oldName` (string): When renaming a field (using [`editType(data)`](./methods.md#edittype-data)), specify the former name here
|
||||
* `type` (string): The type of field, one of `textarea`, `dropdown`, `checkbox`, `input`
|
||||
* `controlColour`, `controlSize`, `controlSymbol`, `controlShape`, `controlWidth`, `controlStroke` (boolean): If this field is a dropdown, whether the different options set a specific property on the object
|
||||
* `controlColour`, `controlSize`, `controlIcon`, `controlShape`, `controlWidth`, `controlStroke` (boolean): If this field is a dropdown, whether the different options set a specific property on the object
|
||||
* `default` (string/boolean): The default value of this field
|
||||
* `options` ([object]): If this field is a dropdown or a checkbox, an array of objects with the following properties. For a checkbox, the array has to have 2 items, the first representing the unchecked and the second the checked state.
|
||||
* `value` (string): The value of this option.
|
||||
* `oldValue` (string): When renaming a dropdown option (using [`editType(data)`](./methods.md#edittype-data)), specify the former value here
|
||||
* `colour`, `size`, `shape`, `symbol`, `width`, `stroke` (string/number): The property value if this field controls that property
|
||||
* `colour`, `size`, `shape`, `icon`, `width`, `stroke` (string/number): The property value if this field controls that property
|
||||
|
||||
## SearchResult
|
||||
|
||||
|
@ -135,7 +135,7 @@ their `idx` property.
|
|||
* `zoom` (number): Zoom level at which there is a good view onto the result. Might be null if `boundingbox` is set.
|
||||
* `extratags` (object): Extra OSM tags that might be useful
|
||||
* `geojson` (object): GeoJSON if the result has a shape
|
||||
* `icon` (string): Symbol key of the result
|
||||
* `icon` (string): Icon key of the result
|
||||
* `type` (string): Type of the result
|
||||
* `id` (string): If the result is an OSM object, the ID of the OSM object, prefixed by `n` (node), `w` (way) or `r` (relation)
|
||||
* `ele` (number): Elevation in meters
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Icons
|
||||
|
||||
FacilMap comes with a large selection of icons (called “symbols” in the code) and marker shapes. The icons come from the following sources:
|
||||
FacilMap comes with a large selection of icons and marker shapes. The icons come from the following sources:
|
||||
* All the [Open SVG Map Icons](https://github.com/twain47/Open-SVG-Map-Icons/) (these are the ones used by Nominatim for search results)
|
||||
* A selection of [Glyphicons](https://getbootstrap.com/docs/3.4/components/#glyphicons-glyphs) from Bootstrap 3.
|
||||
* A few icons from [Material Design Iconic Font](https://zavoloklom.github.io/material-design-iconic-font/).
|
||||
|
@ -10,45 +10,45 @@ FacilMap uses these icons as part of markers on the map and in regular UI elemen
|
|||
|
||||
facilmap-leaflet includes all the icons and marker shapes and provides some helper methods to access them in different sizes and styles.
|
||||
|
||||
To make the bundle size smaller, the symbols are separated into two sets:
|
||||
* The *core* symbols are included in the main facilmap-leaflet bundle. This includes all all symbols that are used by FacilMap as UI elements.
|
||||
* The *extra* symbols are included in a separate file. When you call any of the methods below for the first time for an extra symbol, this separate file is loaded using an async import. You can also explicitly load the extra symbols at any point of time by calling `preloadExtraSymbols()`.
|
||||
To make the bundle size smaller, the icons are separated into two sets:
|
||||
* The *core* icons are included in the main facilmap-leaflet bundle. This includes all all icons that are used by FacilMap as UI elements.
|
||||
* The *extra* icons are included in a separate file. When you call any of the methods below for the first time for an extra icon, this separate file is loaded using an async import. You can also explicitly load the extra icons at any point of time by calling `preloadExtraIcons()`.
|
||||
|
||||
## Available symbols and shapes
|
||||
## Available icons and shapes
|
||||
|
||||
`symbolList` and `shapeList` are arrays of strings that contain the names of all the available symbols (core and extra) and marker shapes. The `coreSymbolList` array contains only the names of the core symbols.
|
||||
`iconList` and `shapeList` are arrays of strings that contain the names of all the available icons (core and extra) and marker shapes. The `coreIconList` array contains only the names of the core icons.
|
||||
|
||||
In addition to the symbols listed in `symbolList`, any single character can be used as a symbol. Single-character symbols are rendered in the browser, they don’t require loading the extra symbols.
|
||||
In addition to the icons listed in `iconList`, any single character can be used as an icon. Single-character icons are rendered in the browser, they don’t require loading the extra icons.
|
||||
|
||||
## Get a symbol
|
||||
## Get an icon
|
||||
|
||||
The following methods returns a simple monochrome icon.
|
||||
|
||||
* `async getSymbolCode(colour, size, symbol)`: Returns a raw SVG object with the code of the symbol as a string.
|
||||
* `async getSymbolUrl(colour, size, symbol)`: Returns the symbol as a `data:` URL (that can be used as the `src` of an `img` for example)
|
||||
* `async getSymbolHtml(colour, size, symbol)`: Returns the symbol as an SVG element source code (as a string) that can be embedded straight into a HTML page.
|
||||
* `async getIconCode(colour, size, icon)`: Returns a raw SVG object with the code of the icon as a string.
|
||||
* `async getIconUrl(colour, size, icon)`: Returns the icon as a `data:` URL (that can be used as the `src` of an `img` for example)
|
||||
* `async getIconHtml(colour, size, icon)`: Returns the icon as an SVG element source code (as a string) that can be embedded straight into a HTML page.
|
||||
|
||||
The following arguments are expected:
|
||||
* `colour`: Any colour that would be acceptable in SVG, for example `#000000` or `currentColor`.
|
||||
* `size`: The height/width in pixels that the symbol should have (symbols are square). For `getSymbolHtml()`, the size can also be a string (for example `1em`).
|
||||
* `symbol`: Either one of the symbol name that is listed in `symbolList`, or a single letter, or an empty string or undefined to render the default symbol (a dot).
|
||||
* `size`: The height/width in pixels that the icon should have (icons are square). For `getIconHtml()`, the size can also be a string (for example `1em`).
|
||||
* `icon`: Either one of the icon name that is listed in `iconList`, or a single letter, or an empty string or undefined to render the default icon (a dot).
|
||||
|
||||
## Get a marker icon
|
||||
|
||||
The following methods returns a marker icon with the specified shape and the specified symbol inside.
|
||||
The following methods returns a marker icon with the specified shape and the specified icon inside.
|
||||
|
||||
* `async getMarkerCode(colour, height, symbol, shape, highlight)`: Returns a raw SVG object with the code of the marker as a string.
|
||||
* `async getMarkerUrl(colour, height, symbol, shape, highlight)`: Returns the marker as a `data:` URL (that can be used as the `src` of an `img` for example)
|
||||
* `async getMarkerHtml(colour, height, symbol, shape, highlight)`: Returns the marker as an SVG element source code (as a string) that can be embedded straight into a HTML page.
|
||||
* `getMarkerIcon(colour, height, symbol, shape, highlight)`: Returns the marker as a [Leaflet Icon](https://leafletjs.com/reference.html#icon) that can be used for Leaflet markers. The anchor point is set correctly. The Icon object is returned synchronously and updates its `src` automatically as soon as it is loaded.
|
||||
* `async getMarkerCode(colour, height, icon, shape, highlight)`: Returns a raw SVG object with the code of the marker as a string.
|
||||
* `async getMarkerUrl(colour, height, icon, shape, highlight)`: Returns the marker as a `data:` URL (that can be used as the `src` of an `img` for example)
|
||||
* `async getMarkerHtml(colour, height, icon, shape, highlight)`: Returns the marker as an SVG element source code (as a string) that can be embedded straight into a HTML page.
|
||||
* `getMarkerIcon(colour, height, icon, shape, highlight)`: Returns the marker as a [Leaflet Icon](https://leafletjs.com/reference.html#icon) that can be used for Leaflet markers. The anchor point is set correctly. The Icon object is returned synchronously and updates its `src` automatically as soon as it is loaded.
|
||||
|
||||
The following arguments are expected:
|
||||
* `colour`: A colour in hex format, for example `#000000`.
|
||||
* `height`: The height of the marker in pixels. Different marker shapes have different aspect ratios, so the width will differ depending on which shape is used. Note that the height is approximate, it is scaled down for some shapes with the aim that two markers with different shapes but the same `height` should visually appear roughly the same size.
|
||||
* `symbol`: Either one of the symbol name that is listed in `symbolList`, or a single letter, or an empty string or undefined to render the default symbol (a dot).
|
||||
* `icon`: Either one of the icon name that is listed in `iconList`, or a single letter, or an empty string or undefined to render the default icon (a dot).
|
||||
* `shape`: A shape name that is listed in `shapeList`, or an empty string or undefined to render the default shape (`drop`).
|
||||
* `highlight`: If this is set to true, the marker is rendered as highlighted (with an increased border width).
|
||||
|
||||
## Get a symbol for an OSM element
|
||||
## Get an icon for an OSM element
|
||||
|
||||
Calling `getSymbolForTags(tags)` will make a best attempt to return a symbol that is appropriate for a given OSM element. `tags` should be an object that maps OSM tag keys to values. The function returns a string representing the name of a symbol. If no fitting symbol can be found, an empty string is returned.
|
||||
Calling `getIconForTags(tags)` will make a best attempt to return an icon that is appropriate for a given OSM element. `tags` should be an object that maps OSM tag keys to values. The function returns a string representing the name of an icon. If no fitting icon can be found, an empty string is returned.
|
|
@ -55,7 +55,7 @@ If you want to make sure that a marker with a particular ID is shown (regardless
|
|||
`MarkerLayer` (in singular, as opposed to `MarkersLayer`) makes it possible to render an individual marker object with its appropriate style. It does not automatically update when the marker changes and can also be used for markers that are not saved on the map.
|
||||
|
||||
`MarkerLayer` is based on regular [Leaflet markers](https://leafletjs.com/reference.html#marker), but accepts the following additional options:
|
||||
* `marker`: A marker object that the marker style will be based on. Only the properties relevant for the style (`colour`, `size`, `symbol` and `shape`) need to be set.
|
||||
* `marker`: A marker object that the marker style will be based on. Only the properties relevant for the style (`colour`, `size`, `icon` and `shape`) need to be set.
|
||||
* `highlight`: If this is `true`, the marker will be shown with a thicker border.
|
||||
* `raised`: If this is `true`, the marker will be rendered above other map objects.
|
||||
|
||||
|
@ -65,7 +65,7 @@ import { MarkerLayer } from "facilmap-leaflet";
|
|||
|
||||
const map = L.map('map');
|
||||
new MarkerLayer([52.5295, 13.3840], {
|
||||
marker: { colour: "00ff00", size: 40, symbol: "alert", shape: "pentagon" }
|
||||
marker: { colour: "00ff00", size: 40, icon: "alert", shape: "pentagon" }
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
* `markerShape`: The shape of search result markers, defaults to `drop`
|
||||
* `pathOptions`: [Path options](https://leafletjs.com/reference.html#path-option) for lines and polygons.
|
||||
|
||||
Note that the marker symbol is determined by the type of result.
|
||||
Note that the marker icon is determined by the type of result.
|
||||
|
||||
Example usage:
|
||||
```javascript
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "facilmap-frontend",
|
||||
"version": "4.1.1",
|
||||
"version": "5.0.0",
|
||||
"description": "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration.",
|
||||
"keywords": [
|
||||
"maps",
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
"lon-lat-description": "Koordinaten des Markers",
|
||||
"colour-description": "Farbe des Markers oder der Linie",
|
||||
"size-description": "Größe des Markers",
|
||||
"symbol-description": "Symbol des Markers",
|
||||
"icon-description": "Symbol des Markers",
|
||||
"shape-description": "Form des Markers",
|
||||
"ele-description": "Höhe über NN des Markers",
|
||||
"mode-description": "Routenmodus der Linie (z. B. {{straight}} (Luftlinie) / {{car}} (Auto) / {{bicycle}} (Fahrrad) / {{pedestrian}} (zu Fuß) / {{track}} (importierter GPX-Track))",
|
||||
|
@ -715,7 +715,7 @@
|
|||
"dashed": "Gestrichelt",
|
||||
"dotted": "Gepunktet"
|
||||
},
|
||||
"symbol-picker": {
|
||||
"icon-picker": {
|
||||
"unknown-icon-error": "Unbekanntes Symbol",
|
||||
"filter-placeholder": "Filtern",
|
||||
"no-icons-found-error": "Es konnten keine Symbole gefunden werden."
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
"lon-lat-description": "Marker coordinates",
|
||||
"colour-description": "Marker/line colour",
|
||||
"size-description": "Marker size",
|
||||
"symbol-description": "Marker icon",
|
||||
"icon-description": "Marker icon",
|
||||
"shape-description": "Marker shape",
|
||||
"ele-description": "Marker elevation",
|
||||
"mode-description": "Line routing mode ({{straight}} / {{car}} / {{bicycle}} / {{pedestrian}} / {{track}})",
|
||||
|
@ -717,7 +717,7 @@
|
|||
"dashed": "Dashed",
|
||||
"dotted": "Dotted"
|
||||
},
|
||||
"symbol-picker": {
|
||||
"icon-picker": {
|
||||
"unknown-icon-error": "Unknown icon",
|
||||
"filter-placeholder": "Filter",
|
||||
"no-icons-found-error": "No icons could be found."
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"apply": "Bruk",
|
||||
"typeId-description-separator": " / ",
|
||||
"size-description": "Pekerstørrelse",
|
||||
"symbol-description": "Pekerikon",
|
||||
"icon-description": "Pekerikon",
|
||||
"shape-description": "Pekerform",
|
||||
"width-description": "Linjebredde"
|
||||
},
|
||||
|
|
|
@ -166,9 +166,9 @@
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><code>symbol</code></td>
|
||||
<td>{{i18n.t("edit-filter-dialog.symbol-description")}}</td>
|
||||
<td><code>symbol == "accommodation_camping"</code></td>
|
||||
<td><code>icon</code></td>
|
||||
<td>{{i18n.t("edit-filter-dialog.icon-description")}}</td>
|
||||
<td><code>icon == "accommodation_camping"</code></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import { cloneDeep, isEqual } from "lodash-es";
|
||||
import ModalDialog from "./ui/modal-dialog.vue";
|
||||
import ColourPicker from "./ui/colour-picker.vue";
|
||||
import SymbolPicker from "./ui/symbol-picker.vue";
|
||||
import IconPicker from "./ui/icon-picker.vue";
|
||||
import ShapePicker from "./ui/shape-picker.vue";
|
||||
import FieldInput from "./ui/field-input.vue";
|
||||
import SizePicker from "./ui/size-picker.vue";
|
||||
|
@ -118,11 +118,11 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="resolvedCanControl.includes('symbol')">
|
||||
<template v-if="resolvedCanControl.includes('icon')">
|
||||
<div class="row mb-3">
|
||||
<label :for="`${id}-symbol-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-marker-dialog.icon")}}</label>
|
||||
<label :for="`${id}-icon-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-marker-dialog.icon")}}</label>
|
||||
<div class="col-sm-9">
|
||||
<SymbolPicker :id="`${id}-symbol-input`" v-model="marker.symbol"></SymbolPicker>
|
||||
<IconPicker :id="`${id}-icon-input`" v-model="marker.icon"></IconPicker>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { useToasts } from "../ui/toasts/toasts.vue";
|
||||
import ColourPicker from "../ui/colour-picker.vue";
|
||||
import ShapePicker from "../ui/shape-picker.vue";
|
||||
import SymbolPicker from "../ui/symbol-picker.vue";
|
||||
import IconPicker from "../ui/icon-picker.vue";
|
||||
import RouteMode from "../ui/route-mode.vue";
|
||||
import Draggable from "vuedraggable";
|
||||
import FieldInput from "../ui/field-input.vue";
|
||||
|
@ -252,26 +252,26 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="resolvedCanControl.includes('symbol')">
|
||||
<template v-if="resolvedCanControl.includes('icon')">
|
||||
<div class="row mb-3">
|
||||
<label :for="`${id}-default-symbol-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-icon")}}</label>
|
||||
<label :for="`${id}-default-icon-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-icon")}}</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-sm-9">
|
||||
<SymbolPicker
|
||||
:id="`${id}-default-symbol-input`"
|
||||
v-model="type.defaultSymbol"
|
||||
></SymbolPicker>
|
||||
<IconPicker
|
||||
:id="`${id}-default-icon-input`"
|
||||
v-model="type.defaultIcon"
|
||||
></IconPicker>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
:id="`${id}-default-symbol-fixed`"
|
||||
v-model="type.symbolFixed"
|
||||
:id="`${id}-default-icon-fixed`"
|
||||
v-model="type.iconFixed"
|
||||
/>
|
||||
<label :for="`${id}-default-symbol-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
|
||||
<label :for="`${id}-default-icon-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ModalDialog from "../ui/modal-dialog.vue";
|
||||
import ShapePicker from "../ui/shape-picker.vue";
|
||||
import SizePicker from "../ui/size-picker.vue";
|
||||
import SymbolPicker from "../ui/symbol-picker.vue";
|
||||
import IconPicker from "../ui/icon-picker.vue";
|
||||
import WidthPicker from "../ui/width-picker.vue";
|
||||
import { useToasts } from "../ui/toasts/toasts.vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
@ -24,7 +24,7 @@
|
|||
field.controlColour,
|
||||
...(type.type == "marker" ? [
|
||||
field.controlSize,
|
||||
field.controlSymbol,
|
||||
field.controlIcon,
|
||||
field.controlShape
|
||||
] : []),
|
||||
...(type.type == "line" ? [
|
||||
|
@ -196,15 +196,15 @@
|
|||
|
||||
<div v-if="type.type == 'marker'" class="form-check">
|
||||
<input
|
||||
:id="`${id}-control-symbol`"
|
||||
:id="`${id}-control-icon`"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model="fieldValue.controlSymbol"
|
||||
:disabled="!resolvedCanControl.includes('symbol')"
|
||||
v-model="fieldValue.controlIcon"
|
||||
:disabled="!resolvedCanControl.includes('icon')"
|
||||
/>
|
||||
<label
|
||||
class="form-check-label"
|
||||
:for="`${id}-control-symbol`"
|
||||
:for="`${id}-control-icon`"
|
||||
>
|
||||
{{i18n.t("edit-type-dropdown-dialog.control-icon", { type: typeInterpolation })}}
|
||||
</label>
|
||||
|
@ -266,7 +266,7 @@
|
|||
<th v-if="fieldValue.type == 'checkbox'">{{i18n.t("edit-type-dropdown-dialog.label")}}</th>
|
||||
<th v-if="fieldValue.controlColour">{{i18n.t("edit-type-dropdown-dialog.colour")}}</th>
|
||||
<th v-if="fieldValue.controlSize">{{i18n.t("edit-type-dropdown-dialog.size")}}</th>
|
||||
<th v-if="fieldValue.controlSymbol">{{i18n.t("edit-type-dropdown-dialog.icon")}}</th>
|
||||
<th v-if="fieldValue.controlIcon">{{i18n.t("edit-type-dropdown-dialog.icon")}}</th>
|
||||
<th v-if="fieldValue.controlShape">{{i18n.t("edit-type-dropdown-dialog.shape")}}</th>
|
||||
<th v-if="fieldValue.controlWidth">{{i18n.t("edit-type-dropdown-dialog.width")}}</th>
|
||||
<th v-if="fieldValue.controlStroke">{{i18n.t("edit-type-dropdown-dialog.stroke")}}</th>
|
||||
|
@ -315,11 +315,11 @@
|
|||
class="fm-custom-range-with-label"
|
||||
></SizePicker>
|
||||
</td>
|
||||
<td v-if="fieldValue.controlSymbol" class="field">
|
||||
<SymbolPicker
|
||||
:modelValue="option.symbol ?? type.defaultSymbol"
|
||||
@update:modelValue="option.symbol = $event"
|
||||
></SymbolPicker>
|
||||
<td v-if="fieldValue.controlIcon" class="field">
|
||||
<IconPicker
|
||||
:modelValue="option.icon ?? type.defaultIcon"
|
||||
@update:modelValue="option.icon = $event"
|
||||
></IconPicker>
|
||||
</td>
|
||||
<td v-if="fieldValue.controlShape" class="field">
|
||||
<ShapePicker
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import FacilMapContextProvider from "./facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import type { FacilMapSettings } from "./facil-map-context-provider/facil-map-context";
|
||||
import ClientProvider from "./client-provider.vue";
|
||||
import { preloadExtraSymbols } from "facilmap-leaflet";
|
||||
import { preloadExtraIcons } from "facilmap-leaflet";
|
||||
|
||||
const props = defineProps<{
|
||||
baseUrl: string;
|
||||
|
@ -59,7 +59,7 @@
|
|||
context
|
||||
});
|
||||
|
||||
preloadExtraSymbols().catch((err) => {
|
||||
preloadExtraIcons().catch((err) => {
|
||||
console.error("Error preloading extra icons", err);
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { type Ref, ref, watch, markRaw, reactive, watchEffect, shallowRef, shallowReadonly, type Raw, nextTick } from "vue";
|
||||
import { type Control, latLng, latLngBounds, type Map, map as leafletMap, DomUtil, control } from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { BboxHandler, getSymbolHtml, getVisibleLayers, HashHandler, LinesLayer, MarkersLayer, SearchResultsLayer, OverpassLayer, OverpassLoadStatus, displayView, getInitialView, coreSymbolList } from "facilmap-leaflet";
|
||||
import { BboxHandler, getIconHtml, getVisibleLayers, HashHandler, LinesLayer, MarkersLayer, SearchResultsLayer, OverpassLayer, OverpassLoadStatus, displayView, getInitialView, coreIconList } from "facilmap-leaflet";
|
||||
import "leaflet.locatecontrol";
|
||||
import "leaflet.locatecontrol/dist/L.Control.Locate.css";
|
||||
import "leaflet-graphicscale";
|
||||
|
@ -179,11 +179,11 @@ function useLocateControl(map: Ref<Map>, context: FacilMapContext): Ref<Raw<Cont
|
|||
if (locateControl) {
|
||||
locateControl.addTo(map.value);
|
||||
|
||||
if (!coreSymbolList.includes("screenshot")) {
|
||||
if (!coreIconList.includes("screenshot")) {
|
||||
console.warn(`Icon "screenshot" is not in core icons.`);
|
||||
}
|
||||
|
||||
getSymbolHtml("currentColor", "1.5em", "screenshot").then((html) => {
|
||||
getIconHtml("currentColor", "1.5em", "screenshot").then((html) => {
|
||||
locateControl._container.querySelector("a")?.insertAdjacentHTML("beforeend", html);
|
||||
}).catch((err) => {
|
||||
console.error("Error loading locate control icon", err);
|
||||
|
@ -454,14 +454,14 @@ export async function useMapContext(context: FacilMapContext, mapRef: Ref<HTMLEl
|
|||
return mapContext;
|
||||
}
|
||||
|
||||
/* function createButton(symbol: string, onClick: () => void): Control {
|
||||
/* function createButton(icon: Icon, onClick: () => void): Control {
|
||||
return Object.assign(new Control(), {
|
||||
onAdd() {
|
||||
const div = document.createElement('div');
|
||||
div.className = "leaflet-bar";
|
||||
const a = document.createElement('a');
|
||||
a.href = "javascript:";
|
||||
a.innerHTML = createSymbolHtml("currentColor", "1.5em", symbol);
|
||||
a.innerHTML = createIconHtml("currentColor", "1.5em", icon);
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { getMarkerHtml, getSymbolHtml } from "facilmap-leaflet";
|
||||
import { getMarkerHtml, getIconHtml } from "facilmap-leaflet";
|
||||
import { makeTypeFilter, markdownBlock } from "facilmap-utils";
|
||||
import type { LegendItem, LegendType } from "./legend-utils";
|
||||
import { createLinePlaceholderHtml } from "../../utils/ui";
|
||||
|
@ -54,13 +54,13 @@
|
|||
mapContext.value.components.map.setFmFilter(makeTypeFilter(mapContext.value.components.map.fmFilter, typeInfo.typeId, filters));
|
||||
}
|
||||
|
||||
async function makeSymbol(typeInfo: LegendType, item: LegendItem, height = 15): Promise<string> {
|
||||
async function makeIcon(typeInfo: LegendType, item: LegendItem, height = 15): Promise<string> {
|
||||
if(typeInfo.type == "line")
|
||||
return createLinePlaceholderHtml(item.colour || "rainbow", item.width || 5, 50, item.stroke ?? "");
|
||||
else if (item.colour || item.shape != null)
|
||||
return await getMarkerHtml(item.colour || "rainbow", height, item.symbol, item.shape);
|
||||
return await getMarkerHtml(item.colour || "rainbow", height, item.icon, item.shape);
|
||||
else
|
||||
return await getSymbolHtml("#000000", height, item.symbol);
|
||||
return await getIconHtml("#000000", height, item.icon);
|
||||
}
|
||||
|
||||
function togglePopover(itemKey: string, show: boolean) {
|
||||
|
@ -84,9 +84,9 @@
|
|||
<dl>
|
||||
<template v-for="(item, idx) in type.items" :key="item.key">
|
||||
<dt
|
||||
:class="[ 'fm-legend-symbol', 'fm-' + type.type, { filtered: item.filtered, first: (item.first && idx !== 0), bright: item.bright } ]"
|
||||
:class="[ 'fm-legend-icon', 'fm-' + type.type, { filtered: item.filtered, first: (item.first && idx !== 0), bright: item.bright } ]"
|
||||
@click="toggleFilter(type, item)"
|
||||
v-html-async="makeSymbol(type, item)"
|
||||
v-html-async="makeIcon(type, item)"
|
||||
@mouseenter="togglePopover(item.key, true)"
|
||||
@mouseleave="togglePopover(item.key, false)"
|
||||
:ref="mapRef(itemIconRefs, item.key)"
|
||||
|
@ -112,14 +112,14 @@
|
|||
>
|
||||
<div
|
||||
:class="[
|
||||
'fm-legend-symbol',
|
||||
'fm-legend-icon',
|
||||
`fm-${type.type}`,
|
||||
{
|
||||
filtered: item.filtered,
|
||||
bright: item.bright
|
||||
}
|
||||
]"
|
||||
v-html-async="makeSymbol(type, item, 40)"
|
||||
v-html-async="makeIcon(type, item, 40)"
|
||||
></div>
|
||||
<p>
|
||||
<span class="text-break" :style="item.strikethrough ? {'text-decoration': 'line-through'} : {}">{{item.label}}</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ID, Shape, Stroke, Symbol, Type } from "facilmap-types";
|
||||
import { symbolList } from "facilmap-leaflet";
|
||||
import type { ID, Shape, Stroke, Icon, Type } from "facilmap-types";
|
||||
import { iconList } from "facilmap-leaflet";
|
||||
import { formatTypeName, getOrderedTypes, isBright } from "facilmap-utils";
|
||||
import type { FacilMapContext } from "../facil-map-context-provider/facil-map-context";
|
||||
import { requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
|
@ -22,7 +22,7 @@ export interface LegendItem {
|
|||
first?: boolean;
|
||||
strikethrough?: boolean;
|
||||
colour?: string;
|
||||
symbol?: Symbol;
|
||||
icon?: Icon;
|
||||
shape?: Shape;
|
||||
width?: number;
|
||||
stroke?: Stroke;
|
||||
|
@ -43,7 +43,7 @@ export function getLegendItems(context: FacilMapContext): LegendType[] {
|
|||
|
||||
if (
|
||||
type.colourFixed ||
|
||||
(type.type == "marker" && type.symbolFixed && type.defaultSymbol && (symbolList.includes(type.defaultSymbol) || type.defaultSymbol.length == 1)) ||
|
||||
(type.type == "marker" && type.iconFixed && type.defaultIcon && (iconList.includes(type.defaultIcon) || type.defaultIcon.length == 1)) ||
|
||||
(type.type == "marker" && type.shapeFixed) ||
|
||||
(type.type == "line" && type.widthFixed) ||
|
||||
(type.type === "line" && type.strokeFixed)
|
||||
|
@ -58,8 +58,8 @@ export function getLegendItems(context: FacilMapContext): LegendType[] {
|
|||
if (type.colourFixed) {
|
||||
item.colour = type.defaultColour ? `#${type.defaultColour}` : undefined;
|
||||
}
|
||||
if (type.type == "marker" && type.symbolFixed && type.defaultSymbol && (symbolList.includes(type.defaultSymbol) || type.defaultSymbol.length == 1)) {
|
||||
item.symbol = type.defaultSymbol;
|
||||
if (type.type == "marker" && type.iconFixed && type.defaultIcon && (iconList.includes(type.defaultIcon) || type.defaultIcon.length == 1)) {
|
||||
item.icon = type.defaultIcon;
|
||||
}
|
||||
if (type.type == "marker" && type.shapeFixed) {
|
||||
item.shape = type.defaultShape ?? "";
|
||||
|
@ -82,7 +82,7 @@ export function getLegendItems(context: FacilMapContext): LegendType[] {
|
|||
(field.type != "dropdown" && field.type != "checkbox") ||
|
||||
(
|
||||
!field.controlColour &&
|
||||
!(type.type === "marker" && field.controlSymbol) &&
|
||||
!(type.type === "marker" && field.controlIcon) &&
|
||||
!(type.type === "marker" && field.controlShape) &&
|
||||
!(type.type === "line" && field.controlWidth) &&
|
||||
!(type.type === "line" && field.controlStroke)
|
||||
|
@ -119,10 +119,10 @@ export function getLegendItems(context: FacilMapContext): LegendType[] {
|
|||
}
|
||||
|
||||
if (type.type == "marker") {
|
||||
if (field.controlSymbol) {
|
||||
item.symbol = option.symbol ?? type.defaultSymbol;
|
||||
} else if (type.symbolFixed) {
|
||||
item.symbol = type.defaultSymbol;
|
||||
if (field.controlIcon) {
|
||||
item.icon = option.icon ?? type.defaultIcon;
|
||||
} else if (type.iconFixed) {
|
||||
item.icon = type.defaultIcon;
|
||||
}
|
||||
|
||||
if (field.controlShape) {
|
||||
|
|
|
@ -366,7 +366,7 @@
|
|||
marker: {
|
||||
colour: dragMarkerColour,
|
||||
size: 35,
|
||||
symbol: "",
|
||||
icon: "",
|
||||
shape: "drop"
|
||||
}
|
||||
})).addTo(mapContext.value.components.map));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { getSymbolHtml, symbolList } from "facilmap-leaflet";
|
||||
import { getIconHtml, iconList } from "facilmap-leaflet";
|
||||
import Icon from "./icon.vue";
|
||||
import Picker from "./picker.vue";
|
||||
import { arrowNavigation } from "../../utils/ui";
|
||||
|
@ -12,8 +12,8 @@
|
|||
let allItemsP: Promise<Record<string, string>>;
|
||||
async function getAllItems(): Promise<Record<string, string>> {
|
||||
if (!allItemsP) { // eslint-disable-line @typescript-eslint/no-misused-promises
|
||||
allItemsP = Promise.all(symbolList.map(async (s) => (
|
||||
[s, await getSymbolHtml("currentColor", "1.5em", s)] as const
|
||||
allItemsP = Promise.all(iconList.map(async (s) => (
|
||||
[s, await getIconHtml("currentColor", "1.5em", s)] as const
|
||||
))).then((l) => Object.fromEntries(l));
|
||||
}
|
||||
return await allItemsP;
|
||||
|
@ -49,14 +49,14 @@
|
|||
return Object.fromEntries<string>((await Promise.all([
|
||||
(async (): Promise<Array<[string, string]>> => {
|
||||
if (filter.value.length == 1) {
|
||||
return [[filter.value, await getSymbolHtml("currentColor", "1.5em", filter.value)]];
|
||||
return [[filter.value, await getIconHtml("currentColor", "1.5em", filter.value)]];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
(async (): Promise<Array<[string, string]>> => {
|
||||
if (props.modelValue?.length == 1 && props.modelValue != filter.value) {
|
||||
return [[props.modelValue, await getSymbolHtml("currentColor", "1.5em", props.modelValue)]];
|
||||
return [[props.modelValue, await getIconHtml("currentColor", "1.5em", props.modelValue)]];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
@ -75,7 +75,7 @@
|
|||
});
|
||||
|
||||
function validateSymbol(symbol: string) {
|
||||
if (symbol && symbol.length !== 1 && !symbolList.includes(symbol)) {
|
||||
if (symbol && symbol.length !== 1 && !iconList.includes(symbol)) {
|
||||
return i18n.t("symbol-picker.unknown-icon-error");
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { coreSymbolList, getSymbolHtml } from "facilmap-leaflet";
|
||||
import { coreIconList, getIconHtml } from "facilmap-leaflet";
|
||||
import { vHtmlAsync } from "../../utils/vue";
|
||||
import { computed, watchEffect } from "vue";
|
||||
|
||||
|
@ -13,12 +13,12 @@
|
|||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.icon && !props.async && !coreSymbolList.includes(props.icon)) {
|
||||
if (props.icon && !props.async && !coreIconList.includes(props.icon)) {
|
||||
console.warn(`Icon "${props.icon}" is not in core icons.`);
|
||||
}
|
||||
});
|
||||
|
||||
const iconCodeP = computed(() => getSymbolHtml("currentColor", props.size, props.icon));
|
||||
const iconCodeP = computed(() => getIconHtml("currentColor", props.size, props.icon));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -100,7 +100,7 @@ export { default as ShapePicker } from "./components/ui/shape-picker.vue";
|
|||
export { default as Sidebar } from "./components/ui/sidebar.vue";
|
||||
export { default as SizePicker } from "./components/ui/size-picker.vue";
|
||||
export { default as StrokePicker } from "./components/ui/stroke-picker.vue";
|
||||
export { default as SymbolPicker } from "./components/ui/symbol-picker.vue";
|
||||
export { default as IconPicker } from "./components/ui/icon-picker.vue";
|
||||
export { default as UseAsDropdown } from "./components/ui/use-as-dropdown.vue";
|
||||
export { default as WidthPicker } from "./components/ui/width-picker.vue";
|
||||
export { default as ZoomToObjectButton } from "./components/ui/zoom-to-object-button.vue";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Feature, Geometry } from "geojson";
|
||||
import type { GeoJsonExport, LineFeature, MarkerFeature, SearchResult } from "facilmap-types";
|
||||
import { legacyV2MarkerToCurrent, legacyV2TypeToCurrent, type GeoJsonExport, type LineFeature, type MarkerFeature, type SearchResult } from "facilmap-types";
|
||||
import { flattenObject } from "facilmap-utils";
|
||||
import { getI18n } from "./i18n";
|
||||
|
||||
|
@ -71,7 +71,7 @@ export async function parseFiles(files: string[]): Promise<FileResultObject> {
|
|||
if (geojson.facilmap.types) {
|
||||
for (const i of Object.keys(geojson.facilmap.types)) {
|
||||
typeMapping[i] = nextTypeIdx++;
|
||||
ret.types[typeMapping[i]] = geojson.facilmap.types[i];
|
||||
ret.types[typeMapping[i]] = legacyV2TypeToCurrent(geojson.facilmap.types[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ export async function parseFiles(files: string[]): Promise<FileResultObject> {
|
|||
if(geojson.facilmap) {
|
||||
if(feature.properties.typeId && typeMapping[feature.properties.typeId])
|
||||
f.fmTypeId = typeMapping[feature.properties.typeId];
|
||||
f.fmProperties = feature.properties;
|
||||
f.fmProperties = legacyV2MarkerToCurrent(feature.properties);
|
||||
}
|
||||
|
||||
ret.features.push(f);
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { FileResult, FileResultObject } from "./files";
|
|||
import type { ClientContext } from "../components/facil-map-context-provider/client-context";
|
||||
|
||||
const VIEW_KEYS: Array<keyof FileResultObject["views"][0]> = ["name", "baseLayer", "layers", "top", "bottom", "left", "right", "filter"];
|
||||
const TYPE_KEYS: Array<keyof FileResultObject["types"][0]> = ["name", "type", "defaultColour", "colourFixed", "defaultSize", "sizeFixed", "defaultSymbol", "symbolFixed", "defaultShape", "shapeFixed", "defaultWidth", "widthFixed", "defaultStroke", "strokeFixed", "defaultMode", "modeFixed", "fields"];
|
||||
const TYPE_KEYS: Array<keyof FileResultObject["types"][0]> = ["name", "type", "defaultColour", "colourFixed", "defaultSize", "sizeFixed", "defaultIcon", "iconFixed", "defaultShape", "shapeFixed", "defaultWidth", "widthFixed", "defaultStroke", "strokeFixed", "defaultMode", "modeFixed", "fields"];
|
||||
|
||||
export function isSearchResult(result: SearchResult | FindOnMapResult | FileResult): result is SearchResult {
|
||||
return !isMapResult(result) && !isFileResult(result);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "facilmap-integration-tests",
|
||||
"version": "4.1.1",
|
||||
"version": "5.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"homepage": "https://github.com/FacilMap/facilmap",
|
||||
|
@ -21,7 +21,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"facilmap-client": "workspace:^",
|
||||
"facilmap-client-v3": "patch:facilmap-client@npm%3A3#../.yarn/patches/facilmap-client-npm-3.4.0-9ca14d53cc.patch",
|
||||
"facilmap-client-v4": "npm:facilmap-client@4",
|
||||
"facilmap-types": "workspace:^",
|
||||
"facilmap-types-v3": "npm:facilmap-types@3",
|
||||
"facilmap-types-v4": "npm:facilmap-types@4",
|
||||
"facilmap-utils": "workspace:^",
|
||||
"lodash-es": "^4.17.21",
|
||||
"socket.io-client": "^4.7.5",
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, openClient, retry } from "../utils";
|
||||
import { SocketVersion, type Line } from "facilmap-types";
|
||||
|
||||
test("Socket v1 line name", async () => {
|
||||
// client1: Creates the line and has it in its bbox
|
||||
// client2: Has the line in its bbox
|
||||
// client3: Does not have the line in its bbox
|
||||
const client1 = await openClient(undefined, SocketVersion.V1);
|
||||
|
||||
await createTemporaryPad(client1, {}, async (createPadData, padData) => {
|
||||
const client2 = await openClient(padData.adminId, SocketVersion.V1);
|
||||
const client3 = await openClient(padData.adminId, SocketVersion.V1);
|
||||
|
||||
const onLine1 = vi.fn();
|
||||
client1.on("line", onLine1);
|
||||
const onLine2 = vi.fn();
|
||||
client2.on("line", onLine2);
|
||||
const onLine3 = vi.fn();
|
||||
client3.on("line", onLine3);
|
||||
|
||||
const lineType = Object.values(client1.types).find((t) => t.type === "line")!;
|
||||
|
||||
await client1.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await client2.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await client3.updateBbox({ top: 5, bottom: 0, left: 0, right: 5, zoom: 1 });
|
||||
|
||||
const line = await client1.addLine({
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id
|
||||
});
|
||||
|
||||
const expectedLine = {
|
||||
id: line.id,
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id,
|
||||
padId: padData.id,
|
||||
name: "Untitled line",
|
||||
mode: "",
|
||||
colour: "0000ff",
|
||||
width: 4,
|
||||
stroke: "",
|
||||
distance: 1247.95,
|
||||
time: null,
|
||||
ascent: null,
|
||||
descent: null,
|
||||
extraInfo: null,
|
||||
top: 14,
|
||||
right: 14,
|
||||
bottom: 6,
|
||||
left: 6,
|
||||
data: {}
|
||||
} satisfies Line;
|
||||
|
||||
expect(line).toEqual(expectedLine);
|
||||
|
||||
await retry(() => {
|
||||
expect(onLine1).toHaveBeenCalledTimes(1);
|
||||
expect(onLine2).toHaveBeenCalledTimes(1);
|
||||
expect(onLine3).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(onLine1).toHaveBeenCalledWith(expectedLine);
|
||||
expect(onLine2).toHaveBeenCalledWith(expectedLine);
|
||||
expect(onLine3).toHaveBeenCalledWith(expectedLine);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, openClient, retry } from "../utils";
|
||||
import { SocketVersion } from "facilmap-types";
|
||||
import type { LegacyV2Marker } from "facilmap-types";
|
||||
|
||||
test("Socket v1 marker name", async () => {
|
||||
// client1: Creates the marker and has it in its bbox
|
||||
// client2: Has the marker in its bbox
|
||||
// client3: Does not have the marker in its bbox
|
||||
const client1 = await openClient(undefined, SocketVersion.V1);
|
||||
|
||||
await createTemporaryPad(client1, {}, async (createPadData, padData) => {
|
||||
const client2 = await openClient(padData.adminId, SocketVersion.V1);
|
||||
const client3 = await openClient(padData.adminId, SocketVersion.V1);
|
||||
|
||||
const onMarker1 = vi.fn();
|
||||
client1.on("marker", onMarker1);
|
||||
const onMarker2 = vi.fn();
|
||||
client2.on("marker", onMarker2);
|
||||
const onMarker3 = vi.fn();
|
||||
client3.on("marker", onMarker3);
|
||||
|
||||
const markerType = Object.values(client1.types).find((t) => t.type === "marker")!;
|
||||
|
||||
await client1.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await client2.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await client3.updateBbox({ top: 5, bottom: 0, left: 0, right: 5, zoom: 1 });
|
||||
|
||||
const marker = await client1.addMarker({
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
typeId: markerType.id,
|
||||
ele: null as any
|
||||
});
|
||||
|
||||
const expectedMarker = {
|
||||
id: marker.id,
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
typeId: markerType.id,
|
||||
padId: padData.id,
|
||||
name: "Untitled marker",
|
||||
colour: "ff0000",
|
||||
size: 30,
|
||||
symbol: "",
|
||||
shape: "",
|
||||
data: {},
|
||||
ele: null
|
||||
} satisfies LegacyV2Marker;
|
||||
|
||||
expect(marker).toEqual(expectedMarker);
|
||||
|
||||
await retry(() => {
|
||||
expect(onMarker1).toHaveBeenCalledTimes(1);
|
||||
expect(onMarker2).toHaveBeenCalledTimes(1);
|
||||
expect(onMarker3).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
expect(onMarker1).toHaveBeenCalledWith(expectedMarker);
|
||||
expect(onMarker2).toHaveBeenCalledWith(expectedMarker);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, openClient } from "../utils";
|
||||
import { SocketVersion } from "facilmap-types";
|
||||
|
||||
test("Socket v1 pad name", async () => {
|
||||
const client = await openClient(undefined, SocketVersion.V1);
|
||||
|
||||
const onPadData = vi.fn();
|
||||
client.on("padData", onPadData);
|
||||
|
||||
await createTemporaryPad(client, {}, async (createPadData, padData) => {
|
||||
expect(onPadData).toBeCalledTimes(1);
|
||||
expect(onPadData.mock.calls[0][0].name).toBe("Unnamed map");
|
||||
|
||||
const result2 = await client.editPad({ name: "New name" });
|
||||
expect(result2.name).toBe("New name");
|
||||
expect(onPadData).toBeCalledTimes(2);
|
||||
expect(onPadData.mock.calls[1][0].name).toBe("New name");
|
||||
|
||||
const result3 = await client.editPad({ name: "" });
|
||||
expect(result3.name).toBe("Unnamed map");
|
||||
expect(onPadData).toBeCalledTimes(3);
|
||||
expect(onPadData.mock.calls[2][0].name).toBe("Unnamed map");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
// listenToHistory returns all past history events and transmits all future history events
|
||||
// stopListeningToHistory stops transmitting future history events
|
||||
// revertHistoryEntry reverts a marker creation
|
||||
// revertHistoryEntry reverts a marker update
|
||||
// revertHistoryEntry reverts a marker deletion, updating the ID in the history
|
||||
// revertHistoryEntry reverts a line creation
|
||||
// revertHistoryEntry reverts a line update
|
||||
// revertHistoryEntry reverts a line deletion, updating the ID in the history
|
||||
// revertHistoryEntry reverts a view creation
|
||||
// revertHistoryEntry reverts a view update
|
||||
// revertHistoryEntry reverts a view deletion, updating the ID in the history
|
||||
// revertHistoryEntry cannot revert view creation in non-admin mode
|
||||
// revertHistoryEntry cannot revert view update in non-admin mode
|
||||
// revertHistoryEntry cannot revert view deletion in non-admin mode
|
||||
// revertHistoryEntry reverts a type creation
|
||||
// revertHistoryEntry reverts a type update
|
||||
// revertHistoryEntry reverts a type deletion, updating the ID in the history
|
||||
// revertHistoryEntry cannot revert type creation in non-admin mode
|
||||
// revertHistoryEntry cannot revert type update in non-admin mode
|
||||
// revertHistoryEntry cannot revert type deletion in non-admin mode
|
||||
// revertHistoryEntry reverts a pad data update
|
||||
// revertHistoryEntry cannot revert a pad data update in non-admin mode
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, emit, getTemporaryPadData, openClient, openSocket, retry } from "./utils";
|
||||
import { SocketVersion, CRU, type Line, type LinePointsEvent, type FindOnMapLine, type ID } from "facilmap-types";
|
||||
import { createTemporaryPad, openClient, retry } from "../utils";
|
||||
import { CRU, type Line, type LinePointsEvent, type FindOnMapLine, type ID } from "facilmap-types";
|
||||
import type { LineWithTrackPoints } from "facilmap-client";
|
||||
import { cloneDeep, omit } from "lodash-es";
|
||||
|
||||
|
@ -517,82 +517,6 @@ test("Try to update line with line type from other pad", async () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("Socket v1 line name", async () => {
|
||||
// socket1: Creates the line and has it in its bbox
|
||||
// socket2: Has the line in its bbox
|
||||
// socket3: Does not have the line in its bbox
|
||||
const socket1 = await openSocket(SocketVersion.V1);
|
||||
const socket2 = await openSocket(SocketVersion.V1);
|
||||
const socket3 = await openSocket(SocketVersion.V1);
|
||||
|
||||
const onLine1 = vi.fn();
|
||||
socket1.on("line", onLine1);
|
||||
const onLine2 = vi.fn();
|
||||
socket2.on("line", onLine2);
|
||||
const onLine3 = vi.fn();
|
||||
socket3.on("line", onLine3);
|
||||
|
||||
try {
|
||||
const padData = getTemporaryPadData({});
|
||||
const padResult = await emit(socket1, "createPad", padData);
|
||||
await emit(socket2, "setPadId", padData.adminId);
|
||||
await emit(socket3, "setPadId", padData.adminId);
|
||||
|
||||
const lineType = padResult.type!.find((t) => t.type === "line")!;
|
||||
|
||||
await emit(socket1, "updateBbox", { top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await emit(socket2, "updateBbox", { top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await emit(socket3, "updateBbox", { top: 5, bottom: 0, left: 0, right: 5, zoom: 1 });
|
||||
|
||||
const line = await emit(socket1, "addLine", {
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id
|
||||
});
|
||||
|
||||
const expectedLine = {
|
||||
id: line.id,
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id,
|
||||
padId: padData.id,
|
||||
name: "Untitled line",
|
||||
mode: "",
|
||||
colour: "0000ff",
|
||||
width: 4,
|
||||
stroke: "",
|
||||
distance: 1247.95,
|
||||
time: null,
|
||||
ascent: null,
|
||||
descent: null,
|
||||
extraInfo: null,
|
||||
top: 14,
|
||||
right: 14,
|
||||
bottom: 6,
|
||||
left: 6,
|
||||
data: {}
|
||||
} satisfies Line;
|
||||
|
||||
expect(line).toEqual(expectedLine);
|
||||
|
||||
await retry(() => {
|
||||
expect(onLine1).toHaveBeenCalledTimes(1);
|
||||
expect(onLine2).toHaveBeenCalledTimes(1);
|
||||
expect(onLine3).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(onLine1).toHaveBeenCalledWith(expectedLine);
|
||||
expect(onLine2).toHaveBeenCalledWith(expectedLine);
|
||||
expect(onLine3).toHaveBeenCalledWith(expectedLine);
|
||||
} finally {
|
||||
await emit(socket1, "deletePad", undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test("Export line", async () => {
|
||||
const client = await openClient();
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, emit, getTemporaryPadData, openClient, openSocket, retry } from "./utils";
|
||||
import { SocketVersion, CRU, type Marker, type FindOnMapMarker, type ID } from "facilmap-types";
|
||||
import { createTemporaryPad, openClient, retry } from "../utils";
|
||||
import { CRU, type Marker, type FindOnMapMarker, type ID } from "facilmap-types";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
test("Create marker (using default values)", async () => {
|
||||
|
@ -43,7 +43,7 @@ test("Create marker (using default values)", async () => {
|
|||
name: "",
|
||||
colour: "ff0000",
|
||||
size: 30,
|
||||
symbol: "",
|
||||
icon: "",
|
||||
shape: "",
|
||||
data: {},
|
||||
ele: null
|
||||
|
@ -80,7 +80,7 @@ test("Create marker (using custom values)", async () => {
|
|||
name: "Test marker",
|
||||
colour: "0000ff",
|
||||
size: 40,
|
||||
symbol: "symbol",
|
||||
icon: "icon",
|
||||
shape: "shape",
|
||||
data: {
|
||||
test: "value"
|
||||
|
@ -144,7 +144,7 @@ test("Edit marker", async () => {
|
|||
name: "Test marker",
|
||||
colour: "0000ff",
|
||||
size: 40,
|
||||
symbol: "symbol",
|
||||
icon: "icon",
|
||||
shape: "shape",
|
||||
data: {
|
||||
test: "value"
|
||||
|
@ -252,7 +252,7 @@ test("Get marker", async () => {
|
|||
name: "",
|
||||
colour: "ff0000",
|
||||
size: 30,
|
||||
symbol: "",
|
||||
icon: "",
|
||||
shape: "",
|
||||
data: {},
|
||||
ele: null
|
||||
|
@ -275,7 +275,7 @@ test("Find marker", async () => {
|
|||
lat: 10,
|
||||
lon: 10,
|
||||
typeId: markerType.id,
|
||||
symbol: "a",
|
||||
icon: "a",
|
||||
ele: null
|
||||
});
|
||||
|
||||
|
@ -287,7 +287,7 @@ test("Find marker", async () => {
|
|||
lon: 10,
|
||||
typeId: markerType.id,
|
||||
name: "Marker test",
|
||||
symbol: "a"
|
||||
icon: "a"
|
||||
};
|
||||
|
||||
expect(await client2.findOnMap({ query: "Test" })).toEqual([{ ...expectedResult, similarity: 0.3333333333333333 }]);
|
||||
|
@ -400,67 +400,3 @@ test("Try to update marker with marker type from other pad", async () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Socket v1 marker name", async () => {
|
||||
// socket1: Creates the marker and has it in its bbox
|
||||
// socket2: Has the marker in its bbox
|
||||
// socket3: Does not have the marker in its bbox
|
||||
const socket1 = await openSocket(SocketVersion.V1);
|
||||
const socket2 = await openSocket(SocketVersion.V1);
|
||||
const socket3 = await openSocket(SocketVersion.V1);
|
||||
|
||||
const onMarker1 = vi.fn();
|
||||
socket1.on("marker", onMarker1);
|
||||
const onMarker2 = vi.fn();
|
||||
socket2.on("marker", onMarker2);
|
||||
const onMarker3 = vi.fn();
|
||||
socket3.on("marker", onMarker3);
|
||||
|
||||
try {
|
||||
const padData = getTemporaryPadData({});
|
||||
const padResult = await emit(socket1, "createPad", padData);
|
||||
await emit(socket2, "setPadId", padData.adminId);
|
||||
await emit(socket3, "setPadId", padData.adminId);
|
||||
|
||||
const markerType = padResult.type!.find((t) => t.type === "marker")!;
|
||||
|
||||
await emit(socket1, "updateBbox", { top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await emit(socket2, "updateBbox", { top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await emit(socket3, "updateBbox", { top: 5, bottom: 0, left: 0, right: 5, zoom: 1 });
|
||||
|
||||
const marker = await emit(socket1, "addMarker", {
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
typeId: markerType.id,
|
||||
ele: null
|
||||
});
|
||||
|
||||
const expectedMarker = {
|
||||
id: marker.id,
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
typeId: markerType.id,
|
||||
padId: padData.id,
|
||||
name: "Untitled marker",
|
||||
colour: "ff0000",
|
||||
size: 30,
|
||||
symbol: "",
|
||||
shape: "",
|
||||
data: {},
|
||||
ele: null
|
||||
} satisfies Marker;
|
||||
|
||||
expect(marker).toEqual(expectedMarker);
|
||||
|
||||
await retry(() => {
|
||||
expect(onMarker1).toHaveBeenCalledTimes(1);
|
||||
expect(onMarker2).toHaveBeenCalledTimes(1);
|
||||
expect(onMarker3).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
expect(onMarker1).toHaveBeenCalledWith(expectedMarker);
|
||||
expect(onMarker2).toHaveBeenCalledWith(expectedMarker);
|
||||
} finally {
|
||||
await emit(socket1, "deletePad", undefined);
|
||||
}
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, emit, generateTestPadId, getFacilMapUrl, getTemporaryPadData, openClient, openSocket } from "./utils";
|
||||
import { Writable, type PadData, SocketVersion, CRU, type FindPadsResult, type PagedResults } from "facilmap-types";
|
||||
import { createTemporaryPad, generateTestPadId, getFacilMapUrl, getTemporaryPadData, openClient } from "../utils";
|
||||
import { Writable, type PadData, CRU, type FindPadsResult, type PagedResults } from "facilmap-types";
|
||||
import { pick } from "lodash-es";
|
||||
import Client from "facilmap-client";
|
||||
|
||||
|
@ -355,30 +355,4 @@ test("Find pads", async () => {
|
|||
|
||||
expect(await client.findPads({ query: `Test ${uniqueId} pad` })).toEqual(expectedNotFound);
|
||||
});
|
||||
});
|
||||
|
||||
test("Socket v1 pad name", async () => {
|
||||
const socket = await openSocket(SocketVersion.V1);
|
||||
|
||||
const onPadData = vi.fn();
|
||||
socket.on("padData", onPadData);
|
||||
|
||||
try {
|
||||
const padData = getTemporaryPadData({});
|
||||
const result = await emit(socket, "createPad", padData);
|
||||
|
||||
expect(result.padData![0].name).toBe("Unnamed map");
|
||||
|
||||
const result2 = await emit(socket, "editPad", { name: "New name" });
|
||||
expect(result2.name).toBe("New name");
|
||||
expect(onPadData).toBeCalledTimes(1);
|
||||
expect(onPadData.mock.calls[0][0].name).toBe("New name");
|
||||
|
||||
const result3 = await emit(socket, "editPad", { name: "" });
|
||||
expect(result3.name).toBe("Unnamed map");
|
||||
expect(onPadData).toBeCalledTimes(2);
|
||||
expect(onPadData.mock.calls[1][0].name).toBe("Unnamed map");
|
||||
} finally {
|
||||
await emit(socket, "deletePad", undefined);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
// getRoute returns calculated route
|
||||
// With route ID and without route ID:
|
||||
// setRoute calculates route and transmits track points for initial bbox and for updated bbox
|
||||
// setRoute updates route and transmits track points for initial bbox and for updated bbox
|
||||
// clearRoute clears route and does not transmit track points on bbox update anymore
|
||||
// lineToRoute converts a line to a route
|
||||
// exportRoute exports a route to GPX
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, openClient } from "../utils";
|
||||
import { createTemporaryPad, openClient } from "../../utils";
|
||||
|
||||
test("New marker is created with dropdown styles", async () => {
|
||||
const client = await openClient();
|
||||
|
@ -14,11 +14,11 @@ test("New marker is created with dropdown styles", async () => {
|
|||
type: "dropdown",
|
||||
controlColour: true,
|
||||
controlSize: true,
|
||||
controlSymbol: true,
|
||||
controlIcon: true,
|
||||
controlShape: true,
|
||||
options: [
|
||||
{ value: "Value 1", colour: "00ffff", size: 60, symbol: "z", shape: "rectangle" },
|
||||
{ value: "Value 2", colour: "00ff00", size: 50, symbol: "a", shape: "circle" }
|
||||
{ value: "Value 1", colour: "00ffff", size: 60, icon: "z", shape: "rectangle" },
|
||||
{ value: "Value 2", colour: "00ff00", size: 50, icon: "a", shape: "circle" }
|
||||
],
|
||||
default: "Value 2"
|
||||
}
|
||||
|
@ -31,14 +31,14 @@ test("New marker is created with dropdown styles", async () => {
|
|||
typeId: type.id,
|
||||
colour: "ffffff",
|
||||
size: 20,
|
||||
symbol: "b",
|
||||
icon: "b",
|
||||
shape: "drop"
|
||||
});
|
||||
|
||||
expect(marker).toMatchObject({
|
||||
colour: "00ff00",
|
||||
size: 50,
|
||||
symbol: "a",
|
||||
icon: "a",
|
||||
shape: "circle",
|
||||
});
|
||||
});
|
||||
|
@ -138,11 +138,11 @@ test("Marker update is overridden by dropdown styles", async () => {
|
|||
type: "dropdown",
|
||||
controlColour: true,
|
||||
controlSize: true,
|
||||
controlSymbol: true,
|
||||
controlIcon: true,
|
||||
controlShape: true,
|
||||
options: [
|
||||
{ value: "Value 1", colour: "00ffff", size: 60, symbol: "z", shape: "rectangle" },
|
||||
{ value: "Value 2", colour: "00ff00", size: 50, symbol: "a", shape: "circle" }
|
||||
{ value: "Value 1", colour: "00ffff", size: 60, icon: "z", shape: "rectangle" },
|
||||
{ value: "Value 2", colour: "00ff00", size: 50, icon: "a", shape: "circle" }
|
||||
],
|
||||
default: "Value 2"
|
||||
}
|
||||
|
@ -162,14 +162,14 @@ test("Marker update is overridden by dropdown styles", async () => {
|
|||
id: marker.id,
|
||||
colour: "ffffff",
|
||||
size: 20,
|
||||
symbol: "b",
|
||||
icon: "b",
|
||||
shape: "drop"
|
||||
});
|
||||
|
||||
expect(markerUpdate).toMatchObject({
|
||||
colour: "00ffff",
|
||||
size: 60,
|
||||
symbol: "z",
|
||||
icon: "z",
|
||||
shape: "rectangle",
|
||||
});
|
||||
});
|
||||
|
@ -241,7 +241,7 @@ test("New dropdown styles are applied to existing markers", async () => {
|
|||
typeId: type.id,
|
||||
colour: "ffffff",
|
||||
size: 20,
|
||||
symbol: "b",
|
||||
icon: "b",
|
||||
shape: "drop"
|
||||
});
|
||||
|
||||
|
@ -256,11 +256,11 @@ test("New dropdown styles are applied to existing markers", async () => {
|
|||
type: "dropdown",
|
||||
controlColour: true,
|
||||
controlSize: true,
|
||||
controlSymbol: true,
|
||||
controlIcon: true,
|
||||
controlShape: true,
|
||||
options: [
|
||||
{ value: "Value 1", colour: "00ffff", size: 60, symbol: "z", shape: "rectangle" },
|
||||
{ value: "Value 2", colour: "00ff00", size: 50, symbol: "a", shape: "circle" }
|
||||
{ value: "Value 1", colour: "00ffff", size: 60, icon: "z", shape: "rectangle" },
|
||||
{ value: "Value 2", colour: "00ff00", size: 50, icon: "a", shape: "circle" }
|
||||
],
|
||||
default: "Value 2"
|
||||
}
|
||||
|
@ -272,7 +272,7 @@ test("New dropdown styles are applied to existing markers", async () => {
|
|||
expect(client.markers[marker.id]).toMatchObject({
|
||||
colour: "00ff00",
|
||||
size: 50,
|
||||
symbol: "a",
|
||||
icon: "a",
|
||||
shape: "circle",
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, openClient } from "../utils";
|
||||
import { createTemporaryPad, openClient } from "../../utils";
|
||||
|
||||
test("Rename field (marker type)", async () => {
|
||||
const client = await openClient();
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test } from "vitest";
|
||||
import { createTemporaryPad, openClient } from "../utils";
|
||||
import { createTemporaryPad, openClient } from "../../utils";
|
||||
|
||||
test("Reorder types", async () => {
|
||||
const client = await openClient();
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, openClient } from "../utils";
|
||||
import { createTemporaryPad, openClient } from "../../utils";
|
||||
|
||||
test("New marker is created with default settings", async () => {
|
||||
const client = await openClient();
|
||||
|
@ -10,7 +10,7 @@ test("New marker is created with default settings", async () => {
|
|||
type: "marker",
|
||||
defaultColour: "00ff00",
|
||||
defaultSize: 50,
|
||||
defaultSymbol: "a",
|
||||
defaultIcon: "a",
|
||||
defaultShape: "circle"
|
||||
});
|
||||
|
||||
|
@ -23,7 +23,7 @@ test("New marker is created with default settings", async () => {
|
|||
expect(marker).toMatchObject({
|
||||
colour: "00ff00",
|
||||
size: 50,
|
||||
symbol: "a",
|
||||
icon: "a",
|
||||
shape: "circle",
|
||||
});
|
||||
});
|
||||
|
@ -99,8 +99,8 @@ test("New marker is created with fixed settings", async () => {
|
|||
colourFixed: true,
|
||||
defaultSize: 50,
|
||||
sizeFixed: true,
|
||||
defaultSymbol: "a",
|
||||
symbolFixed: true,
|
||||
defaultIcon: "a",
|
||||
iconFixed: true,
|
||||
defaultShape: "circle",
|
||||
shapeFixed: true
|
||||
});
|
||||
|
@ -111,14 +111,14 @@ test("New marker is created with fixed settings", async () => {
|
|||
typeId: type.id,
|
||||
colour: "ffffff",
|
||||
size: 20,
|
||||
symbol: "b",
|
||||
icon: "b",
|
||||
shape: "drop"
|
||||
});
|
||||
|
||||
expect(marker).toMatchObject({
|
||||
colour: "00ff00",
|
||||
size: 50,
|
||||
symbol: "a",
|
||||
icon: "a",
|
||||
shape: "circle",
|
||||
});
|
||||
});
|
||||
|
@ -173,8 +173,8 @@ test("Marker update is overridden by fixed settings", async () => {
|
|||
colourFixed: true,
|
||||
defaultSize: 50,
|
||||
sizeFixed: true,
|
||||
defaultSymbol: "a",
|
||||
symbolFixed: true,
|
||||
defaultIcon: "a",
|
||||
iconFixed: true,
|
||||
defaultShape: "circle",
|
||||
shapeFixed: true
|
||||
});
|
||||
|
@ -189,14 +189,14 @@ test("Marker update is overridden by fixed settings", async () => {
|
|||
id: marker.id,
|
||||
colour: "ffffff",
|
||||
size: 20,
|
||||
symbol: "b",
|
||||
icon: "b",
|
||||
shape: "drop"
|
||||
});
|
||||
|
||||
expect(markerUpdate).toMatchObject({
|
||||
colour: "00ff00",
|
||||
size: 50,
|
||||
symbol: "a",
|
||||
icon: "a",
|
||||
shape: "circle",
|
||||
});
|
||||
});
|
||||
|
@ -261,7 +261,7 @@ test("New fixed marker styles are applied to existing markers", async () => {
|
|||
typeId: type.id,
|
||||
colour: "ffffff",
|
||||
size: 20,
|
||||
symbol: "b",
|
||||
icon: "b",
|
||||
shape: "drop"
|
||||
});
|
||||
|
||||
|
@ -274,8 +274,8 @@ test("New fixed marker styles are applied to existing markers", async () => {
|
|||
colourFixed: true,
|
||||
defaultSize: 50,
|
||||
sizeFixed: true,
|
||||
defaultSymbol: "a",
|
||||
symbolFixed: true,
|
||||
defaultIcon: "a",
|
||||
iconFixed: true,
|
||||
defaultShape: "circle",
|
||||
shapeFixed: true
|
||||
});
|
||||
|
@ -285,7 +285,7 @@ test("New fixed marker styles are applied to existing markers", async () => {
|
|||
expect(client.markers[marker.id]).toMatchObject({
|
||||
colour: "00ff00",
|
||||
size: 50,
|
||||
symbol: "a",
|
||||
icon: "a",
|
||||
shape: "circle",
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, openClient, retry } from "../utils";
|
||||
import { createTemporaryPad, openClient, retry } from "../../utils";
|
||||
import { CRU, type ID, type Type } from "facilmap-types";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
|
@ -25,8 +25,8 @@ test("Default types are added", async () => {
|
|||
colourFixed: false,
|
||||
defaultSize: 30,
|
||||
sizeFixed: false,
|
||||
defaultSymbol: '',
|
||||
symbolFixed: false,
|
||||
defaultIcon: '',
|
||||
iconFixed: false,
|
||||
defaultShape: '',
|
||||
shapeFixed: false,
|
||||
defaultWidth: 4,
|
||||
|
@ -50,8 +50,8 @@ test("Default types are added", async () => {
|
|||
colourFixed: false,
|
||||
defaultSize: 30,
|
||||
sizeFixed: false,
|
||||
defaultSymbol: '',
|
||||
symbolFixed: false,
|
||||
defaultIcon: '',
|
||||
iconFixed: false,
|
||||
defaultShape: '',
|
||||
shapeFixed: false,
|
||||
defaultWidth: 4,
|
||||
|
@ -113,8 +113,8 @@ test("Create type (marker, default settings)", async () => {
|
|||
colourFixed: false,
|
||||
defaultSize: 30,
|
||||
sizeFixed: false,
|
||||
defaultSymbol: "",
|
||||
symbolFixed: false,
|
||||
defaultIcon: "",
|
||||
iconFixed: false,
|
||||
defaultShape: "",
|
||||
shapeFixed: false,
|
||||
defaultWidth: 4,
|
||||
|
@ -184,8 +184,8 @@ test("Create type (line, default settings)", async () => {
|
|||
colourFixed: false,
|
||||
defaultSize: 30,
|
||||
sizeFixed: false,
|
||||
defaultSymbol: "",
|
||||
symbolFixed: false,
|
||||
defaultIcon: "",
|
||||
iconFixed: false,
|
||||
defaultShape: "",
|
||||
shapeFixed: false,
|
||||
defaultWidth: 4,
|
||||
|
@ -242,8 +242,8 @@ test("Create type (custom settings)", async () => {
|
|||
colourFixed: true,
|
||||
defaultSize: 35,
|
||||
sizeFixed: true,
|
||||
defaultSymbol: "a",
|
||||
symbolFixed: true,
|
||||
defaultIcon: "a",
|
||||
iconFixed: true,
|
||||
defaultShape: "star",
|
||||
shapeFixed: true,
|
||||
defaultWidth: 10,
|
||||
|
@ -307,8 +307,8 @@ test("Update type", async () => {
|
|||
colourFixed: true,
|
||||
defaultSize: 35,
|
||||
sizeFixed: true,
|
||||
defaultSymbol: "a",
|
||||
symbolFixed: true,
|
||||
defaultIcon: "a",
|
||||
iconFixed: true,
|
||||
defaultShape: "star",
|
||||
shapeFixed: true,
|
||||
defaultWidth: 10,
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, openClient, retry } from "./utils";
|
||||
import { createTemporaryPad, openClient, retry } from "../utils";
|
||||
import { type CRU, type ID, type View } from "facilmap-types";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
import { io, Socket } from "socket.io-client";
|
||||
import Client from "facilmap-client";
|
||||
import { type CRU, type PadData, SocketVersion, type SocketClientToServerEvents, type SocketServerToClientEvents, Writable, type MultipleEvents, type SocketEvents } from "facilmap-types";
|
||||
import { type CRU, type PadData, SocketVersion, type SocketClientToServerEvents, type SocketServerToClientEvents, Writable } from "facilmap-types";
|
||||
import { generateRandomPadId, sleep } from "facilmap-utils";
|
||||
|
||||
// Workaround for https://stackoverflow.com/q/64639839/242365
|
||||
global.self = this as any;
|
||||
const { default: ClientV3 } = await import("facilmap-client-v3");
|
||||
|
||||
|
||||
export function getFacilMapUrl(): string {
|
||||
if (!process.env.FACILMAP_URL) {
|
||||
throw new Error("Please specify the FacilMap server URL as FACILMAP_URL.");
|
||||
|
@ -23,8 +28,14 @@ export async function openSocket<V extends SocketVersion>(version: V): Promise<S
|
|||
return socket;
|
||||
}
|
||||
|
||||
export async function openClient(id?: string): Promise<Client> {
|
||||
const client = new Client(getFacilMapUrl(), id, { reconnection: false });
|
||||
const clientConstructors = {
|
||||
[SocketVersion.V1]: ClientV3,
|
||||
[SocketVersion.V2]: Client,
|
||||
[SocketVersion.V3]: Client
|
||||
};
|
||||
|
||||
export async function openClient<V extends SocketVersion = SocketVersion.V3>(id?: string, version: V = SocketVersion.V3 as any): Promise<InstanceType<typeof clientConstructors[V]>> {
|
||||
const client = new clientConstructors[version](getFacilMapUrl(), id, { reconnection: false }) as any;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (id != null) {
|
||||
client.on("padData", () => {
|
||||
|
@ -52,15 +63,15 @@ export function getTemporaryPadData<D extends Partial<PadData<CRU.CREATE>>>(data
|
|||
};
|
||||
}
|
||||
|
||||
export async function createTemporaryPad<D extends Partial<PadData<CRU.CREATE>>>(
|
||||
client: Client,
|
||||
export async function createTemporaryPad<D extends Partial<PadData<CRU.CREATE>>, C extends InstanceType<typeof clientConstructors[keyof typeof clientConstructors]>>(
|
||||
client: C,
|
||||
data: D,
|
||||
callback?: (createPadData: ReturnType<typeof getTemporaryPadData<D>>, padData: PadData & { writable: Writable }, result: MultipleEvents<SocketEvents<SocketVersion.V2>>) => Promise<void>
|
||||
callback?: (createPadData: ReturnType<typeof getTemporaryPadData<D>>, padData: PadData & { writable: Writable }, result: Awaited<ReturnType<C["createPad"]>>) => Promise<void>
|
||||
): Promise<void> {
|
||||
const createPadData = getTemporaryPadData(data);
|
||||
const result = await client.createPad(createPadData);
|
||||
try {
|
||||
await callback?.(createPadData, result.padData![0], result);
|
||||
await callback?.(createPadData, client.padData!, result as any);
|
||||
} finally {
|
||||
await client.deletePad();
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
const el = document.createElement("div");
|
||||
el.id = id;
|
||||
document.body.appendChild(el);
|
||||
for (const icon of ["", "O", "g", "j", "a", "*", ...FacilMap.symbolList]) {
|
||||
for (const icon of ["", "O", "g", "j", "a", "*", ...FacilMap.iconList]) {
|
||||
const span = document.createElement('span');
|
||||
span.title = icon;
|
||||
createIcon(icon).then((html) => {
|
||||
|
@ -35,7 +35,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
createIcons("icons", (icon) => FacilMap.getSymbolHtml("#000000", 50, icon));
|
||||
createIcons("icons", (icon) => FacilMap.getIconHtml("#000000", 50, icon));
|
||||
for (const shape of ["drop", "rectangle-marker", "circle", "rectangle", "diamond", "pentagon", "hexagon", "triangle", "triangle-down", "star"]) {
|
||||
createIcons(`icons-${shape}`, (icon) => FacilMap.getMarkerHtml("#ccffcc", 50, icon, shape));
|
||||
createIcons(`icons-${shape}-highlight`, (icon) => FacilMap.getMarkerHtml("#ccffcc", 50, icon, shape, true));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "facilmap-leaflet",
|
||||
"version": "4.1.1",
|
||||
"version": "5.0.0",
|
||||
"description": "Utilities to show FacilMap objects on a Leaflet map.",
|
||||
"keywords": [
|
||||
"maps",
|
||||
|
|
|
@ -9,7 +9,7 @@ Map.addInitHook(function (this: Map) {
|
|||
});
|
||||
|
||||
export interface MarkerLayerOptions extends MarkerOptions {
|
||||
marker?: Partial<Marker> & Pick<Marker, 'colour' | 'size' | 'symbol' | 'shape'>;
|
||||
marker?: Partial<Marker> & Pick<Marker, 'colour' | 'size' | 'icon' | 'shape'>;
|
||||
highlight?: boolean;
|
||||
raised?: boolean;
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ export default class MarkerLayer extends LeafletMarker {
|
|||
|
||||
_initIcon(): void {
|
||||
if (this.options.marker)
|
||||
this.options.icon = getMarkerIcon(`#${this.options.marker.colour}`, this.options.marker.size, this.options.marker.symbol ?? undefined, this.options.marker.shape ?? undefined, this.options.highlight);
|
||||
this.options.icon = getMarkerIcon(`#${this.options.marker.colour}`, this.options.marker.size, this.options.marker.icon ?? undefined, this.options.marker.shape ?? undefined, this.options.highlight);
|
||||
|
||||
super._initIcon();
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Colour, Shape } from "facilmap-types";
|
||||
import { FeatureGroup, latLng, Layer, type LayerOptions } from "leaflet";
|
||||
import MarkerLayer from "../markers/marker-layer";
|
||||
import { getSymbolForTags } from "../utils/icons";
|
||||
import { getIconForTags } from "../utils/icons";
|
||||
import { tooltipOptions } from "../utils/leaflet";
|
||||
import type { OverpassPreset } from "./overpass-presets";
|
||||
import { getOverpassElements, isOverpassQueryEmpty, type OverpassElement } from "./overpass-utils";
|
||||
|
@ -114,7 +114,7 @@ export default class OverpassLayer extends FeatureGroup {
|
|||
marker: {
|
||||
colour: this.options.markerColour!,
|
||||
size: this.options.markerSize!,
|
||||
symbol: getSymbolForTags(element.tags),
|
||||
icon: getIconForTags(element.tags),
|
||||
shape: this.options.markerShape!
|
||||
},
|
||||
raised: isHighlighted,
|
||||
|
|
|
@ -78,7 +78,7 @@ export default class SearchResultsLayer extends FeatureGroup {
|
|||
marker: {
|
||||
colour: this.options.markerColour!,
|
||||
size: this.options.markerSize!,
|
||||
symbol: result.icon || '',
|
||||
icon: result.icon || '',
|
||||
shape: this.options.markerShape!
|
||||
},
|
||||
pathOptions: this.options.pathOptions
|
||||
|
@ -97,7 +97,7 @@ export default class SearchResultsLayer extends FeatureGroup {
|
|||
marker: {
|
||||
colour: this.options.markerColour!,
|
||||
size: this.options.markerSize!,
|
||||
symbol: result.icon || '',
|
||||
icon: result.icon || '',
|
||||
shape: this.options.markerShape!
|
||||
}
|
||||
}).bindTooltip(result.display_name, { ...tooltipOptions, offset: [ 20, 0 ] })
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import type { Shape, Symbol } from "facilmap-types";
|
||||
import type { Shape, Icon } from "facilmap-types";
|
||||
import { makeTextColour, quoteHtml } from "facilmap-utils";
|
||||
import { Icon, type IconOptions } from "leaflet";
|
||||
import { Icon as LeafletIcon, type IconOptions } from "leaflet";
|
||||
import { memoize } from "lodash-es";
|
||||
import iconKeys, { coreIconKeys } from "virtual:icons:keys";
|
||||
import rawIconsCore from "virtual:icons:core";
|
||||
|
||||
export { coreIconKeys as coreSymbolList };
|
||||
export { coreIconKeys as coreIconList };
|
||||
|
||||
export const symbolList: string[] = Object.values(iconKeys).flat();
|
||||
export const iconList: string[] = Object.values(iconKeys).flat();
|
||||
|
||||
export const RAINBOW_STOPS = `<stop offset="0" stop-color="red"/><stop offset="33%" stop-color="#ff0"/><stop offset="50%" stop-color="#0f0"/><stop offset="67%" stop-color="cyan"/><stop offset="100%" stop-color="blue"/>`;
|
||||
|
||||
|
@ -22,12 +22,12 @@ interface ShapeInfo {
|
|||
* For a square marker, the bottom center would be [36, 18] and the center would be [18, 18]. */
|
||||
base: [number, number];
|
||||
|
||||
/** The X and Y coordinates of the center of the marker. This is where the symbol will be inserted.
|
||||
/** The X and Y coordinates of the center of the marker. This is where the icon will be inserted.
|
||||
* For a square marker, the center would be [18, 18]. */
|
||||
center: [number, number];
|
||||
|
||||
/** The height/width that inserted symbols should have. */
|
||||
symbolSize: number;
|
||||
/** The height/width that inserted icons should have. */
|
||||
iconSize: number;
|
||||
|
||||
/** Scale factor for the shape itself. If this is 1, a markers that has its size set to 25 will be 25px high. */
|
||||
scale: number;
|
||||
|
@ -42,7 +42,7 @@ const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
|
|||
width: 26,
|
||||
base: [13, 36],
|
||||
center: [13, 13],
|
||||
symbolSize: 18,
|
||||
iconSize: 18,
|
||||
scale: 1
|
||||
},
|
||||
"rectangle-marker": {
|
||||
|
@ -50,7 +50,7 @@ const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
|
|||
width: 30,
|
||||
base: [15, 36],
|
||||
center: [15, 15],
|
||||
symbolSize: 24,
|
||||
iconSize: 24,
|
||||
scale: 0.95
|
||||
},
|
||||
"circle": {
|
||||
|
@ -58,7 +58,7 @@ const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
|
|||
width: 36,
|
||||
base: [18, 18],
|
||||
center: [18, 18],
|
||||
symbolSize: 24,
|
||||
iconSize: 24,
|
||||
scale: 0.85
|
||||
},
|
||||
"rectangle": {
|
||||
|
@ -66,7 +66,7 @@ const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
|
|||
width: 36,
|
||||
base: [18, 18],
|
||||
center: [18, 18],
|
||||
symbolSize: 28,
|
||||
iconSize: 28,
|
||||
scale: 0.8
|
||||
},
|
||||
"diamond": {
|
||||
|
@ -74,7 +74,7 @@ const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
|
|||
width: 36,
|
||||
base: [18, 18],
|
||||
center: [18, 18],
|
||||
symbolSize: 18,
|
||||
iconSize: 18,
|
||||
scale: 0.9
|
||||
},
|
||||
"pentagon": {
|
||||
|
@ -82,7 +82,7 @@ const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
|
|||
width: 38,
|
||||
base: [19, 20],
|
||||
center: [19, 20],
|
||||
symbolSize: 20,
|
||||
iconSize: 20,
|
||||
scale: 0.85
|
||||
},
|
||||
"hexagon": {
|
||||
|
@ -90,7 +90,7 @@ const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
|
|||
width: 32,
|
||||
base: [16, 18],
|
||||
center: [16, 18],
|
||||
symbolSize: 22,
|
||||
iconSize: 22,
|
||||
scale: 0.9
|
||||
},
|
||||
"triangle": {
|
||||
|
@ -98,7 +98,7 @@ const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
|
|||
width: 42,
|
||||
base: [21, 24],
|
||||
center: [21, 24],
|
||||
symbolSize: 16,
|
||||
iconSize: 16,
|
||||
scale: 0.85
|
||||
},
|
||||
"triangle-down": {
|
||||
|
@ -106,7 +106,7 @@ const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
|
|||
width: 40,
|
||||
base: [20, 36],
|
||||
center: [20, 12],
|
||||
symbolSize: 16,
|
||||
iconSize: 16,
|
||||
scale: 0.85
|
||||
},
|
||||
"star": {
|
||||
|
@ -114,7 +114,7 @@ const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
|
|||
width: 38,
|
||||
base: [19, 18],
|
||||
center: [19, 20],
|
||||
symbolSize: 14,
|
||||
iconSize: 14,
|
||||
scale: 0.9
|
||||
}
|
||||
};
|
||||
|
@ -147,41 +147,41 @@ let rawIconsExtra: (typeof import("virtual:icons:extra"))["default"];
|
|||
/**
|
||||
* Downloads the icons chunk to have them already downloaded the first time the icon code is needed.
|
||||
*/
|
||||
export async function preloadExtraSymbols(): Promise<void> {
|
||||
export async function preloadExtraIcons(): Promise<void> {
|
||||
if (!rawIconsExtra) {
|
||||
rawIconsExtra = (await import("virtual:icons:extra")).default;
|
||||
}
|
||||
}
|
||||
|
||||
function symbolNeedsPreload(symbol: Symbol | undefined): boolean {
|
||||
return !!symbol && symbolList.includes(symbol) && !coreIconKeys.includes(symbol);
|
||||
function iconNeedsPreload(icon: Icon | undefined): boolean {
|
||||
return !!icon && iconList.includes(icon) && !coreIconKeys.includes(icon);
|
||||
}
|
||||
|
||||
export async function preloadSymbol(symbol: Symbol | undefined): Promise<void> {
|
||||
if (symbolNeedsPreload(symbol)) {
|
||||
await preloadExtraSymbols();
|
||||
export async function preloadIcon(icon: Icon | undefined): Promise<void> {
|
||||
if (iconNeedsPreload(icon)) {
|
||||
await preloadExtraIcons();
|
||||
}
|
||||
}
|
||||
|
||||
export function isSymbolPreloaded(symbol: Symbol | undefined): boolean {
|
||||
return !symbolNeedsPreload(symbol) || !!rawIconsExtra;
|
||||
export function isIconPreloaded(icon: Icon | undefined): boolean {
|
||||
return !iconNeedsPreload(icon) || !!rawIconsExtra;
|
||||
}
|
||||
|
||||
function getRawSymbolCodeSync(symbol: Symbol): [set: string, code: string] {
|
||||
if (coreIconKeys.includes(symbol)) {
|
||||
const set = Object.keys(rawIconsCore).filter((i) => (rawIconsCore[i][symbol] != null))[0];
|
||||
return [set, rawIconsCore[set][symbol]];
|
||||
} else if (symbolList.includes(symbol)) {
|
||||
const set = Object.keys(rawIconsExtra).filter((i) => (rawIconsExtra[i][symbol] != null))[0];
|
||||
return [set, rawIconsExtra[set][symbol]];
|
||||
function getRawIconCodeSync(icon: Icon): [set: string, code: string] {
|
||||
if (coreIconKeys.includes(icon)) {
|
||||
const set = Object.keys(rawIconsCore).filter((i) => (rawIconsCore[i][icon] != null))[0];
|
||||
return [set, rawIconsCore[set][icon]];
|
||||
} else if (iconList.includes(icon)) {
|
||||
const set = Object.keys(rawIconsExtra).filter((i) => (rawIconsExtra[i][icon] != null))[0];
|
||||
return [set, rawIconsExtra[set][icon]];
|
||||
} else {
|
||||
throw new Error(`Unknown icon ${symbol}.`);
|
||||
throw new Error(`Unknown icon ${icon}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getSymbolCodeSync(colour: string, size: number, symbol?: Symbol): string {
|
||||
if(symbol && symbolList.includes(symbol)) {
|
||||
const [set, code] = getRawSymbolCodeSync(symbol);
|
||||
export function getIconCodeSync(colour: string, size: number, icon?: Icon): string {
|
||||
if(icon && iconList.includes(icon)) {
|
||||
const [set, code] = getRawIconCodeSync(icon);
|
||||
|
||||
if(set == "osmi") {
|
||||
return `<g transform="scale(${size / sizes.osmi})">${code.replace(/#000/g, colour)}</g>`;
|
||||
|
@ -199,107 +199,107 @@ export function getSymbolCodeSync(colour: string, size: number, symbol?: Symbol)
|
|||
const content = code.replace(/^<svg [^>]*>/, "").replace(/<\/svg>$/, "");
|
||||
|
||||
return `<g transform="scale(${scale}) translate(${moveX}, ${moveY})" fill="${colour}">${content}</g>`;
|
||||
} else if (symbol && symbol.length == 1) {
|
||||
} else if (icon && icon.length == 1) {
|
||||
try {
|
||||
const offset = getLetterOffset(symbol);
|
||||
const offset = getLetterOffset(icon);
|
||||
return (
|
||||
`<g transform="scale(${size / 25}) translate(${offset.x}, ${offset.y})">` +
|
||||
`<text style="font-size: 25px; font-family: sans-serif; fill: ${colour}">${quoteHtml(symbol)}</text>` +
|
||||
`<text style="font-size: 25px; font-family: sans-serif; fill: ${colour}">${quoteHtml(icon)}</text>` +
|
||||
`</g>`
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Error creating letter symbol.", e);
|
||||
console.error("Error creating letter icon.", e);
|
||||
}
|
||||
}
|
||||
|
||||
return `<circle style="fill:${colour}" cx="${Math.floor(size / 2)}" cy="${Math.floor(size / 2)}" r="${Math.floor(size / 6)}" />`;
|
||||
}
|
||||
|
||||
export async function getSymbolCode(colour: string, size: number, symbol?: Symbol): Promise<string> {
|
||||
await preloadSymbol(symbol);
|
||||
return getSymbolCodeSync(colour, size, symbol);
|
||||
export async function getIconCode(colour: string, size: number, icon?: Icon): Promise<string> {
|
||||
await preloadIcon(icon);
|
||||
return getIconCodeSync(colour, size, icon);
|
||||
}
|
||||
|
||||
export function getSymbolUrlSync(colour: string, height: number, symbol?: Symbol): string {
|
||||
export function getIconUrlSync(colour: string, height: number, icon?: Icon): string {
|
||||
const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` +
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${height}" height="${height}" viewbox="0 0 24 24" version="1.1">` +
|
||||
getSymbolCodeSync(colour, 24, symbol) +
|
||||
getIconCodeSync(colour, 24, icon) +
|
||||
`</svg>`;
|
||||
|
||||
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
export async function getSymbolUrl(colour: string, height: number, symbol?: Symbol): Promise<string> {
|
||||
await preloadSymbol(symbol);
|
||||
return getSymbolUrlSync(colour, height, symbol);
|
||||
export async function getIconUrl(colour: string, height: number, icon?: Icon): Promise<string> {
|
||||
await preloadIcon(icon);
|
||||
return getIconUrlSync(colour, height, icon);
|
||||
}
|
||||
|
||||
export function getSymbolHtmlSync(colour: string, height: number | string, symbol?: Symbol): string {
|
||||
export function getIconHtmlSync(colour: string, height: number | string, icon?: Icon): string {
|
||||
return `<svg width="${height}" height="${height}" viewbox="0 0 24 24">` +
|
||||
getSymbolCodeSync(colour, 24, symbol) +
|
||||
getIconCodeSync(colour, 24, icon) +
|
||||
`</svg>`;
|
||||
}
|
||||
|
||||
export async function getSymbolHtml(colour: string, height: number | string, symbol?: Symbol): Promise<string> {
|
||||
await preloadSymbol(symbol);
|
||||
return getSymbolHtmlSync(colour, height, symbol);
|
||||
export async function getIconHtml(colour: string, height: number | string, icon?: Icon): Promise<string> {
|
||||
await preloadIcon(icon);
|
||||
return getIconHtmlSync(colour, height, icon);
|
||||
}
|
||||
|
||||
let idCounter = 0;
|
||||
|
||||
export function getMarkerCodeSync(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): string {
|
||||
export function getMarkerCodeSync(colour: string, height: number, icon?: Icon, shape?: Shape, highlight = false): string {
|
||||
const borderColour = makeTextColour(colour, 0.3);
|
||||
const id = `${idCounter++}`;
|
||||
const colourCode = colour == "rainbow" ? `url(#fm-rainbow-${id})` : colour;
|
||||
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES[DEFAULT_SHAPE];
|
||||
const symbolCode = getSymbolCodeSync(borderColour, shapeObj.symbolSize, symbol);
|
||||
const translateX = `${Math.floor(shapeObj.center[0] - shapeObj.symbolSize / 2)}`;
|
||||
const translateY = `${Math.floor(shapeObj.center[1] - shapeObj.symbolSize / 2)}`;
|
||||
const iconCode = getIconCodeSync(borderColour, shapeObj.iconSize, icon);
|
||||
const translateX = `${Math.floor(shapeObj.center[0] - shapeObj.iconSize / 2)}`;
|
||||
const translateY = `${Math.floor(shapeObj.center[1] - shapeObj.iconSize / 2)}`;
|
||||
|
||||
return (
|
||||
`<g transform="scale(${height / SHAPE_HEIGHT})">` +
|
||||
(colour == "rainbow" ? `<defs><linearGradient id="fm-rainbow-${id}" x2="0" y2="100%">${RAINBOW_STOPS}</linearGradient></defs>` : '') +
|
||||
`<path id="shape-${id}" style="stroke: ${borderColour}; stroke-width: ${highlight ? 6 : 2}; stroke-linecap: round; fill: ${colourCode}; clip-path: url(#clip-${id})" d="${quoteHtml(shapeObj.path)}"/>"/>` +
|
||||
`<clipPath id="clip-${id}"><use xlink:href="#shape-${id}"/></clipPath>` + // Don't increase the size by increasing the border: https://stackoverflow.com/a/32162431/242365
|
||||
`<g transform="translate(${translateX}, ${translateY})">${symbolCode}</g>` +
|
||||
`<g transform="translate(${translateX}, ${translateY})">${iconCode}</g>` +
|
||||
`</g>`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getMarkerCode(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): Promise<string> {
|
||||
await preloadSymbol(symbol);
|
||||
return getMarkerCodeSync(colour, height, symbol, shape, highlight);
|
||||
export async function getMarkerCode(colour: string, height: number, icon?: Icon, shape?: Shape, highlight = false): Promise<string> {
|
||||
await preloadIcon(icon);
|
||||
return getMarkerCodeSync(colour, height, icon, shape, highlight);
|
||||
}
|
||||
|
||||
export function getMarkerUrlSync(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): string {
|
||||
export function getMarkerUrlSync(colour: string, height: number, icon?: Icon, shape?: Shape, highlight = false): string {
|
||||
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES[DEFAULT_SHAPE];
|
||||
const width = Math.ceil(height * shapeObj.width / SHAPE_HEIGHT);
|
||||
return "data:image/svg+xml,"+encodeURIComponent(
|
||||
`<?xml version="1.0" encoding="UTF-8" standalone="no"?>` +
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${shapeObj.width} ${SHAPE_HEIGHT}" version="1.1">` +
|
||||
getMarkerCodeSync(colour, SHAPE_HEIGHT, symbol, shape, highlight) +
|
||||
getMarkerCodeSync(colour, SHAPE_HEIGHT, icon, shape, highlight) +
|
||||
`</svg>`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getMarkerUrl(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): Promise<string> {
|
||||
await preloadSymbol(symbol);
|
||||
return getMarkerUrlSync(colour, height, symbol, shape, highlight);
|
||||
export async function getMarkerUrl(colour: string, height: number, icon?: Icon, shape?: Shape, highlight = false): Promise<string> {
|
||||
await preloadIcon(icon);
|
||||
return getMarkerUrlSync(colour, height, icon, shape, highlight);
|
||||
}
|
||||
|
||||
export function getMarkerHtmlSync(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): string {
|
||||
export function getMarkerHtmlSync(colour: string, height: number, icon?: Icon, shape?: Shape, highlight = false): string {
|
||||
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES[DEFAULT_SHAPE];
|
||||
const width = Math.ceil(height * shapeObj.width / SHAPE_HEIGHT);
|
||||
return (
|
||||
`<svg width="${width}" height="${height}" viewBox="0 0 ${shapeObj.width} ${SHAPE_HEIGHT}">` +
|
||||
getMarkerCodeSync(colour, SHAPE_HEIGHT, symbol, shape, highlight) +
|
||||
getMarkerCodeSync(colour, SHAPE_HEIGHT, icon, shape, highlight) +
|
||||
`</svg>`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getMarkerHtml(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): Promise<string> {
|
||||
await preloadSymbol(symbol);
|
||||
return getMarkerHtmlSync(colour, height, symbol, shape, highlight);
|
||||
export async function getMarkerHtml(colour: string, height: number, icon?: Icon, shape?: Shape, highlight = false): Promise<string> {
|
||||
await preloadIcon(icon);
|
||||
return getMarkerHtmlSync(colour, height, icon, shape, highlight);
|
||||
}
|
||||
|
||||
export const TRANSPARENT_IMAGE_URL = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E";
|
||||
|
@ -313,7 +313,7 @@ declare global {
|
|||
/**
|
||||
* A Leaflet icon that accepts a promise for its URL and will update the image src when the promise is resolved.
|
||||
*/
|
||||
export class AsyncIcon extends Icon {
|
||||
export class AsyncIcon extends LeafletIcon {
|
||||
private _asyncIconUrl?: Promise<string>;
|
||||
|
||||
constructor(options: Omit<IconOptions, "iconUrl"> & { iconUrl: string | Promise<string> }) {
|
||||
|
@ -351,13 +351,13 @@ export class AsyncIcon extends Icon {
|
|||
}
|
||||
}
|
||||
|
||||
export function getMarkerIcon(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): Icon {
|
||||
export function getMarkerIcon(colour: string, height: number, icon?: Icon, shape?: Shape, highlight = false): LeafletIcon {
|
||||
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES[DEFAULT_SHAPE];
|
||||
const scale = shapeObj.scale * height / SHAPE_HEIGHT;
|
||||
const result = new AsyncIcon({
|
||||
iconUrl: (
|
||||
isSymbolPreloaded(symbol) ? getMarkerUrlSync(colour, height, symbol, shape, highlight)
|
||||
: getMarkerUrl(colour, height, symbol, shape, highlight)
|
||||
isIconPreloaded(icon) ? getMarkerUrlSync(colour, height, icon, shape, highlight)
|
||||
: getMarkerUrl(colour, height, icon, shape, highlight)
|
||||
),
|
||||
iconSize: [Math.round(shapeObj.width*scale), Math.round(SHAPE_HEIGHT*scale)],
|
||||
iconAnchor: [Math.round(shapeObj.base[0]*scale), Math.round(shapeObj.base[1]*scale)],
|
||||
|
@ -376,9 +376,9 @@ const RELEVANT_TAGS = [
|
|||
"plant:source"
|
||||
];
|
||||
|
||||
export function getSymbolForTags(tags: Record<string, string>): Symbol {
|
||||
export function getIconForTags(tags: Record<string, string>): Icon {
|
||||
const tagWords = Object.entries(tags).flatMap(([k, v]) => (RELEVANT_TAGS.includes(k) ? v.split(/_:/) : []));
|
||||
let result: Symbol = "";
|
||||
let result: Icon = "";
|
||||
let resultMatch: number = 0;
|
||||
for (const icon of iconKeys.osmi) {
|
||||
const iconWords = icon.split("_");
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "facilmap-server",
|
||||
"version": "4.1.1",
|
||||
"version": "5.0.0",
|
||||
"type": "module",
|
||||
"description": "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration.",
|
||||
"keywords": [
|
||||
|
@ -63,6 +63,7 @@
|
|||
"mysql2": "^3.9.4",
|
||||
"p-throttle": "^6.1.0",
|
||||
"pg": "^8.11.5",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"sequelize": "^6.37.2",
|
||||
"serialize-error": "^11.0.3",
|
||||
"socket.io": "^4.7.5",
|
||||
|
@ -83,6 +84,7 @@
|
|||
"@types/geojson": "^7946.0.14",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.12.6",
|
||||
"@types/proxy-addr": "^2.0.3",
|
||||
"@types/string-similarity": "^4.0.2",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"debug": "^4.3.4",
|
||||
|
|
|
@ -15,7 +15,7 @@ import DatabaseMigrations from "./migrations.js";
|
|||
import { TypedEventEmitter } from "../utils/events.js";
|
||||
import type { HistoryEntry, ID, Line, Marker, ObjectWithId, PadData, PadId, TrackPoint, Type, View } from "facilmap-types";
|
||||
|
||||
export interface DatabaseEvents {
|
||||
export interface DatabaseEventsInterface {
|
||||
addHistoryEntry: [padId: PadId, newEntry: HistoryEntry];
|
||||
historyChange: [padId: PadId];
|
||||
|
||||
|
@ -36,6 +36,8 @@ export interface DatabaseEvents {
|
|||
deleteView: [padId: PadId, data: ObjectWithId];
|
||||
}
|
||||
|
||||
export type DatabaseEvents = Pick<DatabaseEventsInterface, keyof DatabaseEventsInterface>; // Workaround for https://github.com/microsoft/TypeScript/issues/15300
|
||||
|
||||
export default class Database extends TypedEventEmitter<DatabaseEvents> {
|
||||
|
||||
_conn: Sequelize;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type CreationOptional, DataTypes, type ForeignKey, type InferAttributes, type InferCreationAttributes, Model } from "sequelize";
|
||||
import type { BboxWithZoom, CRU, Colour, ID, Latitude, Longitude, Marker, PadId, Shape, Size, Symbol, Type } from "facilmap-types";
|
||||
import type { BboxWithZoom, CRU, Colour, ID, Latitude, Longitude, Marker, PadId, Shape, Size, Icon, Type } from "facilmap-types";
|
||||
import { type BboxWithExcept, createModel, dataDefinition, type DataModel, getDefaultIdType, getPosType, getVirtualLatType, getVirtualLonType, makeNotNullForeignKey } from "./helpers.js";
|
||||
import Database from "./database.js";
|
||||
import { getElevationForPoint, resolveCreateMarker, resolveUpdateMarker } from "facilmap-utils";
|
||||
|
@ -18,7 +18,7 @@ export interface MarkerModel extends Model<InferAttributes<MarkerModel>, InferCr
|
|||
typeId: ForeignKey<TypeModel["id"]>;
|
||||
colour: Colour;
|
||||
size: Size;
|
||||
symbol: Symbol;
|
||||
icon: Icon;
|
||||
shape: Shape;
|
||||
ele: number | null;
|
||||
toJSON: () => Marker;
|
||||
|
@ -42,7 +42,7 @@ export default class DatabaseMarkers {
|
|||
name : { type: DataTypes.TEXT, allowNull: false },
|
||||
colour : { type: DataTypes.STRING(6), allowNull: false },
|
||||
size : { type: DataTypes.INTEGER.UNSIGNED, allowNull: false },
|
||||
symbol : { type: DataTypes.TEXT, allowNull: false },
|
||||
icon : { type: DataTypes.TEXT, allowNull: false },
|
||||
shape : { type: DataTypes.TEXT, allowNull: false },
|
||||
ele: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
|
|
@ -17,6 +17,7 @@ export interface MetaProperties {
|
|||
extraInfoNullMigrationCompleted: "1";
|
||||
typesIdxMigrationCompleted: "1";
|
||||
viewsIdxMigrationCompleted: "1";
|
||||
fieldIconsMigrationCompleted: "1";
|
||||
}
|
||||
|
||||
export default class DatabaseMeta {
|
||||
|
|
|
@ -30,6 +30,7 @@ export default class DatabaseMigrations {
|
|||
await this._extraInfoNullMigration();
|
||||
await this._typesIdxMigration();
|
||||
await this._viewsIdxMigration();
|
||||
await this._fieldIconsMigration();
|
||||
|
||||
(async () => {
|
||||
await this._elevationMigration();
|
||||
|
@ -44,6 +45,16 @@ export default class DatabaseMigrations {
|
|||
async _renameColMigrations(): Promise<void> {
|
||||
const queryInterface = this._db._conn.getQueryInterface();
|
||||
|
||||
|
||||
const markerAttrs = await queryInterface.describeTable('Markers');
|
||||
|
||||
// Rename Marker.symbol to Marker.icon
|
||||
if (markerAttrs.symbol) {
|
||||
console.log("DB migration: Rename Markers.symbol to Markers.icons");
|
||||
await queryInterface.renameColumn('Markers', 'symbol', 'icon');
|
||||
}
|
||||
|
||||
|
||||
const lineAttrs = await queryInterface.describeTable('Lines');
|
||||
|
||||
// Rename Line.points to Line.routePoints
|
||||
|
@ -59,6 +70,21 @@ export default class DatabaseMigrations {
|
|||
}
|
||||
|
||||
|
||||
const typeAttrs = await queryInterface.describeTable('Types');
|
||||
|
||||
// Rename Types.defaultSymbol to Types.defaultIcon
|
||||
if (typeAttrs.defaultSymbol) {
|
||||
console.log("DB migration: Rename Types.defaultSymbol to Types.defaultIcon");
|
||||
await queryInterface.renameColumn('Types', 'defaultSymbol', 'defaultIcon');
|
||||
}
|
||||
|
||||
// Rename Types.symbolFixed to Types.iconFixed
|
||||
if (typeAttrs.symbolFixed) {
|
||||
console.log("DB migration: Rename Types.symbolFixed to Types.iconFixed");
|
||||
await queryInterface.renameColumn('Types', 'symbolFixed', 'iconFixed');
|
||||
}
|
||||
|
||||
|
||||
const padAttrs = await queryInterface.describeTable('Pads');
|
||||
|
||||
// Rename writeId to adminId
|
||||
|
@ -142,11 +168,11 @@ export default class DatabaseMigrations {
|
|||
await queryInterface.changeColumn("Markers", "size", this._db.markers.MarkerModel.getAttributes().size);
|
||||
}
|
||||
|
||||
// Forbid null marker symbol
|
||||
if (markersAttributes.symbol.allowNull) {
|
||||
console.log("DB migration: Remove defaultValue from Markers.symbol");
|
||||
await this._db.markers.MarkerModel.update({ symbol: "" }, { where: { symbol: null as any } });
|
||||
await queryInterface.changeColumn("Markers", "symbol", this._db.markers.MarkerModel.getAttributes().symbol);
|
||||
// Forbid null marker icon
|
||||
if (markersAttributes.icon.allowNull) {
|
||||
console.log("DB migration: Remove defaultValue from Markers.icon");
|
||||
await this._db.markers.MarkerModel.update({ icon: "" }, { where: { icon: null as any } });
|
||||
await queryInterface.changeColumn("Markers", "icon", this._db.markers.MarkerModel.getAttributes().icon);
|
||||
}
|
||||
|
||||
// Forbid null marker shape
|
||||
|
@ -236,18 +262,18 @@ export default class DatabaseMigrations {
|
|||
await queryInterface.changeColumn("Types", "sizeFixed", this._db.types.TypeModel.getAttributes().sizeFixed);
|
||||
}
|
||||
|
||||
// Forbid null defaultSymbol
|
||||
if (typesAttributes.defaultSymbol.allowNull) {
|
||||
console.log("DB migration: Disallow null for Types.defaultSymbol");
|
||||
await this._db.types.TypeModel.update({ defaultSymbol: "" }, { where: { defaultSymbol: null as any } });
|
||||
await queryInterface.changeColumn("Types", "defaultSymbol", this._db.types.TypeModel.getAttributes().defaultSymbol);
|
||||
// Forbid null defaultIcon
|
||||
if (typesAttributes.defaultIcon.allowNull) {
|
||||
console.log("DB migration: Disallow null for Types.defaultIcon");
|
||||
await this._db.types.TypeModel.update({ defaultIcon: "" }, { where: { defaultIcon: null as any } });
|
||||
await queryInterface.changeColumn("Types", "defaultIcon", this._db.types.TypeModel.getAttributes().defaultIcon);
|
||||
}
|
||||
|
||||
// Forbid null symbolFixed
|
||||
if (typesAttributes.symbolFixed.allowNull) {
|
||||
console.log("DB migration: Disallow null for Types.symbolFixed");
|
||||
await this._db.types.TypeModel.update({ symbolFixed: false }, { where: { symbolFixed: null as any } });
|
||||
await queryInterface.changeColumn("Types", "symbolFixed", this._db.types.TypeModel.getAttributes().symbolFixed);
|
||||
// Forbid null iconFixed
|
||||
if (typesAttributes.iconFixed.allowNull) {
|
||||
console.log("DB migration: Disallow null for Types.iconFixed");
|
||||
await this._db.types.TypeModel.update({ iconFixed: false }, { where: { iconFixed: null as any } });
|
||||
await queryInterface.changeColumn("Types", "iconFixed", this._db.types.TypeModel.getAttributes().iconFixed);
|
||||
}
|
||||
|
||||
// Forbid null defaultShape
|
||||
|
@ -439,12 +465,12 @@ export default class DatabaseMigrations {
|
|||
for(const type of types) {
|
||||
let showInLegend = false;
|
||||
|
||||
if(type.colourFixed || (type.type == "marker" && type.symbolFixed && type.defaultSymbol) || (type.type == "marker" && type.shapeFixed) || (type.type == "line" && type.widthFixed))
|
||||
if(type.colourFixed || (type.type == "marker" && type.iconFixed && type.defaultIcon) || (type.type == "marker" && type.shapeFixed) || (type.type == "line" && type.widthFixed))
|
||||
showInLegend = true;
|
||||
|
||||
if(!showInLegend) {
|
||||
for(const field of type.fields) {
|
||||
if((field.type == "dropdown" || field.type == "checkbox") && (field.controlColour || (type.type == "marker" && field.controlSymbol) || (type.type == "marker" && field.controlShape) || (type.type == "line" && field.controlWidth))) {
|
||||
if((field.type == "dropdown" || field.type == "checkbox") && (field.controlColour || (type.type == "marker" && field.controlIcon) || (type.type == "marker" && field.controlShape) || (type.type == "line" && field.controlWidth))) {
|
||||
showInLegend = true;
|
||||
break;
|
||||
}
|
||||
|
@ -642,4 +668,43 @@ export default class DatabaseMigrations {
|
|||
await this._db.meta.setMeta("viewsIdxMigrationCompleted", "1");
|
||||
}
|
||||
|
||||
/** Rename Field.controlSymbol to Field.controlIcon and FieldOption.symbol to FieldOption.icon */
|
||||
async _fieldIconsMigration(): Promise<void> {
|
||||
if(await this._db.meta.getMeta("fieldIconsMigrationCompleted") == "1")
|
||||
return;
|
||||
|
||||
console.log("DB migration: Rename Field.controlSymbol to Field.controlIcon and FieldOption.symbol to FieldOption.icon");
|
||||
|
||||
const allTypes = await this._db.types.TypeModel.findAll({
|
||||
attributes: ["id", "fields"]
|
||||
});
|
||||
|
||||
for (const type of allTypes) {
|
||||
let fields = type.fields;
|
||||
let fieldsChanged = false;
|
||||
for (const field of fields) {
|
||||
if ("controlSymbol" in field) {
|
||||
field.controlIcon = field.controlSymbol as any;
|
||||
delete field.controlSymbol;
|
||||
fieldsChanged = true;
|
||||
}
|
||||
|
||||
for (const option of field.options ?? []) {
|
||||
if ("symbol" in option) {
|
||||
option.icon = option.symbol as any;
|
||||
delete option.symbol;
|
||||
fieldsChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fieldsChanged) {
|
||||
await type.update({
|
||||
fields
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this._db.meta.setMeta("fieldIconsMigrationCompleted", "1");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export default class DatabaseSearch {
|
|||
{ padId },
|
||||
where(fn("lower", col(`${kind}.name`)), {[Op.like]: `%${searchText.toLowerCase()}%`})
|
||||
),
|
||||
attributes: [ "id", "name", "typeId" ].concat(kind == "Marker" ? [ "pos", "lat", "lon", "symbol" ] : [ "top", "left", "bottom", "right" ])
|
||||
attributes: [ "id", "name", "typeId" ].concat(kind == "Marker" ? [ "pos", "lat", "lon", "icon" ] : [ "top", "left", "bottom", "right" ])
|
||||
});
|
||||
|
||||
return objs.map((obj) => ({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type CreationOptional, DataTypes, type ForeignKey, type InferAttributes, type InferCreationAttributes, Model } from "sequelize";
|
||||
import { typeValidator, type CRU, type Field, type ID, type PadId, type Type, type Colour, type Size, type Symbol, type Shape, type Width, type Stroke, type RouteMode } from "facilmap-types";
|
||||
import { typeValidator, type CRU, type Field, type ID, type PadId, type Type, type Colour, type Size, type Icon, type Shape, type Width, type Stroke, type RouteMode } from "facilmap-types";
|
||||
import Database from "./database.js";
|
||||
import { createModel, getDefaultIdType, makeNotNullForeignKey } from "./helpers.js";
|
||||
import type { PadModel } from "./pad.js";
|
||||
|
@ -16,8 +16,8 @@ export interface TypeModel extends Model<InferAttributes<TypeModel>, InferCreati
|
|||
colourFixed: boolean;
|
||||
defaultSize: Size;
|
||||
sizeFixed: boolean;
|
||||
defaultSymbol: Symbol;
|
||||
symbolFixed: boolean;
|
||||
defaultIcon: Icon;
|
||||
iconFixed: boolean;
|
||||
defaultShape: Shape;
|
||||
shapeFixed: boolean;
|
||||
defaultWidth: Width;
|
||||
|
@ -54,8 +54,8 @@ export default class DatabaseTypes {
|
|||
colourFixed: { type: DataTypes.BOOLEAN, allowNull: false },
|
||||
defaultSize: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false },
|
||||
sizeFixed: { type: DataTypes.BOOLEAN, allowNull: false },
|
||||
defaultSymbol: { type: DataTypes.TEXT, allowNull: false },
|
||||
symbolFixed: { type: DataTypes.BOOLEAN, allowNull: false },
|
||||
defaultIcon: { type: DataTypes.TEXT, allowNull: false },
|
||||
iconFixed: { type: DataTypes.BOOLEAN, allowNull: false },
|
||||
defaultShape: { type: DataTypes.TEXT, allowNull: false },
|
||||
shapeFixed: { type: DataTypes.BOOLEAN, allowNull: false },
|
||||
defaultWidth: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false },
|
||||
|
|
|
@ -61,7 +61,7 @@ function markerToGeoJson(marker: Marker): JsonStream {
|
|||
name: marker.name,
|
||||
colour: marker.colour,
|
||||
size: marker.size,
|
||||
symbol: marker.symbol,
|
||||
icon: marker.icon,
|
||||
shape: marker.shape,
|
||||
data: cloneDeep(marker.data),
|
||||
typeId: marker.typeId
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { type Socket as SocketIO } from "socket.io";
|
||||
import Database, { type DatabaseEvents } from "../database/database.js";
|
||||
import { type EventHandler, type EventName, type SocketRequestName, type SocketResponse, SocketVersion, type ValidatedSocketRequest, socketRequestValidators } from "facilmap-types";
|
||||
import { serializeError } from "serialize-error";
|
||||
import { type DatabaseEvents } from "../database/database.js";
|
||||
import { type EventHandler, type EventName, type SocketRequestName, type SocketResponse, SocketVersion, type ValidatedSocketRequest, type SocketServerToClientEmitArgs, type SocketEvents, type MultipleEvents } from "facilmap-types";
|
||||
|
||||
// Socket.io converts undefined to null. In socket handlers that allow a null response, let's also allow undefined.
|
||||
type FixedNullResponse<T> = null extends T ? T | undefined | void : T;
|
||||
|
@ -11,86 +9,25 @@ export type SocketHandlers<V extends SocketVersion> = {
|
|||
};
|
||||
|
||||
export type DatabaseHandlers = {
|
||||
[eventName in EventName<DatabaseEvents>]?: EventHandler<DatabaseEvents, eventName>;
|
||||
[eventName in EventName<Pick<DatabaseEvents, keyof DatabaseEvents>>]?: EventHandler<Pick<DatabaseEvents, keyof DatabaseEvents>, eventName>;
|
||||
}
|
||||
|
||||
export abstract class SocketConnection {
|
||||
socket: SocketIO;
|
||||
database: Database;
|
||||
clearDatabaseHandlers: () => void = () => undefined;
|
||||
export interface SocketConnection<V extends SocketVersion> {
|
||||
getSocketHandlers(): SocketHandlers<V>;
|
||||
handleDisconnect(): void;
|
||||
}
|
||||
|
||||
constructor(socket: SocketIO, database: Database) {
|
||||
this.socket = socket;
|
||||
this.database = database;
|
||||
|
||||
this.socket.on("error", (err) => {
|
||||
this.handleError(err);
|
||||
});
|
||||
this.socket.on("disconnect", () => {
|
||||
this.handleDisconnect();
|
||||
});
|
||||
|
||||
this.registerSocketHandlers();
|
||||
this.registerDatabaseHandlers();
|
||||
}
|
||||
|
||||
abstract getSocketHandlers(): SocketHandlers<SocketVersion>;
|
||||
|
||||
abstract getVersion(): SocketVersion;
|
||||
|
||||
abstract getDatabaseHandlers(): DatabaseHandlers;
|
||||
|
||||
getSocketRequestValidators(): typeof socketRequestValidators[SocketVersion] {
|
||||
return socketRequestValidators[this.getVersion()];
|
||||
}
|
||||
|
||||
registerSocketHandlers(): void {
|
||||
const socketHandlers = this.getSocketHandlers();
|
||||
const validators = this.getSocketRequestValidators();
|
||||
for (const i of Object.keys(socketHandlers) as Array<keyof SocketHandlers<SocketVersion>>) {
|
||||
this.socket.on(i, async (data: unknown, callback: unknown): Promise<void> => {
|
||||
const validatedCallback = typeof callback === 'function' ? callback : undefined;
|
||||
|
||||
try {
|
||||
const validatedData = validators[i].parse(data);
|
||||
const res = await (socketHandlers[i] as any)(validatedData);
|
||||
|
||||
if(!validatedCallback && res)
|
||||
console.trace("No callback available to send result of socket handler " + i);
|
||||
|
||||
validatedCallback?.(null, res);
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
|
||||
validatedCallback?.(serializeError(err));
|
||||
export function mapMultipleEvents<VIn extends SocketVersion, VOut extends SocketVersion>(events: MultipleEvents<SocketEvents<VIn>>, mapper: (...args: SocketServerToClientEmitArgs<VIn>) => Array<SocketServerToClientEmitArgs<VOut>>): MultipleEvents<SocketEvents<VOut>> {
|
||||
const result: any = {};
|
||||
for (const [oldEventName, oldEvents] of Object.entries(events)) {
|
||||
for (const oldEvent of oldEvents) {
|
||||
for (const [newEventName, ...newEvent] of (mapper as any)(oldEventName, oldEvent)) {
|
||||
if (!result[newEventName]) {
|
||||
result[newEventName] = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerDatabaseHandlers(): void {
|
||||
this.clearDatabaseHandlers();
|
||||
|
||||
const handlers = this.getDatabaseHandlers();
|
||||
|
||||
for (const eventName of Object.keys(handlers) as Array<EventName<DatabaseEvents>>) {
|
||||
this.database.on(eventName as any, handlers[eventName] as any);
|
||||
}
|
||||
|
||||
this.clearDatabaseHandlers = () => {
|
||||
for (const eventName of Object.keys(handlers) as Array<EventName<DatabaseEvents>>) {
|
||||
this.database.removeListener(eventName as any, handlers[eventName] as any);
|
||||
result[newEventName].push(newEvent[0]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleError(err: Error): void {
|
||||
console.error("Error! Disconnecting client.");
|
||||
console.error(err);
|
||||
this.socket.disconnect();
|
||||
}
|
||||
|
||||
handleDisconnect(): void {
|
||||
this.clearDatabaseHandlers();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
|
@ -1,8 +1,53 @@
|
|||
import { SocketVersion, type Marker, type SocketEvents, type MultipleEvents, type PadData, type Line, type FindPadsResult, type FindOnMapResult, type FindOnMapMarker, type FindOnMapLine, type SocketClientToServerEvents, type SocketServerToClientEvents } from "facilmap-types";
|
||||
import { SocketVersion, type SocketEvents, type MultipleEvents, type PadData, type Line, type FindPadsResult, type FindOnMapLine, type SocketServerToClientEmitArgs, type LegacyV2FindOnMapMarker, type LegacyV2Marker, type LegacyV2FindOnMapResult } from "facilmap-types";
|
||||
import { SocketConnectionV2 } from "./socket-v2";
|
||||
import type { DatabaseHandlers, SocketHandlers } from "./socket-common";
|
||||
import { mapMultipleEvents, type SocketConnection, type SocketHandlers } from "./socket-common";
|
||||
import { normalizeLineName, normalizeMarkerName, normalizePadName } from "facilmap-utils";
|
||||
import { type Socket as SocketIO } from "socket.io";
|
||||
import type Database from "../database/database";
|
||||
|
||||
function preparePadData<P extends PadData | FindPadsResult>(padData: P): P {
|
||||
return {
|
||||
...padData,
|
||||
name: normalizePadName(padData.name)
|
||||
};
|
||||
}
|
||||
|
||||
function prepareMarker<M extends LegacyV2Marker | LegacyV2FindOnMapMarker>(marker: M): M {
|
||||
return {
|
||||
...marker,
|
||||
name: normalizeMarkerName(marker.name)
|
||||
};
|
||||
}
|
||||
|
||||
function prepareLine<L extends Line | FindOnMapLine>(line: L): L {
|
||||
return {
|
||||
...line,
|
||||
name: normalizeLineName(line.name)
|
||||
};
|
||||
}
|
||||
|
||||
function prepareMapResult(result: LegacyV2FindOnMapResult): LegacyV2FindOnMapResult {
|
||||
if (result.kind === "marker") {
|
||||
return prepareMarker(result);
|
||||
} else {
|
||||
return prepareLine(result);
|
||||
}
|
||||
}
|
||||
|
||||
function prepareEvent(...args: SocketServerToClientEmitArgs<SocketVersion.V2>): Array<SocketServerToClientEmitArgs<SocketVersion.V1>> {
|
||||
if (args[0] === "line") {
|
||||
return [["line", prepareLine(args[1])]];
|
||||
} else if (args[0] === "marker") {
|
||||
return [["marker", prepareMarker(args[1])]];
|
||||
} else if (args[0] === "padData") {
|
||||
return [["padData", preparePadData(args[1])]];
|
||||
} else {
|
||||
return [args];
|
||||
}
|
||||
}
|
||||
|
||||
function prepareMultiple(events: MultipleEvents<SocketEvents<SocketVersion.V2>>): MultipleEvents<SocketEvents<SocketVersion.V1>> {
|
||||
return mapMultipleEvents(events, prepareEvent);
|
||||
}
|
||||
|
||||
function mapResult<Input, Output, Args extends any[]>(func: (...args: Args) => Input | PromiseLike<Input>, mapper: (result: Input) => Output): (...args: Args) => Promise<Output> {
|
||||
return async (...args) => {
|
||||
|
@ -11,82 +56,41 @@ function mapResult<Input, Output, Args extends any[]>(func: (...args: Args) => I
|
|||
};
|
||||
}
|
||||
|
||||
export class SocketConnectionV1 extends SocketConnectionV2 {
|
||||
declare socket: SocketIO<SocketClientToServerEvents<SocketVersion.V2>, SocketServerToClientEvents<SocketVersion.V2>>;;
|
||||
export class SocketConnectionV1 implements SocketConnection<SocketVersion.V1> {
|
||||
socketV2: SocketConnectionV2;
|
||||
|
||||
override getVersion(): SocketVersion {
|
||||
return SocketVersion.V1;
|
||||
constructor(emit: (...args: SocketServerToClientEmitArgs<SocketVersion.V1>) => void, database: Database, remoteAddr: string) {
|
||||
this.socketV2 = new SocketConnectionV2((...args) => {
|
||||
for (const ev of prepareEvent(...args)) {
|
||||
emit(...ev);
|
||||
}
|
||||
}, database, remoteAddr);
|
||||
}
|
||||
|
||||
override getSocketHandlers(): SocketHandlers<SocketVersion.V1> {
|
||||
const socketHandlers = super.getSocketHandlers();
|
||||
getSocketHandlers(): SocketHandlers<SocketVersion.V1> {
|
||||
const socketHandlers = this.socketV2.getSocketHandlers();
|
||||
|
||||
return {
|
||||
...socketHandlers,
|
||||
setPadId: mapResult(socketHandlers.setPadId, (events) => this.prepareMultiple(events)),
|
||||
updateBbox: mapResult(socketHandlers.updateBbox, (events) => this.prepareMultiple(events)),
|
||||
getPad: mapResult(socketHandlers.getPad, (result) => result ? this.preparePadData(result) : result),
|
||||
findPads: mapResult(socketHandlers.findPads, (result) => ({ ...result, results: result.results.map((r) => this.preparePadData(r)) })),
|
||||
createPad: mapResult(socketHandlers.createPad, (events) => this.prepareMultiple(events)),
|
||||
editPad: mapResult(socketHandlers.editPad, (padData) => this.preparePadData(padData)),
|
||||
getMarker: mapResult(socketHandlers.getMarker, (marker) => this.prepareMarker(marker)),
|
||||
addMarker: mapResult(socketHandlers.addMarker, (marker) => this.prepareMarker(marker)),
|
||||
editMarker: mapResult(socketHandlers.editMarker, (marker) => this.prepareMarker(marker)),
|
||||
deleteMarker: mapResult(socketHandlers.deleteMarker, (marker) => this.prepareMarker(marker)),
|
||||
addLine: mapResult(socketHandlers.addLine, (line) => this.prepareLine(line)),
|
||||
editLine: mapResult(socketHandlers.editLine, (line) => this.prepareLine(line)),
|
||||
deleteLine: mapResult(socketHandlers.deleteLine, (line) => this.prepareLine(line)),
|
||||
findOnMap: mapResult(socketHandlers.findOnMap, (results) => results.map((r) => this.prepareMapResult(r)))
|
||||
setPadId: mapResult(socketHandlers.setPadId, (events) => prepareMultiple(events)),
|
||||
updateBbox: mapResult(socketHandlers.updateBbox, (events) => prepareMultiple(events)),
|
||||
getPad: mapResult(socketHandlers.getPad, (result) => result ? preparePadData(result) : result),
|
||||
findPads: mapResult(socketHandlers.findPads, (result) => ({ ...result, results: result.results.map((r) => preparePadData(r)) })),
|
||||
createPad: mapResult(socketHandlers.createPad, (events) => prepareMultiple(events)),
|
||||
editPad: mapResult(socketHandlers.editPad, (padData) => preparePadData(padData)),
|
||||
getMarker: mapResult(socketHandlers.getMarker, (marker) => prepareMarker(marker)),
|
||||
addMarker: mapResult(socketHandlers.addMarker, (marker) => prepareMarker(marker)),
|
||||
editMarker: mapResult(socketHandlers.editMarker, (marker) => prepareMarker(marker)),
|
||||
deleteMarker: mapResult(socketHandlers.deleteMarker, (marker) => prepareMarker(marker)),
|
||||
addLine: mapResult(socketHandlers.addLine, (line) => prepareLine(line)),
|
||||
editLine: mapResult(socketHandlers.editLine, (line) => prepareLine(line)),
|
||||
deleteLine: mapResult(socketHandlers.deleteLine, (line) => prepareLine(line)),
|
||||
findOnMap: mapResult(socketHandlers.findOnMap, (results) => results.map((r) => prepareMapResult(r)))
|
||||
};
|
||||
}
|
||||
|
||||
override getDatabaseHandlers(): DatabaseHandlers {
|
||||
const databaseHandlers = super.getDatabaseHandlers();
|
||||
return {
|
||||
...databaseHandlers,
|
||||
...databaseHandlers.line ? { line: (padId, data) => { databaseHandlers.line!(padId, this.prepareLine(data)); } } : {},
|
||||
...databaseHandlers.marker ? { marker: (padId, data) => { databaseHandlers.marker!(padId, this.prepareMarker(data)); } } : {},
|
||||
...databaseHandlers.padData ? { padData: (padId, data) => { databaseHandlers.padData!(padId, this.preparePadData(data)); } } : {}
|
||||
};
|
||||
}
|
||||
|
||||
prepareMultiple(events: MultipleEvents<SocketEvents<SocketVersion.V1>>): MultipleEvents<SocketEvents<SocketVersion.V1>> {
|
||||
return {
|
||||
...events,
|
||||
...(events.padData ? { padData: events.padData.map((p) => this.preparePadData(p)) } : {}),
|
||||
...(events.marker ? { marker: events.marker.map((m) => this.prepareMarker(m)) } : {}),
|
||||
...(events.line ? { line: events.line.map((l) => this.prepareLine(l)) } : {})
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
preparePadData<P extends PadData | FindPadsResult>(padData: P): P {
|
||||
return {
|
||||
...padData,
|
||||
name: normalizePadName(padData.name)
|
||||
};
|
||||
}
|
||||
|
||||
prepareMarker<M extends Marker | FindOnMapMarker>(marker: M): M {
|
||||
return {
|
||||
...marker,
|
||||
name: normalizeMarkerName(marker.name)
|
||||
};
|
||||
}
|
||||
|
||||
prepareLine<L extends Line | FindOnMapLine>(line: L): L {
|
||||
return {
|
||||
...line,
|
||||
name: normalizeLineName(line.name)
|
||||
};
|
||||
}
|
||||
|
||||
prepareMapResult(result: FindOnMapResult): FindOnMapResult {
|
||||
if (result.kind === "marker") {
|
||||
return this.prepareMarker(result);
|
||||
} else {
|
||||
return this.prepareLine(result);
|
||||
}
|
||||
handleDisconnect(): void {
|
||||
this.socketV2.handleDisconnect();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,672 +1,66 @@
|
|||
import { promiseProps, type PromiseMap } from "../utils/utils.js";
|
||||
import { asyncIteratorToArray, streamToString } from "../utils/streams.js";
|
||||
import { isInBbox } from "../utils/geo.js";
|
||||
import { Socket, type Socket as SocketIO } from "socket.io";
|
||||
import { exportLineToRouteGpx, exportLineToTrackGpx } from "../export/gpx.js";
|
||||
import { find } from "../search.js";
|
||||
import { geoipLookup } from "../geoip.js";
|
||||
import { cloneDeep, isEqual, omit } from "lodash-es";
|
||||
import Database from "../database/database.js";
|
||||
import { type Bbox, type BboxWithZoom, type SocketEvents, type MultipleEvents, type PadData, type PadId, SocketVersion, Writable, type SocketClientToServerEvents, type SocketServerToClientEvents, PadNotFoundError } from "facilmap-types";
|
||||
import { calculateRoute, prepareForBoundingBox } from "../routing/routing.js";
|
||||
import type { RouteWithId } from "../database/route.js";
|
||||
import { SocketConnection, type DatabaseHandlers, type SocketHandlers } from "./socket-common";
|
||||
import { getI18n, setDomainUnits } from "../i18n.js";
|
||||
import { SocketVersion, type SocketEvents, type MultipleEvents, type FindOnMapResult, type SocketServerToClientEmitArgs, legacyV2MarkerToCurrent, currentMarkerToLegacyV2, currentTypeToLegacyV2, legacyV2TypeToCurrent } from "facilmap-types";
|
||||
import { mapMultipleEvents, type SocketConnection, type SocketHandlers } from "./socket-common";
|
||||
import { SocketConnectionV3 } from "./socket-v3";
|
||||
import type Database from "../database/database";
|
||||
|
||||
export type MultipleEventPromises = {
|
||||
[eventName in keyof MultipleEvents<SocketEvents<SocketVersion.V2>>]: PromiseLike<MultipleEvents<SocketEvents<SocketVersion.V2>>[eventName]> | MultipleEvents<SocketEvents<SocketVersion.V2>>[eventName];
|
||||
function prepareEvent(...args: SocketServerToClientEmitArgs<SocketVersion.V3>): Array<SocketServerToClientEmitArgs<SocketVersion.V2>> {
|
||||
if (args[0] === "marker") {
|
||||
return [[args[0], currentMarkerToLegacyV2(args[1])]];
|
||||
} else if (args[0] === "type") {
|
||||
return [[args[0], currentTypeToLegacyV2(args[1])]];
|
||||
} else {
|
||||
return [args];
|
||||
}
|
||||
}
|
||||
|
||||
function isPadId(padId: PadId | true | undefined): padId is PadId {
|
||||
return !!(padId && padId !== true);
|
||||
function prepareMultiple(events: MultipleEvents<SocketEvents<SocketVersion.V3>>): MultipleEvents<SocketEvents<SocketVersion.V2>> {
|
||||
return mapMultipleEvents(events, prepareEvent);
|
||||
}
|
||||
|
||||
export class SocketConnectionV2 extends SocketConnection {
|
||||
|
||||
declare socket: Socket<SocketClientToServerEvents<SocketVersion.V2>, SocketServerToClientEvents<SocketVersion.V2>>;
|
||||
padId: PadId | true | undefined = undefined;
|
||||
bbox: BboxWithZoom | undefined = undefined;
|
||||
writable: Writable | undefined = undefined;
|
||||
route: Omit<RouteWithId, "trackPoints"> | undefined = undefined;
|
||||
routes: Record<string, Omit<RouteWithId, "trackPoints">> = { };
|
||||
listeningToHistory = false;
|
||||
pauseHistoryListener = 0;
|
||||
|
||||
constructor(socket: SocketIO<SocketClientToServerEvents<SocketVersion.V2>, SocketServerToClientEvents<SocketVersion.V2>>, database: Database) {
|
||||
super(socket, database);
|
||||
function prepareMapResultOutput(result: FindOnMapResult) {
|
||||
if (result.kind === "marker") {
|
||||
return currentMarkerToLegacyV2(result);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
override getVersion(): SocketVersion {
|
||||
return SocketVersion.V2;
|
||||
}
|
||||
export class SocketConnectionV2 implements SocketConnection<SocketVersion.V2> {
|
||||
socketV3: SocketConnectionV3;
|
||||
|
||||
getPadObjects(padData: PadData & { writable: Writable }): Promise<MultipleEvents<SocketEvents<SocketVersion.V2>>> {
|
||||
const promises: PromiseMap<MultipleEvents<SocketEvents<SocketVersion.V2>>> = {
|
||||
padData: [ padData ],
|
||||
view: asyncIteratorToArray(this.database.views.getViews(padData.id)),
|
||||
type: asyncIteratorToArray(this.database.types.getTypes(padData.id)),
|
||||
line: asyncIteratorToArray(this.database.lines.getPadLines(padData.id))
|
||||
};
|
||||
|
||||
if(this.bbox) { // In case bbox is set while fetching pad data
|
||||
Object.assign(promises, {
|
||||
marker: asyncIteratorToArray(this.database.markers.getPadMarkers(padData.id, this.bbox)),
|
||||
linePoints: asyncIteratorToArray(this.database.lines.getLinePointsForPad(padData.id, this.bbox))
|
||||
});
|
||||
}
|
||||
|
||||
return promiseProps(promises);
|
||||
}
|
||||
|
||||
validatePermissions(minimumPermissions: Writable): void {
|
||||
if (minimumPermissions == Writable.ADMIN && ![Writable.ADMIN].includes(this.writable!))
|
||||
throw new Error(getI18n().t("socket.only-in-admin-error"));
|
||||
else if (minimumPermissions === Writable.WRITE && ![Writable.ADMIN, Writable.WRITE].includes(this.writable!))
|
||||
throw new Error(getI18n().t("socket.only-in-write-error"));
|
||||
}
|
||||
|
||||
override handleDisconnect(): void {
|
||||
if(this.route) {
|
||||
this.database.routes.deleteRoute(this.route.id).catch((err) => {
|
||||
console.error("Error clearing route", err);
|
||||
});
|
||||
}
|
||||
|
||||
for (const routeId of Object.keys(this.routes)) {
|
||||
this.database.routes.deleteRoute(this.routes[routeId].id).catch((err) => {
|
||||
console.error("Error clearing route", err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
override getSocketHandlers(): SocketHandlers<SocketVersion.V2> {
|
||||
return {
|
||||
setPadId: async (padId) => {
|
||||
if(this.padId != null)
|
||||
throw new Error(getI18n().t("socket.pad-id-set-error"));
|
||||
|
||||
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 = omit({ ...write, writable: Writable.WRITE }, ["adminId"]);
|
||||
else if(read)
|
||||
pad = omit({ ...read, writable: Writable.READ }, ["writeId", "adminId"]);
|
||||
else {
|
||||
this.padId = undefined;
|
||||
throw new PadNotFoundError(getI18n().t("socket.pad-not-exist-error"));
|
||||
}
|
||||
|
||||
this.padId = pad.id;
|
||||
this.writable = pad.writable;
|
||||
|
||||
this.registerDatabaseHandlers();
|
||||
|
||||
return await this.getPadObjects(pad);
|
||||
},
|
||||
|
||||
setLanguage: async (settings) => {
|
||||
if (settings.lang) {
|
||||
await getI18n().changeLanguage(settings.lang);
|
||||
}
|
||||
if (settings.units) {
|
||||
setDomainUnits(settings.units);
|
||||
}
|
||||
},
|
||||
|
||||
updateBbox: async (bbox) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
const markerBboxWithExcept: BboxWithZoom & { except?: Bbox } = { ...bbox };
|
||||
const lineBboxWithExcept: BboxWithZoom & { except?: Bbox } = { ...bbox };
|
||||
if(this.bbox) {
|
||||
markerBboxWithExcept.except = this.bbox;
|
||||
if (bbox.zoom == this.bbox.zoom) {
|
||||
lineBboxWithExcept.except = this.bbox;
|
||||
}
|
||||
}
|
||||
|
||||
this.bbox = bbox;
|
||||
|
||||
const ret: MultipleEventPromises = {};
|
||||
|
||||
if(this.padId && this.padId !== true) {
|
||||
ret.marker = asyncIteratorToArray(this.database.markers.getPadMarkers(this.padId, markerBboxWithExcept));
|
||||
ret.linePoints = asyncIteratorToArray(this.database.lines.getLinePointsForPad(this.padId, lineBboxWithExcept));
|
||||
}
|
||||
if(this.route)
|
||||
ret.routePoints = this.database.routes.getRoutePoints(this.route.id, lineBboxWithExcept, !lineBboxWithExcept.except).then((points) => ([points]));
|
||||
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, lineBboxWithExcept, !lineBboxWithExcept.except).then((trackPoints) => ({ routeId, trackPoints }))
|
||||
));
|
||||
}
|
||||
|
||||
return await promiseProps(ret);
|
||||
},
|
||||
|
||||
getPad: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
const padData = await this.database.pads.getPadDataByAnyId(data.padId);
|
||||
return padData && {
|
||||
id: padData.id,
|
||||
name: padData.name,
|
||||
description: padData.description
|
||||
};
|
||||
},
|
||||
|
||||
findPads: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
return this.database.pads.findPads(data);
|
||||
},
|
||||
|
||||
createPad: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
if(this.padId)
|
||||
throw new Error(getI18n().t("socket.pad-already-loaded-error"));
|
||||
|
||||
const padData = await this.database.pads.createPad(data);
|
||||
|
||||
this.padId = padData.id;
|
||||
this.writable = Writable.ADMIN;
|
||||
|
||||
this.registerDatabaseHandlers();
|
||||
|
||||
return await this.getPadObjects({ ...padData, writable: Writable.ADMIN });
|
||||
},
|
||||
|
||||
editPad: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return {
|
||||
...await this.database.pads.updatePadData(this.padId, data),
|
||||
writable: this.writable!
|
||||
};
|
||||
},
|
||||
|
||||
deletePad: async () => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
await this.database.pads.deletePad(this.padId);
|
||||
},
|
||||
|
||||
getMarker: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.markers.getMarker(this.padId, data.id);
|
||||
},
|
||||
|
||||
addMarker: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.markers.createMarker(this.padId, data);
|
||||
},
|
||||
|
||||
editMarker: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return this.database.markers.updateMarker(this.padId, data.id, data);
|
||||
},
|
||||
|
||||
deleteMarker: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return this.database.markers.deleteMarker(this.padId, data.id);
|
||||
},
|
||||
|
||||
getLineTemplate: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.lines.getLineTemplate(this.padId, data);
|
||||
},
|
||||
|
||||
addLine: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
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 asyncIteratorToArray(this.database.routes.getAllRoutePoints(route.id)) };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await this.database.lines.createLine(this.padId, data, fromRoute);
|
||||
},
|
||||
|
||||
editLine: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
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 asyncIteratorToArray(this.database.routes.getAllRoutePoints(route.id)) };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await this.database.lines.updateLine(this.padId, data.id, data, undefined, fromRoute);
|
||||
},
|
||||
|
||||
deleteLine: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return this.database.lines.deleteLine(this.padId, data.id);
|
||||
},
|
||||
|
||||
exportLine: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
const lineP = this.database.lines.getLine(this.padId, data.id);
|
||||
lineP.catch(() => null); // Avoid unhandled promise error (https://stackoverflow.com/a/59062117/242365)
|
||||
|
||||
const [line, type] = await Promise.all([
|
||||
lineP,
|
||||
lineP.then((line) => this.database.types.getType(this.padId as string, line.typeId))
|
||||
]);
|
||||
|
||||
switch(data.format) {
|
||||
case "gpx-trk":
|
||||
return await streamToString(exportLineToTrackGpx(line, type, this.database.lines.getAllLinePoints(line.id)));
|
||||
case "gpx-rte":
|
||||
return await streamToString(exportLineToRouteGpx(line, type));
|
||||
default:
|
||||
throw new Error(getI18n().t("socket.unknown-format"));
|
||||
}
|
||||
},
|
||||
|
||||
addView: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.views.createView(this.padId, data);
|
||||
},
|
||||
|
||||
editView: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.views.updateView(this.padId, data.id, data);
|
||||
},
|
||||
|
||||
deleteView: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.views.deleteView(this.padId, data.id);
|
||||
},
|
||||
|
||||
addType: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.types.createType(this.padId, data);
|
||||
},
|
||||
|
||||
editType: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.types.updateType(this.padId, data.id, data);
|
||||
},
|
||||
|
||||
deleteType: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.types.deleteType(this.padId, data.id);
|
||||
},
|
||||
|
||||
find: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
return await find(data.query, data.loadUrls);
|
||||
},
|
||||
|
||||
findOnMap: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.search.search(this.padId, data.query);
|
||||
},
|
||||
|
||||
getRoute: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
return await calculateRoute(data.destinations, data.mode);
|
||||
},
|
||||
|
||||
setRoute: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
const existingRoute = data.routeId ? this.routes[data.routeId] : this.route;
|
||||
|
||||
let routeInfo;
|
||||
if(existingRoute)
|
||||
routeInfo = await this.database.routes.updateRoute(existingRoute.id, data.routePoints, data.mode);
|
||||
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;
|
||||
}
|
||||
|
||||
if (data.routeId)
|
||||
this.routes[data.routeId] = omit(routeInfo, "trackPoints");
|
||||
else
|
||||
this.route = omit(routeInfo, "trackPoints");
|
||||
|
||||
if(this.bbox)
|
||||
routeInfo.trackPoints = prepareForBoundingBox(routeInfo.trackPoints, this.bbox, true);
|
||||
else
|
||||
routeInfo.trackPoints = [];
|
||||
|
||||
return {
|
||||
routeId: data.routeId,
|
||||
top: routeInfo.top,
|
||||
left: routeInfo.left,
|
||||
bottom: routeInfo.bottom,
|
||||
right: routeInfo.right,
|
||||
routePoints: routeInfo.routePoints,
|
||||
mode: routeInfo.mode,
|
||||
time: routeInfo.time,
|
||||
distance: routeInfo.distance,
|
||||
ascent: routeInfo.ascent,
|
||||
descent: routeInfo.descent,
|
||||
extraInfo: routeInfo.extraInfo,
|
||||
trackPoints: routeInfo.trackPoints
|
||||
};
|
||||
},
|
||||
|
||||
clearRoute: async (data) => {
|
||||
if (data) {
|
||||
this.validatePermissions(Writable.READ);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
lineToRoute: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
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");
|
||||
|
||||
if(this.bbox)
|
||||
routeInfo.trackPoints = prepareForBoundingBox(routeInfo.trackPoints, this.bbox, true);
|
||||
else
|
||||
routeInfo.trackPoints = [];
|
||||
|
||||
return {
|
||||
routeId: data.routeId,
|
||||
top: routeInfo.top,
|
||||
left: routeInfo.left,
|
||||
bottom: routeInfo.bottom,
|
||||
right: routeInfo.right,
|
||||
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.validatePermissions(Writable.READ);
|
||||
|
||||
const route = data.routeId ? this.routes[data.routeId] : this.route;
|
||||
if (!route) {
|
||||
throw new Error(getI18n().t("route-not-available-error"));
|
||||
}
|
||||
|
||||
const routeInfo = { ...route, name: getI18n().t("socket.route-name"), data: {} };
|
||||
|
||||
switch(data.format) {
|
||||
case "gpx-trk":
|
||||
return await streamToString(exportLineToTrackGpx(routeInfo, undefined, this.database.routes.getAllRoutePoints(route.id)));
|
||||
case "gpx-rte":
|
||||
return await streamToString(exportLineToRouteGpx(routeInfo, undefined));
|
||||
default:
|
||||
throw new Error(getI18n().t("socket.unknown-format"));
|
||||
}
|
||||
},
|
||||
|
||||
listenToHistory: async () => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
if(this.listeningToHistory)
|
||||
throw new Error(getI18n().t("socket.already-listening-to-history-error"));
|
||||
|
||||
this.listeningToHistory = true;
|
||||
this.registerDatabaseHandlers();
|
||||
|
||||
return promiseProps({
|
||||
history: asyncIteratorToArray(this.database.history.getHistory(this.padId, this.writable == Writable.ADMIN ? undefined : ["Marker", "Line"]))
|
||||
});
|
||||
},
|
||||
|
||||
stopListeningToHistory: () => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if(!this.listeningToHistory)
|
||||
throw new Error(getI18n().t("socket.not-listening-to-history-error"));
|
||||
|
||||
this.listeningToHistory = false;
|
||||
this.registerDatabaseHandlers();
|
||||
},
|
||||
|
||||
revertHistoryEntry: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
const historyEntry = await this.database.history.getHistoryEntry(this.padId, data.id);
|
||||
|
||||
if(!["Marker", "Line"].includes(historyEntry.type) && this.writable != Writable.ADMIN)
|
||||
throw new Error(getI18n().t("socket.admin-revert-error"));
|
||||
|
||||
this.pauseHistoryListener++;
|
||||
|
||||
try {
|
||||
await this.database.history.revertHistoryEntry(this.padId, data.id);
|
||||
} finally {
|
||||
this.pauseHistoryListener--;
|
||||
}
|
||||
|
||||
return promiseProps({
|
||||
history: asyncIteratorToArray(this.database.history.getHistory(this.padId, this.writable == Writable.ADMIN ? undefined : ["Marker", "Line"]))
|
||||
});
|
||||
},
|
||||
|
||||
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) : undefined;
|
||||
constructor(emit: (...args: SocketServerToClientEmitArgs<SocketVersion.V2>) => void, database: Database, remoteAddr: string) {
|
||||
this.socketV3 = new SocketConnectionV3((...args) => {
|
||||
for (const ev of prepareEvent(...args)) {
|
||||
emit(...ev);
|
||||
}
|
||||
}, database, remoteAddr);
|
||||
}
|
||||
|
||||
/*copyPad : function(data, callback) {
|
||||
if(!stripObject(data, { toId: "string" }))
|
||||
return callback("Invalid parameters.");
|
||||
getSocketHandlers(): SocketHandlers<SocketVersion.V2> {
|
||||
const socketHandlers = this.socketV3.getSocketHandlers();
|
||||
|
||||
this.database.copyPad(this.padId, data.toId, callback);
|
||||
}*/
|
||||
return {
|
||||
...socketHandlers,
|
||||
|
||||
addMarker: async (marker) => currentMarkerToLegacyV2(await socketHandlers.addMarker(legacyV2MarkerToCurrent(marker))),
|
||||
editMarker: async (marker) => currentMarkerToLegacyV2(await socketHandlers.editMarker(legacyV2MarkerToCurrent(marker))),
|
||||
addType: async (type) => currentTypeToLegacyV2(await socketHandlers.addType(legacyV2TypeToCurrent(type))),
|
||||
editType: async (type) => currentTypeToLegacyV2(await socketHandlers.editType(legacyV2TypeToCurrent(type))),
|
||||
|
||||
updateBbox: async (bbox) => prepareMultiple(await socketHandlers.updateBbox(bbox)),
|
||||
createPad: async (padData) => prepareMultiple(await socketHandlers.createPad(padData)),
|
||||
listenToHistory: async (data) => prepareMultiple(await socketHandlers.listenToHistory(data)),
|
||||
revertHistoryEntry: async (entry) => prepareMultiple(await socketHandlers.revertHistoryEntry(entry)),
|
||||
getMarker: async (data) => currentMarkerToLegacyV2(await socketHandlers.getMarker(data)),
|
||||
deleteMarker: async (data) => currentMarkerToLegacyV2(await socketHandlers.deleteMarker(data)),
|
||||
findOnMap: async (data) => (await socketHandlers.findOnMap(data)).map((result) => prepareMapResultOutput(result)),
|
||||
deleteType: async (data) => currentTypeToLegacyV2(await socketHandlers.deleteType(data)),
|
||||
setPadId: async (padId) => prepareMultiple(await socketHandlers.setPadId(padId))
|
||||
};
|
||||
}
|
||||
|
||||
override getDatabaseHandlers(): DatabaseHandlers {
|
||||
return {
|
||||
...(this.padId ? {
|
||||
line: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.socket.emit("line", data);
|
||||
},
|
||||
|
||||
linePoints: (padId, lineId, trackPoints) => {
|
||||
if(padId == this.padId)
|
||||
this.socket.emit("linePoints", { reset: true, id: lineId, trackPoints : (this.bbox ? prepareForBoundingBox(trackPoints, this.bbox) : [ ]) });
|
||||
},
|
||||
|
||||
deleteLine: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.socket.emit("deleteLine", data);
|
||||
},
|
||||
|
||||
marker: (padId, data) => {
|
||||
if(padId == this.padId && this.bbox && isInBbox(data, this.bbox))
|
||||
this.socket.emit("marker", data);
|
||||
},
|
||||
|
||||
deleteMarker: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.socket.emit("deleteMarker", data);
|
||||
},
|
||||
|
||||
type: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.socket.emit("type", data);
|
||||
},
|
||||
|
||||
deleteType: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.socket.emit("deleteType", data);
|
||||
},
|
||||
|
||||
padData: (padId, data) => {
|
||||
if(padId == this.padId) {
|
||||
const dataClone = cloneDeep(data);
|
||||
if(this.writable == Writable.READ)
|
||||
delete dataClone.writeId;
|
||||
if(this.writable != Writable.ADMIN)
|
||||
delete dataClone.adminId;
|
||||
|
||||
this.padId = data.id;
|
||||
|
||||
this.socket.emit("padData", {
|
||||
...dataClone,
|
||||
writable: this.writable!
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deletePad: (padId) => {
|
||||
if (padId == this.padId) {
|
||||
this.socket.emit("deletePad");
|
||||
this.writable = Writable.READ;
|
||||
}
|
||||
},
|
||||
|
||||
view: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.socket.emit("view", data);
|
||||
},
|
||||
|
||||
deleteView: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.socket.emit("deleteView", data);
|
||||
},
|
||||
|
||||
...(this.listeningToHistory ? {
|
||||
addHistoryEntry: (padId, data) => {
|
||||
if(padId == this.padId && (this.writable == Writable.ADMIN || ["Marker", "Line"].includes(data.type)) && !this.pauseHistoryListener)
|
||||
this.socket.emit("history", data);
|
||||
}
|
||||
} : {})
|
||||
|
||||
} : {})
|
||||
};
|
||||
handleDisconnect(): void {
|
||||
this.socketV3.handleDisconnect();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,693 @@
|
|||
import { promiseProps, type PromiseMap } from "../utils/utils.js";
|
||||
import { asyncIteratorToArray, streamToString } from "../utils/streams.js";
|
||||
import { isInBbox } from "../utils/geo.js";
|
||||
import { exportLineToRouteGpx, exportLineToTrackGpx } from "../export/gpx.js";
|
||||
import { find } from "../search.js";
|
||||
import { geoipLookup } from "../geoip.js";
|
||||
import { cloneDeep, isEqual, omit } from "lodash-es";
|
||||
import Database, { type DatabaseEvents } from "../database/database.js";
|
||||
import { type Bbox, type BboxWithZoom, type SocketEvents, type MultipleEvents, type PadData, type PadId, SocketVersion, Writable, PadNotFoundError, type SocketServerToClientEmitArgs, type EventName } from "facilmap-types";
|
||||
import { calculateRoute, prepareForBoundingBox } from "../routing/routing.js";
|
||||
import type { RouteWithId } from "../database/route.js";
|
||||
import { type SocketConnection, type DatabaseHandlers, type SocketHandlers } from "./socket-common.js";
|
||||
import { getI18n, setDomainUnits } from "../i18n.js";
|
||||
|
||||
export type MultipleEventPromises = {
|
||||
[eventName in keyof MultipleEvents<SocketEvents<SocketVersion.V3>>]: PromiseLike<MultipleEvents<SocketEvents<SocketVersion.V3>>[eventName]> | MultipleEvents<SocketEvents<SocketVersion.V3>>[eventName];
|
||||
}
|
||||
|
||||
function isPadId(padId: PadId | true | undefined): padId is PadId {
|
||||
return !!(padId && padId !== true);
|
||||
}
|
||||
|
||||
export class SocketConnectionV3 implements SocketConnection<SocketVersion.V3> {
|
||||
|
||||
emit: (...args: SocketServerToClientEmitArgs<SocketVersion.V3>) => void;
|
||||
database: Database;
|
||||
remoteAddr: string;
|
||||
|
||||
padId: PadId | true | undefined = undefined;
|
||||
bbox: BboxWithZoom | undefined = undefined;
|
||||
writable: Writable | undefined = undefined;
|
||||
route: Omit<RouteWithId, "trackPoints"> | undefined = undefined;
|
||||
routes: Record<string, Omit<RouteWithId, "trackPoints">> = { };
|
||||
listeningToHistory = false;
|
||||
pauseHistoryListener = 0;
|
||||
|
||||
unregisterDatabaseHandlers = (): void => undefined;
|
||||
|
||||
constructor(emit: (...args: SocketServerToClientEmitArgs<SocketVersion.V3>) => void, database: Database, remoteAddr: string) {
|
||||
this.emit = emit;
|
||||
this.database = database;
|
||||
this.remoteAddr = remoteAddr;
|
||||
|
||||
this.registerDatabaseHandlers();
|
||||
}
|
||||
|
||||
getPadObjects(padData: PadData & { writable: Writable }): Promise<MultipleEvents<SocketEvents<SocketVersion.V3>>> {
|
||||
const promises: PromiseMap<MultipleEvents<SocketEvents<SocketVersion.V3>>> = {
|
||||
padData: [ padData ],
|
||||
view: asyncIteratorToArray(this.database.views.getViews(padData.id)),
|
||||
type: asyncIteratorToArray(this.database.types.getTypes(padData.id)),
|
||||
line: asyncIteratorToArray(this.database.lines.getPadLines(padData.id))
|
||||
};
|
||||
|
||||
if(this.bbox) { // In case bbox is set while fetching pad data
|
||||
Object.assign(promises, {
|
||||
marker: asyncIteratorToArray(this.database.markers.getPadMarkers(padData.id, this.bbox)),
|
||||
linePoints: asyncIteratorToArray(this.database.lines.getLinePointsForPad(padData.id, this.bbox))
|
||||
});
|
||||
}
|
||||
|
||||
return promiseProps(promises);
|
||||
}
|
||||
|
||||
validatePermissions(minimumPermissions: Writable): void {
|
||||
if (minimumPermissions == Writable.ADMIN && ![Writable.ADMIN].includes(this.writable!))
|
||||
throw new Error(getI18n().t("socket.only-in-admin-error"));
|
||||
else if (minimumPermissions === Writable.WRITE && ![Writable.ADMIN, Writable.WRITE].includes(this.writable!))
|
||||
throw new Error(getI18n().t("socket.only-in-write-error"));
|
||||
}
|
||||
|
||||
handleDisconnect(): void {
|
||||
if(this.route) {
|
||||
this.database.routes.deleteRoute(this.route.id).catch((err) => {
|
||||
console.error("Error clearing route", err);
|
||||
});
|
||||
}
|
||||
|
||||
for (const routeId of Object.keys(this.routes)) {
|
||||
this.database.routes.deleteRoute(this.routes[routeId].id).catch((err) => {
|
||||
console.error("Error clearing route", err);
|
||||
});
|
||||
}
|
||||
|
||||
this.unregisterDatabaseHandlers();
|
||||
};
|
||||
|
||||
getSocketHandlers(): SocketHandlers<SocketVersion.V3> {
|
||||
return {
|
||||
setPadId: async (padId) => {
|
||||
if(this.padId != null)
|
||||
throw new Error(getI18n().t("socket.pad-id-set-error"));
|
||||
|
||||
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 = omit({ ...write, writable: Writable.WRITE }, ["adminId"]);
|
||||
else if(read)
|
||||
pad = omit({ ...read, writable: Writable.READ }, ["writeId", "adminId"]);
|
||||
else {
|
||||
this.padId = undefined;
|
||||
throw new PadNotFoundError(getI18n().t("socket.pad-not-exist-error"));
|
||||
}
|
||||
|
||||
this.padId = pad.id;
|
||||
this.writable = pad.writable;
|
||||
|
||||
this.registerDatabaseHandlers();
|
||||
|
||||
return await this.getPadObjects(pad);
|
||||
},
|
||||
|
||||
setLanguage: async (settings) => {
|
||||
if (settings.lang) {
|
||||
await getI18n().changeLanguage(settings.lang);
|
||||
}
|
||||
if (settings.units) {
|
||||
setDomainUnits(settings.units);
|
||||
}
|
||||
},
|
||||
|
||||
updateBbox: async (bbox) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
const markerBboxWithExcept: BboxWithZoom & { except?: Bbox } = { ...bbox };
|
||||
const lineBboxWithExcept: BboxWithZoom & { except?: Bbox } = { ...bbox };
|
||||
if(this.bbox) {
|
||||
markerBboxWithExcept.except = this.bbox;
|
||||
if (bbox.zoom == this.bbox.zoom) {
|
||||
lineBboxWithExcept.except = this.bbox;
|
||||
}
|
||||
}
|
||||
|
||||
this.bbox = bbox;
|
||||
|
||||
const ret: MultipleEventPromises = {};
|
||||
|
||||
if(this.padId && this.padId !== true) {
|
||||
ret.marker = asyncIteratorToArray(this.database.markers.getPadMarkers(this.padId, markerBboxWithExcept));
|
||||
ret.linePoints = asyncIteratorToArray(this.database.lines.getLinePointsForPad(this.padId, lineBboxWithExcept));
|
||||
}
|
||||
if(this.route)
|
||||
ret.routePoints = this.database.routes.getRoutePoints(this.route.id, lineBboxWithExcept, !lineBboxWithExcept.except).then((points) => ([points]));
|
||||
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, lineBboxWithExcept, !lineBboxWithExcept.except).then((trackPoints) => ({ routeId, trackPoints }))
|
||||
));
|
||||
}
|
||||
|
||||
return await promiseProps(ret);
|
||||
},
|
||||
|
||||
getPad: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
const padData = await this.database.pads.getPadDataByAnyId(data.padId);
|
||||
return padData && {
|
||||
id: padData.id,
|
||||
name: padData.name,
|
||||
description: padData.description
|
||||
};
|
||||
},
|
||||
|
||||
findPads: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
return this.database.pads.findPads(data);
|
||||
},
|
||||
|
||||
createPad: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
if(this.padId)
|
||||
throw new Error(getI18n().t("socket.pad-already-loaded-error"));
|
||||
|
||||
const padData = await this.database.pads.createPad(data);
|
||||
|
||||
this.padId = padData.id;
|
||||
this.writable = Writable.ADMIN;
|
||||
|
||||
this.registerDatabaseHandlers();
|
||||
|
||||
return await this.getPadObjects({ ...padData, writable: Writable.ADMIN });
|
||||
},
|
||||
|
||||
editPad: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return {
|
||||
...await this.database.pads.updatePadData(this.padId, data),
|
||||
writable: this.writable!
|
||||
};
|
||||
},
|
||||
|
||||
deletePad: async () => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
await this.database.pads.deletePad(this.padId);
|
||||
},
|
||||
|
||||
getMarker: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.markers.getMarker(this.padId, data.id);
|
||||
},
|
||||
|
||||
addMarker: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.markers.createMarker(this.padId, data);
|
||||
},
|
||||
|
||||
editMarker: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return this.database.markers.updateMarker(this.padId, data.id, data);
|
||||
},
|
||||
|
||||
deleteMarker: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return this.database.markers.deleteMarker(this.padId, data.id);
|
||||
},
|
||||
|
||||
getLineTemplate: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.lines.getLineTemplate(this.padId, data);
|
||||
},
|
||||
|
||||
addLine: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
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 asyncIteratorToArray(this.database.routes.getAllRoutePoints(route.id)) };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await this.database.lines.createLine(this.padId, data, fromRoute);
|
||||
},
|
||||
|
||||
editLine: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
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 asyncIteratorToArray(this.database.routes.getAllRoutePoints(route.id)) };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await this.database.lines.updateLine(this.padId, data.id, data, undefined, fromRoute);
|
||||
},
|
||||
|
||||
deleteLine: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return this.database.lines.deleteLine(this.padId, data.id);
|
||||
},
|
||||
|
||||
exportLine: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
const lineP = this.database.lines.getLine(this.padId, data.id);
|
||||
lineP.catch(() => null); // Avoid unhandled promise error (https://stackoverflow.com/a/59062117/242365)
|
||||
|
||||
const [line, type] = await Promise.all([
|
||||
lineP,
|
||||
lineP.then((line) => this.database.types.getType(this.padId as string, line.typeId))
|
||||
]);
|
||||
|
||||
switch(data.format) {
|
||||
case "gpx-trk":
|
||||
return await streamToString(exportLineToTrackGpx(line, type, this.database.lines.getAllLinePoints(line.id)));
|
||||
case "gpx-rte":
|
||||
return await streamToString(exportLineToRouteGpx(line, type));
|
||||
default:
|
||||
throw new Error(getI18n().t("socket.unknown-format"));
|
||||
}
|
||||
},
|
||||
|
||||
addView: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.views.createView(this.padId, data);
|
||||
},
|
||||
|
||||
editView: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.views.updateView(this.padId, data.id, data);
|
||||
},
|
||||
|
||||
deleteView: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.views.deleteView(this.padId, data.id);
|
||||
},
|
||||
|
||||
addType: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.types.createType(this.padId, data);
|
||||
},
|
||||
|
||||
editType: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.types.updateType(this.padId, data.id, data);
|
||||
},
|
||||
|
||||
deleteType: async (data) => {
|
||||
this.validatePermissions(Writable.ADMIN);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.types.deleteType(this.padId, data.id);
|
||||
},
|
||||
|
||||
find: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
return await find(data.query, data.loadUrls);
|
||||
},
|
||||
|
||||
findOnMap: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
return await this.database.search.search(this.padId, data.query);
|
||||
},
|
||||
|
||||
getRoute: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
return await calculateRoute(data.destinations, data.mode);
|
||||
},
|
||||
|
||||
setRoute: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
const existingRoute = data.routeId ? this.routes[data.routeId] : this.route;
|
||||
|
||||
let routeInfo;
|
||||
if(existingRoute)
|
||||
routeInfo = await this.database.routes.updateRoute(existingRoute.id, data.routePoints, data.mode);
|
||||
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;
|
||||
}
|
||||
|
||||
if (data.routeId)
|
||||
this.routes[data.routeId] = omit(routeInfo, "trackPoints");
|
||||
else
|
||||
this.route = omit(routeInfo, "trackPoints");
|
||||
|
||||
if(this.bbox)
|
||||
routeInfo.trackPoints = prepareForBoundingBox(routeInfo.trackPoints, this.bbox, true);
|
||||
else
|
||||
routeInfo.trackPoints = [];
|
||||
|
||||
return {
|
||||
routeId: data.routeId,
|
||||
top: routeInfo.top,
|
||||
left: routeInfo.left,
|
||||
bottom: routeInfo.bottom,
|
||||
right: routeInfo.right,
|
||||
routePoints: routeInfo.routePoints,
|
||||
mode: routeInfo.mode,
|
||||
time: routeInfo.time,
|
||||
distance: routeInfo.distance,
|
||||
ascent: routeInfo.ascent,
|
||||
descent: routeInfo.descent,
|
||||
extraInfo: routeInfo.extraInfo,
|
||||
trackPoints: routeInfo.trackPoints
|
||||
};
|
||||
},
|
||||
|
||||
clearRoute: async (data) => {
|
||||
if (data) {
|
||||
this.validatePermissions(Writable.READ);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
lineToRoute: async (data) => {
|
||||
this.validatePermissions(Writable.READ);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
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");
|
||||
|
||||
if(this.bbox)
|
||||
routeInfo.trackPoints = prepareForBoundingBox(routeInfo.trackPoints, this.bbox, true);
|
||||
else
|
||||
routeInfo.trackPoints = [];
|
||||
|
||||
return {
|
||||
routeId: data.routeId,
|
||||
top: routeInfo.top,
|
||||
left: routeInfo.left,
|
||||
bottom: routeInfo.bottom,
|
||||
right: routeInfo.right,
|
||||
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.validatePermissions(Writable.READ);
|
||||
|
||||
const route = data.routeId ? this.routes[data.routeId] : this.route;
|
||||
if (!route) {
|
||||
throw new Error(getI18n().t("route-not-available-error"));
|
||||
}
|
||||
|
||||
const routeInfo = { ...route, name: getI18n().t("socket.route-name"), data: {} };
|
||||
|
||||
switch(data.format) {
|
||||
case "gpx-trk":
|
||||
return await streamToString(exportLineToTrackGpx(routeInfo, undefined, this.database.routes.getAllRoutePoints(route.id)));
|
||||
case "gpx-rte":
|
||||
return await streamToString(exportLineToRouteGpx(routeInfo, undefined));
|
||||
default:
|
||||
throw new Error(getI18n().t("socket.unknown-format"));
|
||||
}
|
||||
},
|
||||
|
||||
listenToHistory: async () => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
if(this.listeningToHistory)
|
||||
throw new Error(getI18n().t("socket.already-listening-to-history-error"));
|
||||
|
||||
this.listeningToHistory = true;
|
||||
this.registerDatabaseHandlers();
|
||||
|
||||
return promiseProps({
|
||||
history: asyncIteratorToArray(this.database.history.getHistory(this.padId, this.writable == Writable.ADMIN ? undefined : ["Marker", "Line"]))
|
||||
});
|
||||
},
|
||||
|
||||
stopListeningToHistory: () => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if(!this.listeningToHistory)
|
||||
throw new Error(getI18n().t("socket.not-listening-to-history-error"));
|
||||
|
||||
this.listeningToHistory = false;
|
||||
this.registerDatabaseHandlers();
|
||||
},
|
||||
|
||||
revertHistoryEntry: async (data) => {
|
||||
this.validatePermissions(Writable.WRITE);
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error(getI18n().t("socket.no-map-open-error"));
|
||||
|
||||
const historyEntry = await this.database.history.getHistoryEntry(this.padId, data.id);
|
||||
|
||||
if(!["Marker", "Line"].includes(historyEntry.type) && this.writable != Writable.ADMIN)
|
||||
throw new Error(getI18n().t("socket.admin-revert-error"));
|
||||
|
||||
this.pauseHistoryListener++;
|
||||
|
||||
try {
|
||||
await this.database.history.revertHistoryEntry(this.padId, data.id);
|
||||
} finally {
|
||||
this.pauseHistoryListener--;
|
||||
}
|
||||
|
||||
return promiseProps({
|
||||
history: asyncIteratorToArray(this.database.history.getHistory(this.padId, this.writable == Writable.ADMIN ? undefined : ["Marker", "Line"]))
|
||||
});
|
||||
},
|
||||
|
||||
geoip: async () => {
|
||||
return await geoipLookup(this.remoteAddr);
|
||||
}
|
||||
|
||||
/*copyPad : function(data, callback) {
|
||||
if(!stripObject(data, { toId: "string" }))
|
||||
return callback("Invalid parameters.");
|
||||
|
||||
this.database.copyPad(this.padId, data.toId, callback);
|
||||
}*/
|
||||
};
|
||||
}
|
||||
|
||||
getDatabaseHandlers(): DatabaseHandlers {
|
||||
return {
|
||||
...(this.padId ? {
|
||||
line: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.emit("line", data);
|
||||
},
|
||||
|
||||
linePoints: (padId, lineId, trackPoints) => {
|
||||
if(padId == this.padId)
|
||||
this.emit("linePoints", { reset: true, id: lineId, trackPoints : (this.bbox ? prepareForBoundingBox(trackPoints, this.bbox) : [ ]) });
|
||||
},
|
||||
|
||||
deleteLine: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.emit("deleteLine", data);
|
||||
},
|
||||
|
||||
marker: (padId, data) => {
|
||||
if(padId == this.padId && this.bbox && isInBbox(data, this.bbox))
|
||||
this.emit("marker", data);
|
||||
},
|
||||
|
||||
deleteMarker: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.emit("deleteMarker", data);
|
||||
},
|
||||
|
||||
type: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.emit("type", data);
|
||||
},
|
||||
|
||||
deleteType: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.emit("deleteType", data);
|
||||
},
|
||||
|
||||
padData: (padId, data) => {
|
||||
if(padId == this.padId) {
|
||||
const dataClone = cloneDeep(data);
|
||||
if(this.writable == Writable.READ)
|
||||
delete dataClone.writeId;
|
||||
if(this.writable != Writable.ADMIN)
|
||||
delete dataClone.adminId;
|
||||
|
||||
this.padId = data.id;
|
||||
|
||||
this.emit("padData", {
|
||||
...dataClone,
|
||||
writable: this.writable!
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deletePad: (padId) => {
|
||||
if (padId == this.padId) {
|
||||
this.emit("deletePad");
|
||||
this.writable = Writable.READ;
|
||||
}
|
||||
},
|
||||
|
||||
view: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.emit("view", data);
|
||||
},
|
||||
|
||||
deleteView: (padId, data) => {
|
||||
if(padId == this.padId)
|
||||
this.emit("deleteView", data);
|
||||
},
|
||||
|
||||
...(this.listeningToHistory ? {
|
||||
addHistoryEntry: (padId, data) => {
|
||||
if(padId == this.padId && (this.writable == Writable.ADMIN || ["Marker", "Line"].includes(data.type)) && !this.pauseHistoryListener)
|
||||
this.emit("history", data);
|
||||
}
|
||||
} : {})
|
||||
|
||||
} : {})
|
||||
};
|
||||
}
|
||||
|
||||
registerDatabaseHandlers(): void {
|
||||
this.unregisterDatabaseHandlers();
|
||||
|
||||
const handlers = this.getDatabaseHandlers();
|
||||
|
||||
for (const eventName of Object.keys(handlers) as Array<EventName<DatabaseEvents>>) {
|
||||
this.database.addListener(eventName as any, handlers[eventName] as any);
|
||||
}
|
||||
|
||||
this.unregisterDatabaseHandlers = () => {
|
||||
for (const eventName of Object.keys(handlers) as Array<EventName<DatabaseEvents>>) {
|
||||
this.database.removeListener(eventName as any, handlers[eventName] as any);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -2,17 +2,34 @@ import { Server, type Socket as SocketIO } from "socket.io";
|
|||
import domain from "domain";
|
||||
import Database from "../database/database.js";
|
||||
import type { Server as HttpServer } from "http";
|
||||
import { SocketVersion } from "facilmap-types";
|
||||
import type { SocketConnection } from "./socket-common";
|
||||
import { SocketVersion, socketRequestValidators, type SocketServerToClientEmitArgs } from "facilmap-types";
|
||||
import type { SocketConnection, SocketHandlers } from "./socket-common";
|
||||
import { SocketConnectionV1 } from "./socket-v1";
|
||||
import { SocketConnectionV2 } from "./socket-v2";
|
||||
import { SocketConnectionV2 } from "./socket-v2.js";
|
||||
import { handleSocketConnection } from "../i18n.js";
|
||||
import { serializeError } from "serialize-error";
|
||||
import { SocketConnectionV3 } from "./socket-v3.js";
|
||||
import proxyAddr from "proxy-addr";
|
||||
import config from "../config.js";
|
||||
|
||||
const constructors: Record<SocketVersion, new (socket: SocketIO, database: Database) => SocketConnection> = {
|
||||
const constructors: {
|
||||
[V in SocketVersion]: new (emit: (...args: SocketServerToClientEmitArgs<V>) => void, database: Database, remoteAttr: string) => SocketConnection<V>;
|
||||
} = {
|
||||
[SocketVersion.V1]: SocketConnectionV1,
|
||||
[SocketVersion.V2]: SocketConnectionV2
|
||||
[SocketVersion.V2]: SocketConnectionV2,
|
||||
[SocketVersion.V3]: SocketConnectionV3
|
||||
};
|
||||
|
||||
const trustProxy = config.trustProxy;
|
||||
const compiledTrust: (addr: string, i: number) => boolean = (
|
||||
// Imitate compileTrust from express (https://github.com/expressjs/express/blob/815f799310a5627c000d4a5156c1c958e4947b4c/lib/utils.js#L215)
|
||||
trustProxy == null ? () => false :
|
||||
typeof trustProxy === "function" ? trustProxy :
|
||||
typeof trustProxy === "boolean" ? () => trustProxy :
|
||||
typeof trustProxy === "number" ? (a, i) => i < trustProxy :
|
||||
proxyAddr.compile(trustProxy)
|
||||
);
|
||||
|
||||
export default class Socket {
|
||||
database: Database;
|
||||
|
||||
|
@ -48,7 +65,41 @@ export default class Socket {
|
|||
d.run(async () => {
|
||||
await handleSocketConnection(socket);
|
||||
}).then(() => {
|
||||
new constructors[version](socket, this.database);
|
||||
const remoteAttr = proxyAddr(socket.request, compiledTrust);
|
||||
|
||||
const handler = new constructors[version]((...args) => {
|
||||
socket.emit.apply(socket, args);
|
||||
}, this.database, remoteAttr);
|
||||
|
||||
socket.on("error", (err) => {
|
||||
console.error("Error! Disconnecting client.");
|
||||
console.error(err);
|
||||
socket.disconnect();
|
||||
});
|
||||
socket.on("disconnect", () => {
|
||||
handler.handleDisconnect();
|
||||
});
|
||||
|
||||
const socketHandlers = handler.getSocketHandlers();
|
||||
for (const i of Object.keys(socketHandlers) as Array<keyof SocketHandlers<SocketVersion>>) {
|
||||
socket.on(i, async (data: unknown, callback: unknown): Promise<void> => {
|
||||
const validatedCallback = typeof callback === 'function' ? callback : undefined;
|
||||
|
||||
try {
|
||||
const validatedData = socketRequestValidators[version][i].parse(data);
|
||||
const res = await (socketHandlers[i] as any)(validatedData);
|
||||
|
||||
if(!validatedCallback && res)
|
||||
console.trace("No callback available to send result of socket handler " + i);
|
||||
|
||||
validatedCallback?.(null, res);
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
|
||||
validatedCallback?.(serializeError(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((err) => {
|
||||
d.emit("error", err);
|
||||
});
|
||||
|
|
|
@ -4,11 +4,11 @@ import type { EventHandler, EventName } from "facilmap-types";
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
|
||||
interface AddListener<EventTypes extends Record<keyof EventTypes, any[]>, This> {
|
||||
interface AddListener<EventTypes extends Record<string, any[]>, This> {
|
||||
<E extends EventName<EventTypes>>(event: E, listener: EventHandler<EventTypes, E>): This;
|
||||
}
|
||||
|
||||
export class TypedEventEmitter<EventTypes extends Record<keyof EventTypes, any[]>> extends EventEmitter {
|
||||
export class TypedEventEmitter<EventTypes extends Record<string, any[]>> extends EventEmitter {
|
||||
|
||||
declare addListener: AddListener<EventTypes, this>;
|
||||
declare on: AddListener<EventTypes, this>;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "facilmap-types",
|
||||
"version": "4.1.1",
|
||||
"version": "5.0.0",
|
||||
"description": "Typescript typings for the FacilMap communication between client and server.",
|
||||
"homepage": "https://github.com/FacilMap/facilmap",
|
||||
"bugs": {
|
||||
|
|
|
@ -16,8 +16,8 @@ export type Colour = z.infer<typeof colourValidator>;
|
|||
export const sizeValidator = z.number().min(15);
|
||||
export type Size = z.infer<typeof sizeValidator>;
|
||||
|
||||
export const symbolValidator = z.string().trim();
|
||||
export type Symbol = z.infer<typeof symbolValidator>;
|
||||
export const iconValidator = z.string().trim();
|
||||
export type Icon = z.infer<typeof iconValidator>;
|
||||
|
||||
export const shapeValidator = z.string().trim();
|
||||
export type Shape = z.infer<typeof shapeValidator>;
|
||||
|
@ -79,4 +79,6 @@ export enum Units {
|
|||
METRIC = "metric",
|
||||
US_CUSTOMARY = "us_customary"
|
||||
}
|
||||
export const unitsValidator = z.nativeEnum(Units);
|
||||
export const unitsValidator = z.nativeEnum(Units);
|
||||
|
||||
export type ReplaceProperties<T1 extends Record<keyof any, any>, T2 extends Partial<Record<keyof T1, any>>> = Omit<T1, keyof T2> & T2;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export type EventName<Events extends Record<keyof Events, any[]>> = keyof Events & string;
|
||||
export type EventName<Events extends Record<string, any[]>> = keyof Events & string;
|
||||
|
||||
export type EventHandler<Events extends Record<keyof Events, any[]>, E extends EventName<Events>> = (...args: Events[E]) => void;
|
||||
export type EventHandler<Events extends Record<string, any[]>, E extends EventName<Events>> = (...args: Events[E]) => void;
|
||||
|
||||
export type MultipleEvents<Events extends Record<keyof Events, any[]>> = {
|
||||
export type MultipleEvents<Events extends Record<string, any[]>> = {
|
||||
[E in EventName<Events>]?: Array<Events[E][0]>;
|
||||
};
|
|
@ -8,6 +8,9 @@ export * from "./marker.js";
|
|||
export * from "./padData.js";
|
||||
export * from "./route.js";
|
||||
export * from "./searchResult.js";
|
||||
export * from "./socket/socket-v1.js";
|
||||
export * from "./socket/socket-v2.js";
|
||||
export * from "./socket/socket-v3.js";
|
||||
export * from "./socket/socket.js";
|
||||
export * from "./type.js";
|
||||
export * from "./view.js";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { colourValidator, idValidator, padIdValidator, pointValidator, shapeValidator, sizeValidator, symbolValidator } from "./base.js";
|
||||
import { colourValidator, idValidator, padIdValidator, pointValidator, shapeValidator, sizeValidator, iconValidator } from "./base.js";
|
||||
import { CRU, type CRUType, cruValidator, onlyRead, optionalUpdate, mapValues, optionalCreate } from "./cru";
|
||||
import * as z from "zod";
|
||||
|
||||
|
@ -8,7 +8,7 @@ export const markerValidator = cruValidator({
|
|||
...mapValues(pointValidator.shape, optionalUpdate),
|
||||
typeId: optionalUpdate(idValidator),
|
||||
name: optionalCreate(z.string().trim().max(100), ""),
|
||||
symbol: optionalCreate(symbolValidator), // defaults to type.defaultSymbol
|
||||
icon: optionalCreate(iconValidator), // defaults to type.defaultIcon
|
||||
shape: optionalCreate(shapeValidator), // defaults to type.defaultShape
|
||||
colour: optionalCreate(colourValidator), // defaults to type.defaultColour
|
||||
size: optionalCreate(sizeValidator), // defaults to type.defaultSize
|
||||
|
|
|
@ -53,7 +53,7 @@ export const findOnMapQueryValidator = z.object({
|
|||
});
|
||||
export type FindOnMapQuery = z.infer<typeof findOnMapQueryValidator>;
|
||||
|
||||
export type FindOnMapMarker = Pick<Marker, "id" | "name" | "typeId" | "lat" | "lon" | "symbol"> & { kind: "marker"; similarity: number };
|
||||
export type FindOnMapMarker = Pick<Marker, "id" | "name" | "typeId" | "lat" | "lon" | "icon"> & { kind: "marker"; similarity: number };
|
||||
export type FindOnMapLine = Pick<Line, "id" | "name" | "typeId" | "left" | "top" | "right" | "bottom"> & { kind: "line"; similarity: number };
|
||||
export type FindOnMapResult = FindOnMapMarker | FindOnMapLine;
|
||||
|
||||
|
@ -75,4 +75,21 @@ export const setLanguageRequestValidator = z.object({
|
|||
lang: z.string().optional(),
|
||||
units: unitsValidator.optional()
|
||||
});
|
||||
export type SetLanguageRequest = z.infer<typeof setLanguageRequestValidator>;
|
||||
export type SetLanguageRequest = z.infer<typeof setLanguageRequestValidator>;
|
||||
|
||||
export type ReplaceProperty<T extends Record<keyof any, any>, Key extends keyof T, Value> = Omit<T, Key> & Record<Key, Value>;
|
||||
|
||||
export type RenameProperty<T, From extends keyof any, To extends keyof any, KeepOld extends boolean = false> = T extends Record<From, any> ? (KeepOld extends true ? From : Omit<T, From>) & Record<To, T[From]> : T;
|
||||
|
||||
export function renameProperty<T, From extends keyof any, To extends keyof any, KeepOld extends boolean = false>(obj: T, from: From, to: To, keepOld?: KeepOld): RenameProperty<T, From, To, KeepOld> {
|
||||
if (from as any !== to as any && obj && typeof obj === "object" && from in obj && !(to in obj)) {
|
||||
const result = { ...obj } as any;
|
||||
result[to] = result[from];
|
||||
if (!keepOld) {
|
||||
delete result[from];
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return obj as any;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { requestDataValidatorsV2, type MapEventsV2, type ResponseDataMapV2 } from "./socket-v2";
|
||||
|
||||
// Socket v1:
|
||||
// - Marker name, line name and pad name is never an empty string but defaults to "Untitled marker", "Untitled line" and "Unnamed map"
|
||||
|
||||
export const requestDataValidatorsV1 = requestDataValidatorsV2;
|
||||
export type ResponseDataMapV1 = ResponseDataMapV2;
|
||||
export type MapEventsV1 = MapEventsV2;
|
|
@ -0,0 +1,161 @@
|
|||
import { idValidator, type ReplaceProperties } from "../base.js";
|
||||
import { markerValidator } from "../marker.js";
|
||||
import { refineRawTypeValidator, rawTypeValidator, fieldOptionValidator, refineRawFieldOptionsValidator, fieldValidator, refineRawFieldsValidator } from "../type.js";
|
||||
import type { MultipleEvents } from "../events.js";
|
||||
import { renameProperty, type FindOnMapMarker, type FindOnMapResult, type RenameProperty, type ReplaceProperty } from "./socket-common.js";
|
||||
import { requestDataValidatorsV3, type MapEventsV3, type ResponseDataMapV3 } from "./socket-v3.js";
|
||||
import type { CRU, CRUType } from "../cru.js";
|
||||
import * as z from "zod";
|
||||
|
||||
// Socket v2:
|
||||
// - “icon” is called “symbol” in `Marker.symbol`, `Type.defaultSymbol`, `Type.symbolFixed`, `Type.fields[].controlSymbol` and
|
||||
// `Type.fields[].options[].symbol`.
|
||||
|
||||
export const legacyV2MarkerValidator = {
|
||||
read: markerValidator.read.omit({ icon: true }).extend({ symbol: markerValidator.read.shape.icon }),
|
||||
create: markerValidator.create.omit({ icon: true }).extend({ symbol: markerValidator.create.shape.icon }),
|
||||
update: markerValidator.update.omit({ icon: true }).extend({ symbol: markerValidator.update.shape.icon })
|
||||
};
|
||||
export type LegacyV2Marker<Mode extends CRU = CRU.READ> = CRUType<Mode, typeof legacyV2MarkerValidator>;
|
||||
|
||||
export const legacyV2FieldOptionsValidator = refineRawFieldOptionsValidator({
|
||||
read: z.array(fieldOptionValidator.read.omit({ icon: true }).extend({ symbol: fieldOptionValidator.read.shape.icon })),
|
||||
create: z.array(fieldOptionValidator.create.omit({ icon: true }).extend({ symbol: fieldOptionValidator.create.shape.icon })),
|
||||
update: z.array(fieldOptionValidator.update.omit({ icon: true }).extend({ symbol: fieldOptionValidator.update.shape.icon }))
|
||||
});
|
||||
|
||||
export const legacyV2FieldsValidator = refineRawFieldsValidator({
|
||||
read: z.array(fieldValidator.read.omit({ controlIcon: true, options: true }).extend({
|
||||
controlSymbol: fieldValidator.read.shape.controlIcon,
|
||||
options: legacyV2FieldOptionsValidator.read.optional()
|
||||
})),
|
||||
create: z.array(fieldValidator.create.omit({ controlIcon: true, options: true }).extend({
|
||||
controlSymbol: fieldValidator.create.shape.controlIcon,
|
||||
options: legacyV2FieldOptionsValidator.create.optional()
|
||||
})),
|
||||
update: z.array(fieldValidator.update.omit({ controlIcon: true, options: true }).extend({
|
||||
controlSymbol: fieldValidator.update.shape.controlIcon,
|
||||
options: legacyV2FieldOptionsValidator.update.optional()
|
||||
}))
|
||||
});
|
||||
|
||||
export const legacyV2TypeValidator = refineRawTypeValidator({
|
||||
read: rawTypeValidator.read.omit({ defaultIcon: true, iconFixed: true, fields: true }).extend({
|
||||
defaultSymbol: rawTypeValidator.read.shape.defaultIcon,
|
||||
symbolFixed: rawTypeValidator.read.shape.iconFixed,
|
||||
fields: legacyV2FieldsValidator.read
|
||||
}),
|
||||
create: rawTypeValidator.create.omit({ defaultIcon: true, iconFixed: true }).extend({
|
||||
defaultSymbol: rawTypeValidator.create.shape.defaultIcon,
|
||||
symbolFixed: rawTypeValidator.create.shape.iconFixed,
|
||||
fields: legacyV2FieldsValidator.create
|
||||
}),
|
||||
update: rawTypeValidator.update.omit({ defaultIcon: true, iconFixed: true }).extend({
|
||||
defaultSymbol: rawTypeValidator.update.shape.defaultIcon,
|
||||
symbolFixed: rawTypeValidator.update.shape.iconFixed,
|
||||
fields: legacyV2FieldsValidator.update
|
||||
})
|
||||
});
|
||||
export type LegacyV2Type<Mode extends CRU = CRU.READ> = CRUType<Mode, typeof legacyV2TypeValidator>;
|
||||
|
||||
export type LegacyV2FindOnMapMarker = RenameProperty<FindOnMapMarker, "icon", "symbol", false>;
|
||||
export type LegacyV2FindOnMapResult = LegacyV2FindOnMapMarker | Exclude<FindOnMapResult, FindOnMapMarker>;
|
||||
|
||||
export const requestDataValidatorsV2 = {
|
||||
...requestDataValidatorsV3,
|
||||
addMarker: legacyV2MarkerValidator.create,
|
||||
editMarker: legacyV2MarkerValidator.update.extend({ id: idValidator }),
|
||||
addType: legacyV2TypeValidator.create,
|
||||
editType: legacyV2TypeValidator.update.extend({ id: idValidator })
|
||||
};
|
||||
|
||||
export type ResponseDataMapV2 = ReplaceProperties<ResponseDataMapV3, {
|
||||
updateBbox: MultipleEvents<MapEventsV2>;
|
||||
createPad: MultipleEvents<MapEventsV2>;
|
||||
listenToHistory: MultipleEvents<MapEventsV2>;
|
||||
revertHistoryEntry: MultipleEvents<MapEventsV2>;
|
||||
getMarker: LegacyV2Marker;
|
||||
addMarker: LegacyV2Marker;
|
||||
editMarker: LegacyV2Marker;
|
||||
deleteMarker: LegacyV2Marker;
|
||||
findOnMap: Array<LegacyV2FindOnMapResult>;
|
||||
addType: LegacyV2Type;
|
||||
editType: LegacyV2Type;
|
||||
deleteType: LegacyV2Type;
|
||||
setPadId: MultipleEvents<MapEventsV2>;
|
||||
}>;
|
||||
|
||||
export type MapEventsV2 = ReplaceProperties<MapEventsV3, {
|
||||
marker: [LegacyV2Marker];
|
||||
type: [LegacyV2Type];
|
||||
}>;
|
||||
|
||||
export function legacyV2MarkerToCurrent<M extends Record<keyof any, any>, KeepOld extends boolean = false>(marker: M, keepOld?: KeepOld): RenameProperty<M, "symbol", "icon", KeepOld> {
|
||||
return renameProperty(marker, "symbol", "icon", keepOld);
|
||||
}
|
||||
|
||||
export function currentMarkerToLegacyV2<M extends Record<keyof any, any>, KeepOld extends boolean = false>(marker: M, keepOld?: KeepOld): RenameProperty<M, "icon", "symbol", KeepOld> {
|
||||
return renameProperty(marker, "icon", "symbol", keepOld);
|
||||
}
|
||||
|
||||
type RenameNestedArrayProperty<T, Key extends keyof any, From extends keyof any, To extends keyof any, KeepOld extends boolean = false> = (
|
||||
T extends Record<Key, Array<Record<From, any>>>
|
||||
? ReplaceProperty<T, Key, Array<RenameProperty<T[Key][number], From, To, KeepOld>>>
|
||||
: T
|
||||
);
|
||||
type RenameNestedNestedArrayProperty<T, Key1 extends keyof any, Key2 extends keyof any, From extends keyof any, To extends keyof any, KeepOld extends boolean = false> = (
|
||||
T extends Record<Key1, Array<Record<Key2, Array<Record<From, any>>>>>
|
||||
? ReplaceProperty<T, Key1, Array<ReplaceProperty<T[Key1][number], Key2, RenameProperty<T[Key1][number][Key2][number], From, To, KeepOld>>>>
|
||||
: T
|
||||
);
|
||||
type RenameTypeProperties<
|
||||
T,
|
||||
DefaultIconKeyOld extends keyof any,
|
||||
DefaultIconKeyNew extends keyof any,
|
||||
IconFixedKeyOld extends keyof any,
|
||||
IconFixedKeyNew extends keyof any,
|
||||
ControlIconKeyOld extends keyof any,
|
||||
ControlIconKeyNew extends keyof any,
|
||||
OptionIconKeyOld extends keyof any,
|
||||
OptionIconKeyNew extends keyof any,
|
||||
KeepOld extends boolean = false
|
||||
> = RenameNestedNestedArrayProperty<
|
||||
RenameNestedArrayProperty<
|
||||
RenameProperty<
|
||||
RenameProperty<T, DefaultIconKeyOld, DefaultIconKeyNew, KeepOld>,
|
||||
IconFixedKeyOld, IconFixedKeyNew, KeepOld
|
||||
>, "fields", ControlIconKeyOld, ControlIconKeyNew, KeepOld
|
||||
>, "fields", "options", OptionIconKeyOld, OptionIconKeyNew, KeepOld
|
||||
>;
|
||||
|
||||
export function legacyV2TypeToCurrent<T extends Record<keyof any, any>, KeepOld extends boolean = false>(type: T, keepOld?: KeepOld): RenameTypeProperties<T, "defaultSymbol", "defaultIcon", "symbolFixed", "iconFixed", "controlSymbol", "controlIcon", "symbol", "icon", KeepOld> {
|
||||
const renamedType = renameProperty(renameProperty(type, "defaultSymbol", "defaultIcon", keepOld), "symbolFixed", "iconFixed", keepOld) as any;
|
||||
|
||||
if (renamedType.fields && Array.isArray(renamedType.fields)) {
|
||||
renamedType.fields = renamedType.fields.map((field: any) => {
|
||||
const renamedField = renameProperty(field, "controlSymbol", "controlIcon", keepOld);
|
||||
if (Array.isArray(renamedField?.options)) {
|
||||
renamedField.options = renamedField.options.map((option: any) => renameProperty(option, "symbol", "icon", keepOld));
|
||||
}
|
||||
return renamedField;
|
||||
});
|
||||
}
|
||||
|
||||
return renamedType;
|
||||
}
|
||||
|
||||
export function currentTypeToLegacyV2<T extends Record<keyof any, any>, KeepOld extends boolean = false>(type: T, keepOld?: KeepOld): RenameTypeProperties<T, "defaultIcon", "defaultSymbol", "iconFixed", "symbolFixed", "controlIcon", "controlSymbol", "icon", "symbol", KeepOld> {
|
||||
const renamedType = renameProperty(renameProperty(type, "defaultIcon", "defaultSymbol", keepOld), "iconFixed", "symbolFixed", keepOld) as any;
|
||||
|
||||
if (renamedType.fields && Array.isArray(renamedType.fields)) {
|
||||
renamedType.fields = renamedType.fields.map((field: any) => {
|
||||
const renamedField = renameProperty(field, "controlIcon", "controlSymbol", keepOld);
|
||||
if (Array.isArray(renamedField?.options)) {
|
||||
renamedField.options = renamedField.options.map((option: any) => renameProperty(option, "icon", "symbol", keepOld));
|
||||
}
|
||||
return renamedField;
|
||||
});
|
||||
}
|
||||
|
||||
return renamedType;
|
||||
}
|
|
@ -8,10 +8,10 @@ import { type View, viewValidator } from "../view.js";
|
|||
import type { MultipleEvents } from "../events.js";
|
||||
import type { SearchResult } from "../searchResult.js";
|
||||
import * as z from "zod";
|
||||
import { findPadsQueryValidator, getPadQueryValidator, type FindPadsResult, type PagedResults, type FindOnMapResult, lineTemplateRequestValidator, lineExportRequestValidator, findQueryValidator, findOnMapQueryValidator, routeExportRequestValidator, type LinePointsEvent, type RoutePointsEvent, nullOrUndefinedValidator, type LineTemplate, setLanguageRequestValidator } from "./socket-common";
|
||||
import type { HistoryEntry } from "../historyEntry";
|
||||
import { findPadsQueryValidator, getPadQueryValidator, type FindPadsResult, type PagedResults, type FindOnMapResult, lineTemplateRequestValidator, lineExportRequestValidator, findQueryValidator, findOnMapQueryValidator, routeExportRequestValidator, type LinePointsEvent, type RoutePointsEvent, nullOrUndefinedValidator, type LineTemplate, setLanguageRequestValidator } from "./socket-common.js";
|
||||
import type { HistoryEntry } from "../historyEntry.js";
|
||||
|
||||
export const requestDataValidatorsV2 = {
|
||||
export const requestDataValidatorsV3 = {
|
||||
updateBbox: bboxWithZoomValidator,
|
||||
getPad: getPadQueryValidator,
|
||||
findPads: findPadsQueryValidator,
|
||||
|
@ -48,16 +48,16 @@ export const requestDataValidatorsV2 = {
|
|||
setLanguage: setLanguageRequestValidator
|
||||
};
|
||||
|
||||
export interface ResponseDataMapV2 {
|
||||
updateBbox: MultipleEvents<MapEventsV2>;
|
||||
export interface ResponseDataMapV3 {
|
||||
updateBbox: MultipleEvents<MapEventsV3>;
|
||||
getPad: FindPadsResult | null;
|
||||
findPads: PagedResults<FindPadsResult>;
|
||||
createPad: MultipleEvents<MapEventsV2>;
|
||||
createPad: MultipleEvents<MapEventsV3>;
|
||||
editPad: PadData & { writable: Writable };
|
||||
deletePad: null;
|
||||
listenToHistory: MultipleEvents<MapEventsV2>;
|
||||
listenToHistory: MultipleEvents<MapEventsV3>;
|
||||
stopListeningToHistory: null;
|
||||
revertHistoryEntry: MultipleEvents<MapEventsV2>;
|
||||
revertHistoryEntry: MultipleEvents<MapEventsV3>;
|
||||
getMarker: Marker;
|
||||
addMarker: Marker;
|
||||
editMarker: Marker;
|
||||
|
@ -81,11 +81,11 @@ export interface ResponseDataMapV2 {
|
|||
editView: View;
|
||||
deleteView: View;
|
||||
geoip: Bbox | null;
|
||||
setPadId: MultipleEvents<MapEventsV2>;
|
||||
setPadId: MultipleEvents<MapEventsV3>;
|
||||
setLanguage: void;
|
||||
}
|
||||
|
||||
export interface MapEventsV2 {
|
||||
export interface MapEventsV3Interface {
|
||||
padData: [PadData & { writable: Writable }];
|
||||
deletePad: [];
|
||||
marker: [Marker];
|
||||
|
@ -102,10 +102,4 @@ export interface MapEventsV2 {
|
|||
history: [HistoryEntry];
|
||||
}
|
||||
|
||||
|
||||
// Socket v1:
|
||||
// - Marker name, line name and pad name is never an empty string but defaults to "Untitled marker", "Untitled line" and "Unnamed map"
|
||||
|
||||
export const requestDataValidatorsV1 = requestDataValidatorsV2;
|
||||
export type ResponseDataMapV1 = ResponseDataMapV2;
|
||||
export type MapEventsV1 = MapEventsV2;
|
||||
export type MapEventsV3 = Pick<MapEventsV3Interface, keyof MapEventsV3Interface>; // Workaround for https://github.com/microsoft/TypeScript/issues/15300
|
|
@ -1,16 +1,20 @@
|
|||
import * as z from "zod";
|
||||
import { requestDataValidatorsV1, requestDataValidatorsV2, type MapEventsV1, type ResponseDataMapV1, type ResponseDataMapV2, type MapEventsV2 } from "./socket-versions";
|
||||
import { requestDataValidatorsV3, type ResponseDataMapV3, type MapEventsV3 } from "./socket-v3";
|
||||
import { requestDataValidatorsV1, type MapEventsV1, type ResponseDataMapV1 } from "./socket-v1";
|
||||
import { requestDataValidatorsV2, type MapEventsV2, type ResponseDataMapV2 } from "./socket-v2";
|
||||
|
||||
export * from "./socket-common";
|
||||
|
||||
export enum SocketVersion {
|
||||
V1 = "v1",
|
||||
V2 = "v2"
|
||||
V2 = "v2",
|
||||
V3 = "v3"
|
||||
};
|
||||
|
||||
export const socketRequestValidators = {
|
||||
[SocketVersion.V1]: requestDataValidatorsV1,
|
||||
[SocketVersion.V2]: requestDataValidatorsV2
|
||||
[SocketVersion.V2]: requestDataValidatorsV2,
|
||||
[SocketVersion.V3]: requestDataValidatorsV3
|
||||
} satisfies Record<SocketVersion, Record<string, z.ZodType>>;
|
||||
|
||||
type SocketRequestMap<V extends SocketVersion> = {
|
||||
|
@ -24,11 +28,13 @@ type ValidatedSocketRequestMap<V extends SocketVersion> = {
|
|||
type SocketResponseMap<V extends SocketVersion> = {
|
||||
[SocketVersion.V1]: ResponseDataMapV1;
|
||||
[SocketVersion.V2]: ResponseDataMapV2;
|
||||
[SocketVersion.V3]: ResponseDataMapV3;
|
||||
}[V];
|
||||
|
||||
export type SocketEvents<V extends SocketVersion> = {
|
||||
[SocketVersion.V1]: MapEventsV1;
|
||||
[SocketVersion.V2]: MapEventsV2;
|
||||
[SocketVersion.V1]: Pick<MapEventsV1, keyof MapEventsV1>;
|
||||
[SocketVersion.V2]: Pick<MapEventsV2, keyof MapEventsV2>;
|
||||
[SocketVersion.V3]: Pick<MapEventsV3, keyof MapEventsV3>;
|
||||
}[V];
|
||||
|
||||
export type SocketRequestName<V extends SocketVersion> = keyof typeof socketRequestValidators[V];
|
||||
|
@ -45,4 +51,7 @@ export type SocketClientToServerEvents<V extends SocketVersion> = {
|
|||
|
||||
export type SocketServerToClientEvents<V extends SocketVersion> = {
|
||||
[E in keyof SocketEvents<V>]: (...args: SocketEvents<V>[E] extends Array<any> ? SocketEvents<V>[E] : never) => void;
|
||||
};
|
||||
};
|
||||
export type SocketServerToClientEmitArgs<V extends SocketVersion> = {
|
||||
[E in keyof SocketEvents<V>]: [e: E, ...args: SocketEvents<V>[E] extends Array<any> ? SocketEvents<V>[E] : never];
|
||||
}[keyof SocketEvents<V>];
|
|
@ -1,5 +1,5 @@
|
|||
import { colourValidator, idValidator, padIdValidator, routeModeValidator, shapeValidator, sizeValidator, strokeValidator, symbolValidator, widthValidator } from "./base.js";
|
||||
import { CRU, type CRUType, cruValidator, onlyUpdate, optionalCreate, exceptUpdate, optionalUpdate, onlyRead } from "./cru";
|
||||
import { colourValidator, idValidator, padIdValidator, routeModeValidator, shapeValidator, sizeValidator, strokeValidator, iconValidator, widthValidator } from "./base.js";
|
||||
import { CRU, type CRUType, cruValidator, onlyUpdate, optionalCreate, exceptUpdate, optionalUpdate, onlyRead, type CRUValidator } from "./cru";
|
||||
import * as z from "zod";
|
||||
|
||||
export const objectTypeValidator = z.enum(["marker", "line"]);
|
||||
|
@ -12,7 +12,7 @@ export const fieldOptionValidator = cruValidator({
|
|||
value: z.string().trim(),
|
||||
colour: colourValidator.optional(),
|
||||
size: sizeValidator.optional(),
|
||||
symbol: symbolValidator.optional(),
|
||||
icon: iconValidator.optional(),
|
||||
shape: shapeValidator.optional(),
|
||||
width: widthValidator.optional(),
|
||||
stroke: strokeValidator.optional(),
|
||||
|
@ -38,11 +38,27 @@ const noDuplicateOptionValues = (options: Array<FieldOption<CRU>>, ctx: z.Refine
|
|||
}
|
||||
}
|
||||
};
|
||||
export const fieldOptionsValidator = {
|
||||
read: z.array(fieldOptionValidator.read).superRefine(noDuplicateOptionValues),
|
||||
create: z.array(fieldOptionValidator.create).superRefine(noDuplicateOptionValues),
|
||||
update: z.array(fieldOptionValidator.update).superRefine(noDuplicateOptionValues)
|
||||
};
|
||||
/** Applies the "no duplicate option values" refinement to the given fieldOptionsValidator */
|
||||
export function refineRawFieldOptionsValidator<
|
||||
ReadValidator extends z.ZodTypeAny,
|
||||
CreateValidator extends z.ZodTypeAny,
|
||||
UpdateValidator extends z.ZodTypeAny
|
||||
>(rawFieldOptionsValidator: CRUValidator<ReadValidator, CreateValidator, UpdateValidator>): CRUValidator<
|
||||
z.ZodEffects<ReadValidator, z.output<ReadValidator>, z.input<ReadValidator>>,
|
||||
z.ZodEffects<CreateValidator, z.output<CreateValidator>, z.input<CreateValidator>>,
|
||||
z.ZodEffects<UpdateValidator, z.output<UpdateValidator>, z.input<UpdateValidator>>
|
||||
> {
|
||||
return {
|
||||
read: rawFieldOptionsValidator.read.superRefine(noDuplicateOptionValues),
|
||||
create: rawFieldOptionsValidator.create.superRefine(noDuplicateOptionValues),
|
||||
update: rawFieldOptionsValidator.update.superRefine(noDuplicateOptionValues)
|
||||
};
|
||||
}
|
||||
export const fieldOptionsValidator = refineRawFieldOptionsValidator({
|
||||
read: z.array(fieldOptionValidator.read),
|
||||
create: z.array(fieldOptionValidator.create),
|
||||
update: z.array(fieldOptionValidator.update)
|
||||
});
|
||||
|
||||
export const fieldValidator = cruValidator({
|
||||
name: z.string().trim().min(1),
|
||||
|
@ -50,7 +66,7 @@ export const fieldValidator = cruValidator({
|
|||
default: z.string().optional(),
|
||||
controlColour: z.boolean().optional(),
|
||||
controlSize: z.boolean().optional(),
|
||||
controlSymbol: z.boolean().optional(),
|
||||
controlIcon: z.boolean().optional(),
|
||||
controlShape: z.boolean().optional(),
|
||||
controlWidth: z.boolean().optional(),
|
||||
controlStroke: z.boolean().optional(),
|
||||
|
@ -83,13 +99,30 @@ const noDuplicateFieldNames = (fields: Array<Field<CRU>>, ctx: z.RefinementCtx)
|
|||
}
|
||||
}
|
||||
};
|
||||
export const fieldsValidator = {
|
||||
read: z.array(fieldValidator.read).superRefine(noDuplicateFieldNames),
|
||||
create: z.array(fieldValidator.create).superRefine(noDuplicateFieldNames),
|
||||
update: z.array(fieldValidator.update).superRefine(noDuplicateFieldNames)
|
||||
};
|
||||
/** Applies the "no duplicate field names" refinement to the given fieldsValidator. */
|
||||
export function refineRawFieldsValidator<
|
||||
ReadValidator extends z.ZodTypeAny,
|
||||
CreateValidator extends z.ZodTypeAny,
|
||||
UpdateValidator extends z.ZodTypeAny
|
||||
>(rawFieldsValidator: CRUValidator<ReadValidator, CreateValidator, UpdateValidator>): CRUValidator<
|
||||
z.ZodEffects<ReadValidator, z.output<ReadValidator>, z.input<ReadValidator>>,
|
||||
z.ZodEffects<CreateValidator, z.output<CreateValidator>, z.input<CreateValidator>>,
|
||||
z.ZodEffects<UpdateValidator, z.output<UpdateValidator>, z.input<UpdateValidator>>
|
||||
> {
|
||||
return {
|
||||
read: rawFieldsValidator.read.superRefine(noDuplicateFieldNames),
|
||||
create: rawFieldsValidator.create.superRefine(noDuplicateFieldNames),
|
||||
update: rawFieldsValidator.update.superRefine(noDuplicateFieldNames)
|
||||
};
|
||||
}
|
||||
export const fieldsValidator = refineRawFieldsValidator({
|
||||
read: z.array(fieldValidator.read),
|
||||
create: z.array(fieldValidator.create),
|
||||
update: z.array(fieldValidator.update)
|
||||
});
|
||||
|
||||
const rawTypeValidator = cruValidator({
|
||||
/** The type validator without the defaultColour default value applied. */
|
||||
export const rawTypeValidator = cruValidator({
|
||||
id: onlyRead(idValidator),
|
||||
type: exceptUpdate(objectTypeValidator),
|
||||
padId: onlyRead(padIdValidator),
|
||||
|
@ -101,8 +134,8 @@ const rawTypeValidator = cruValidator({
|
|||
colourFixed: optionalCreate(z.boolean(), false),
|
||||
defaultSize: optionalCreate(sizeValidator, 30),
|
||||
sizeFixed: optionalCreate(z.boolean(), false),
|
||||
defaultSymbol: optionalCreate(symbolValidator, ""),
|
||||
symbolFixed: optionalCreate(z.boolean(), false),
|
||||
defaultIcon: optionalCreate(iconValidator, ""),
|
||||
iconFixed: optionalCreate(z.boolean(), false),
|
||||
defaultShape: optionalCreate(shapeValidator, ""),
|
||||
shapeFixed: optionalCreate(z.boolean(), false),
|
||||
defaultWidth: optionalCreate(widthValidator, 4),
|
||||
|
@ -119,13 +152,25 @@ const rawTypeValidator = cruValidator({
|
|||
update: fieldsValidator.update.optional()
|
||||
}
|
||||
});
|
||||
export const typeValidator = {
|
||||
...rawTypeValidator,
|
||||
create: rawTypeValidator.create.transform((type) => {
|
||||
return {
|
||||
defaultColour: type.type === "marker" ? "ff0000" : "0000ff",
|
||||
...type
|
||||
};
|
||||
})
|
||||
};
|
||||
/** Applies the defaultColour default value to the given typeValidator */
|
||||
export function refineRawTypeValidator<
|
||||
ReadValidator extends z.ZodTypeAny,
|
||||
CreateValidator extends z.ZodTypeAny,
|
||||
UpdateValidator extends z.ZodTypeAny
|
||||
>(rawTypeValidator: CRUValidator<ReadValidator, CreateValidator, UpdateValidator>): CRUValidator<
|
||||
ReadValidator,
|
||||
z.ZodEffects<CreateValidator, Omit<z.output<CreateValidator>, "defaultColour"> & { defaultColour: string }, z.input<CreateValidator>>,
|
||||
UpdateValidator
|
||||
> {
|
||||
return {
|
||||
...rawTypeValidator,
|
||||
create: rawTypeValidator.create.transform((type) => {
|
||||
return {
|
||||
defaultColour: type.type === "marker" ? "ff0000" : "0000ff",
|
||||
...type
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
export const typeValidator = refineRawTypeValidator(rawTypeValidator);
|
||||
export type Type<Mode extends CRU = CRU.READ> = CRUType<Mode, typeof typeValidator>;
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "facilmap-utils",
|
||||
"version": "4.1.1",
|
||||
"version": "5.0.0",
|
||||
"description": "FacilMap helper functions used in both the frontend and backend.",
|
||||
"keywords": [
|
||||
"facilmap"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { compileExpression as filtrexCompileExpression } from "filtrex";
|
||||
import { flattenObject, getProperty, quoteRegExp } from "./utils.js";
|
||||
import type { ID, Marker, Line, Type, CRU } from "facilmap-types";
|
||||
import { type ID, type Marker, type Line, type Type, type CRU, currentMarkerToLegacyV2 } from "facilmap-types";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { normalizeFieldValue } from "./objects";
|
||||
|
||||
|
@ -110,20 +110,23 @@ export function makeTypeFilter(previousFilter: string = "", typeId: ID, filtered
|
|||
return ret;
|
||||
}
|
||||
|
||||
export function prepareObject<T extends Marker<CRU> | Line<CRU>>(obj: T, type: Type): T & { type?: Type["type"] } {
|
||||
obj = cloneDeep(obj);
|
||||
export function prepareObject(obj: Marker<CRU> | Line<CRU>, type: Type): any {
|
||||
const fixedObj: any = cloneDeep(obj);
|
||||
|
||||
if (!fixedObj.data) {
|
||||
fixedObj.data = Object.create(null) as {};
|
||||
}
|
||||
for (const field of type.fields) {
|
||||
if (Object.getPrototypeOf(obj.data)?.set)
|
||||
(obj.data as any).set(field.name, normalizeFieldValue(field, (obj.data as any).get(field.name)));
|
||||
else
|
||||
(obj.data as any)[field.name] = normalizeFieldValue(field, (obj.data as any)[field.name]);
|
||||
fixedObj.data[field.name] = normalizeFieldValue(field, fixedObj.data[field.name]);
|
||||
}
|
||||
|
||||
const ret = {
|
||||
...flattenObject(obj),
|
||||
...obj
|
||||
} as T & { type?: Type["type"] };
|
||||
let ret = {
|
||||
...flattenObject(fixedObj),
|
||||
...fixedObj
|
||||
};
|
||||
|
||||
// Backwards compatibility for filter expressions that were created before "symbol" was renamed to "icon" (keep old and new properties)
|
||||
ret = currentMarkerToLegacyV2(ret, true);
|
||||
|
||||
if(type)
|
||||
ret.type = type.type;
|
||||
|
|
|
@ -11,7 +11,7 @@ export function isLine<Mode extends CRU.READ | CRU.CREATE>(object: Marker<Mode>
|
|||
}
|
||||
|
||||
export function canControl<T extends Marker | Line = Marker | Line>(type: Type<CRU.READ | CRU.CREATE_VALIDATED>, ignoreField?: Field | null): Array<T extends any ? keyof T : never /* https://stackoverflow.com/a/62085569/242365 */> {
|
||||
const props: string[] = type.type == "marker" ? ["colour", "size", "symbol", "shape"] : type.type == "line" ? ["colour", "width", "stroke", "mode"] : [];
|
||||
const props: string[] = type.type == "marker" ? ["colour", "size", "icon", "shape"] : type.type == "line" ? ["colour", "width", "stroke", "mode"] : [];
|
||||
return props.filter((prop) => {
|
||||
if((type as any)[prop+"Fixed"] && ignoreField !== null)
|
||||
return false;
|
||||
|
@ -54,13 +54,13 @@ export function applyMarkerStyles(marker: Marker<CRU.READ | CRU.CREATE_VALIDATED
|
|||
update.colour = type.defaultColour;
|
||||
if(type.sizeFixed && marker.size != type.defaultSize)
|
||||
update.size = type.defaultSize;
|
||||
if(type.symbolFixed && marker.symbol != type.defaultSymbol)
|
||||
update.symbol = type.defaultSymbol;
|
||||
if(type.iconFixed && marker.icon != type.defaultIcon)
|
||||
update.icon = type.defaultIcon;
|
||||
if(type.shapeFixed && marker.shape != type.defaultShape)
|
||||
update.shape = type.defaultShape;
|
||||
|
||||
for(const field of type.fields) {
|
||||
if(field.controlColour || field.controlSize || field.controlSymbol || field.controlShape) {
|
||||
if(field.controlColour || field.controlSize || field.controlIcon || field.controlShape) {
|
||||
const option = getSelectedOption(field, marker.data?.[field.name]);
|
||||
|
||||
if(option) {
|
||||
|
@ -68,8 +68,8 @@ export function applyMarkerStyles(marker: Marker<CRU.READ | CRU.CREATE_VALIDATED
|
|||
update.colour = option.colour ?? type.defaultColour;
|
||||
if(field.controlSize && marker.size != (option.size ?? type.defaultSize))
|
||||
update.size = option.size ?? type.defaultSize;
|
||||
if(field.controlSymbol && marker.symbol != (option.symbol ?? type.defaultSymbol))
|
||||
update.symbol = option.symbol ?? type.defaultSymbol;
|
||||
if(field.controlIcon && marker.icon != (option.icon ?? type.defaultIcon))
|
||||
update.icon = option.icon ?? type.defaultIcon;
|
||||
if(field.controlShape && marker.shape != (option.shape ?? type.defaultShape))
|
||||
update.shape = option.shape ?? type.defaultShape;
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ export function resolveCreateMarker(marker: Marker<CRU.CREATE>, type: Type): Mar
|
|||
...parsed,
|
||||
colour: parsed.colour ?? type.defaultColour,
|
||||
size: parsed.size ?? type.defaultSize,
|
||||
symbol: parsed.symbol ?? type.defaultSymbol,
|
||||
icon: parsed.icon ?? type.defaultIcon,
|
||||
shape: parsed.shape ?? type.defaultShape
|
||||
};
|
||||
Object.assign(result, applyMarkerStyles(result, type));
|
||||
|
|
71
yarn.lock
71
yarn.lock
|
@ -1189,7 +1189,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/geojson@npm:*, @types/geojson@npm:^7946.0, @types/geojson@npm:^7946.0.14":
|
||||
"@types/geojson@npm:*, @types/geojson@npm:^7946.0, @types/geojson@npm:^7946.0.14, @types/geojson@npm:^7946.0.7":
|
||||
version: 7946.0.14
|
||||
resolution: "@types/geojson@npm:7946.0.14"
|
||||
checksum: ae511bee6488ae3bd5a3a3347aedb0371e997b14225b8983679284e22fa4ebd88627c6e3ff8b08bf4cc35068cb29310c89427311ffc9322c255615821a922e71
|
||||
|
@ -1359,6 +1359,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/proxy-addr@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "@types/proxy-addr@npm:2.0.3"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: eb5e917b216befc5ebadcdd613eac09ee4d6af46200a3d064396dc20c04e2ad63b71480dd8e363ed09686970754a1b4f3a947cecd2ca3475a799612a97543d0e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/qs@npm:*":
|
||||
version: 6.9.14
|
||||
resolution: "@types/qs@npm:6.9.14"
|
||||
|
@ -3894,6 +3903,37 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"facilmap-client-v3@patch:facilmap-client@npm%3A3#../.yarn/patches/facilmap-client-npm-3.4.0-9ca14d53cc.patch::locator=facilmap-integration-tests%40workspace%3Aintegration-tests":
|
||||
version: 3.4.0
|
||||
resolution: "facilmap-client-v3@patch:facilmap-client@npm%3A3.4.0#../.yarn/patches/facilmap-client-npm-3.4.0-9ca14d53cc.patch::version=3.4.0&hash=799670&locator=facilmap-integration-tests%40workspace%3Aintegration-tests"
|
||||
dependencies:
|
||||
facilmap-types: 3.4.0
|
||||
socket.io-client: ^4.1.2
|
||||
checksum: 8130988a9e6f258c76ea1ecf73f6c387709cf38949088e1555525d9efaf898170ae7a46b677215f1b9be7b4437b0810b248af7c56062e49c614e6a35a6664286
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"facilmap-client-v4@npm:facilmap-client@4":
|
||||
version: 4.1.0
|
||||
resolution: "facilmap-client@npm:4.1.0"
|
||||
dependencies:
|
||||
facilmap-types: ^4.1.0
|
||||
serialize-error: ^11.0.3
|
||||
socket.io-client: ^4.7.5
|
||||
checksum: dded594b13c9f4ada8619bb15182984b8a9bb1b08cdc2a29027a775a7835fdfdf33db50fe4465cc9ba8e00d86a09227b1508a270045871869556ffc20b8c6e41
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"facilmap-client@npm:3":
|
||||
version: 3.4.0
|
||||
resolution: "facilmap-client@npm:3.4.0"
|
||||
dependencies:
|
||||
facilmap-types: 3.4.0
|
||||
socket.io-client: ^4.1.2
|
||||
checksum: 7f037cb33b8d0a1b14d29edb40ab9f6ab542593ec8a1ccaddd83adc17ea3dec24a6334c7fffebd53b2f7e73d3abc89c1141d261ef8cecf99a8fb8242610c4f13
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"facilmap-client@workspace:^, facilmap-client@workspace:client":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "facilmap-client@workspace:client"
|
||||
|
@ -3980,7 +4020,11 @@ __metadata:
|
|||
dependencies:
|
||||
"@types/lodash-es": ^4.17.12
|
||||
facilmap-client: "workspace:^"
|
||||
facilmap-client-v3: "patch:facilmap-client@npm%3A3#../.yarn/patches/facilmap-client-npm-3.4.0-9ca14d53cc.patch"
|
||||
facilmap-client-v4: "npm:facilmap-client@4"
|
||||
facilmap-types: "workspace:^"
|
||||
facilmap-types-v3: "npm:facilmap-types@3"
|
||||
facilmap-types-v4: "npm:facilmap-types@4"
|
||||
facilmap-utils: "workspace:^"
|
||||
lodash-es: ^4.17.21
|
||||
socket.io-client: ^4.7.5
|
||||
|
@ -4061,6 +4105,7 @@ __metadata:
|
|||
"@types/geojson": ^7946.0.14
|
||||
"@types/lodash-es": ^4.17.12
|
||||
"@types/node": ^20.12.6
|
||||
"@types/proxy-addr": ^2.0.3
|
||||
"@types/string-similarity": ^4.0.2
|
||||
cheerio: ^1.0.0-rc.12
|
||||
compression: ^1.7.4
|
||||
|
@ -4087,6 +4132,7 @@ __metadata:
|
|||
mysql2: ^3.9.4
|
||||
p-throttle: ^6.1.0
|
||||
pg: ^8.11.5
|
||||
proxy-addr: ^2.0.7
|
||||
rimraf: ^5.0.5
|
||||
sequelize: ^6.37.2
|
||||
serialize-error: ^11.0.3
|
||||
|
@ -4106,6 +4152,25 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"facilmap-types-v3@npm:facilmap-types@3, facilmap-types@npm:3.4.0":
|
||||
version: 3.4.0
|
||||
resolution: "facilmap-types@npm:3.4.0"
|
||||
dependencies:
|
||||
"@types/geojson": ^7946.0.7
|
||||
checksum: 21b45f1e8e9b9c2e4d4e41b297412649f1fe8ca6e82ed6703f4e2cc3e42941fd37439c959177f60344039e7793ca39af4cfb072b50337cd058dc38812168150a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"facilmap-types-v4@npm:facilmap-types@4, facilmap-types@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "facilmap-types@npm:4.1.0"
|
||||
dependencies:
|
||||
"@types/geojson": ^7946.0.14
|
||||
zod: ^3.22.4
|
||||
checksum: 7d5afea1b80ba99994d9c648fe33c908bf816a9f96be9ae2d011d4691dd4842ecc1df30f6e6a74eb99d5836df7e49112189294ff7cc42d79ab90897ba35b683f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"facilmap-types@workspace:^, facilmap-types@workspace:types":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "facilmap-types@workspace:types"
|
||||
|
@ -6856,7 +6921,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"proxy-addr@npm:~2.0.7":
|
||||
"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7":
|
||||
version: 2.0.7
|
||||
resolution: "proxy-addr@npm:2.0.7"
|
||||
dependencies:
|
||||
|
@ -7553,7 +7618,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socket.io-client@npm:^4.7.5":
|
||||
"socket.io-client@npm:^4.1.2, socket.io-client@npm:^4.7.5":
|
||||
version: 4.7.5
|
||||
resolution: "socket.io-client@npm:4.7.5"
|
||||
dependencies:
|
||||
|
|
Ładowanie…
Reference in New Issue