Rename "symbol" to "marker", introduce v3 socket version

v5
Candid Dauth 2024-04-15 02:20:58 +02:00
rodzic e080834696
commit 3589ec3336
78 zmienionych plików z 1878 dodań i 1367 usunięć

Wyświetl plik

@ -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];

Wyświetl plik

@ -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",

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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": {

Wyświetl plik

@ -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.

Wyświetl plik

@ -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

Wyświetl plik

@ -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 dont 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 dont 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.

Wyświetl plik

@ -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);
```

Wyświetl plik

@ -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

Wyświetl plik

@ -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",

Wyświetl plik

@ -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."

Wyświetl plik

@ -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."

Wyświetl plik

@ -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"
},

Wyświetl plik

@ -166,9 +166,9 @@
</tr>
<tr>
<td><code>symbol</code></td>
<td>{{i18n.t("edit-filter-dialog.symbol-description")}}</td>
<td><code>symbol == &quot;accommodation_camping&quot;</code></td>
<td><code>icon</code></td>
<td>{{i18n.t("edit-filter-dialog.icon-description")}}</td>
<td><code>icon == &quot;accommodation_camping&quot;</code></td>
</tr>
<tr>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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();

Wyświetl plik

@ -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>

Wyświetl plik

@ -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) {

Wyświetl plik

@ -366,7 +366,7 @@
marker: {
colour: dragMarkerColour,
size: 35,
symbol: "",
icon: "",
shape: "drop"
}
})).addTo(mapContext.value.components.map));

Wyświetl plik

@ -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");
}
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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";

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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",

Wyświetl plik

@ -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);
});
});

Wyświetl plik

@ -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);
});
});

Wyświetl plik

@ -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");
});
});

Wyświetl plik

@ -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

Wyświetl plik

@ -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();

Wyświetl plik

@ -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);
}
});

Wyświetl plik

@ -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);
}
});

Wyświetl plik

@ -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

Wyświetl plik

@ -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",
});
});

Wyświetl plik

@ -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();

Wyświetl plik

@ -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();

Wyświetl plik

@ -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",
});
});

Wyświetl plik

@ -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,

Wyświetl plik

@ -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";

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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));

Wyświetl plik

@ -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",

Wyświetl plik

@ -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();

Wyświetl plik

@ -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,

Wyświetl plik

@ -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 ] })

Wyświetl plik

@ -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("_");

Wyświetl plik

@ -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",

Wyświetl plik

@ -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;

Wyświetl plik

@ -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,

Wyświetl plik

@ -17,6 +17,7 @@ export interface MetaProperties {
extraInfoNullMigrationCompleted: "1";
typesIdxMigrationCompleted: "1";
viewsIdxMigrationCompleted: "1";
fieldIconsMigrationCompleted: "1";
}
export default class DatabaseMeta {

Wyświetl plik

@ -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");
}
}

Wyświetl plik

@ -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) => ({

Wyświetl plik

@ -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 },

Wyświetl plik

@ -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

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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);
}
};
}
}

Wyświetl plik

@ -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);
});

Wyświetl plik

@ -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>;

Wyświetl plik

@ -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": {

Wyświetl plik

@ -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;

Wyświetl plik

@ -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]>;
};

Wyświetl plik

@ -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";

Wyświetl plik

@ -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

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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>];

Wyświetl plik

@ -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>;

Wyświetl plik

@ -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"

Wyświetl plik

@ -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;

Wyświetl plik

@ -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));

Wyświetl plik

@ -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: