pull/147/head
Candid Dauth 2021-03-04 16:45:34 +01:00
rodzic cc87787a37
commit 72edadfa84
52 zmienionych plików z 9876 dodań i 656 usunięć

Wyświetl plik

@ -1,32 +1,78 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript'
],
plugins: ['@typescript-eslint', 'import'],
extends: ['plugin:import/typescript'],
env: {
node: true
},
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/explicit-module-boundary-types": ["warn", { allowArgumentsExplicitlyTypedAsAny: true }],
"@typescript-eslint/triple-slash-reference": "off",
"no-cond-assign": "off",
"@typescript-eslint/no-empty-function": "off",
"import/no-extraneous-dependencies": "error",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/explicit-module-boundary-types": ["warn", { "allowArgumentsExplicitlyTypedAsAny": true }],
"import/no-unresolved": ["error", { "ignore": [ "geojson" ], "caseSensitive": true }],
"import/no-extraneous-dependencies": ["error"],
"@typescript-eslint/no-unused-vars": ["warn", { "args": "none" }],
"@typescript-eslint/no-inferrable-types": "off",
"import/no-unresolved": ["error", { "ignore": ["geojson" ] }],
"import/export": "off"
"import/no-named-as-default": ["warn"],
"import/no-named-as-default-member": ["warn"],
"import/no-duplicates": ["warn"],
"import/namespace": ["error"],
"import/default": ["error"],
"@typescript-eslint/no-extra-non-null-assertion": ["error"],
"@typescript-eslint/no-non-null-asserted-optional-chain": ["error"],
"@typescript-eslint/prefer-as-const": ["error"],
"no-restricted-globals": ["error", "$"],
"constructor-super": ["error"],
"for-direction": ["error"],
"getter-return": ["error"],
"no-async-promise-executor": ["error"],
"no-case-declarations": ["error"],
"no-class-assign": ["error"],
"no-compare-neg-zero": ["error"],
"no-const-assign": ["error"],
"no-constant-condition": ["error"],
"no-control-regex": ["error"],
"no-debugger": ["error"],
"no-delete-var": ["error"],
"no-dupe-args": ["error"],
"no-dupe-class-members": ["error"],
"no-dupe-else-if": ["error"],
"no-dupe-keys": ["error"],
"no-duplicate-case": ["error"],
"no-empty": ["error"],
"no-empty-character-class": ["error"],
"no-empty-pattern": ["error"],
"no-ex-assign": ["error"],
"no-extra-boolean-cast": ["error"],
"no-fallthrough": ["error"],
"no-func-assign": ["error"],
"no-global-assign": ["error"],
"no-import-assign": ["error"],
"no-inner-declarations": ["error"],
"no-invalid-regexp": ["error"],
"no-irregular-whitespace": ["error"],
"no-misleading-character-class": ["error"],
"no-mixed-spaces-and-tabs": ["error"],
"no-new-symbol": ["error"],
"no-obj-calls": ["error"],
"no-octal": ["error"],
"no-prototype-builtins": ["error"],
"no-redeclare": ["error"],
"no-regex-spaces": ["error"],
"no-self-assign": ["error"],
"no-setter-return": ["error"],
"no-shadow-restricted-names": ["error"],
"no-sparse-arrays": ["error"],
"no-this-before-super": ["error"],
"no-unexpected-multiline": ["error"],
"no-unreachable": ["error"],
"no-unsafe-finally": ["error"],
"no-unsafe-negation": ["error"],
"no-unused-labels": ["error"],
"no-useless-catch": ["error"],
"no-useless-escape": ["error"],
"no-with": ["error"],
"require-yield": ["error"],
"use-isnan": ["error"],
"valid-typeof": ["error"]
}
};

5
.gitignore vendored
Wyświetl plik

@ -1,6 +1,5 @@
*.iml
.idea
node_modules
bower_components
frontend/build
client/dist
*/dist
yarn-error.log

12
.htmlhintrc 100644
Wyświetl plik

@ -0,0 +1,12 @@
{
"tagname-lowercase": false,
"attr-lowercase": false,
"attr-value-double-quotes": true,
"doctype-first": false,
"tag-pair": true,
"spec-char-escape": true,
"id-unique": true,
"src-not-empty": true,
"attr-no-duplication": true,
"title-require": true
}

Wyświetl plik

@ -108,6 +108,11 @@ _Type:_ [`route`](#route-1)
If the opening the pad failed ([`setPadId(padId)`](#setpadidpadid) promise got rejected), the error message is stored
in this property.
### `loading`
A number that indicates how many requests are currently pending. You can use this to show a loading spinner or disable certain
UI elements while the value is greater than 0.
Events
------

Wyświetl plik

@ -21,13 +21,7 @@ Setting it up
Install facilmap-client as a dependency using npm or yarn:
```bash
npm install --save facilmap-client
```
or
```bash
yarn add facilmap-client
npm install -S facilmap-client
```
or load the client directly from facilmap.org (along with socket.io, which is needed by facilmap-client):
@ -42,7 +36,7 @@ The client class will be available as the global `FacilMap.Client` variable.
### Development
Make sure you have yarn installed. Run `npm install` to install the dependencies and `npm run build`
Make sure you have yarn installed. Run `yarn install` to install the dependencies and `yarn run build`
to create the bundle in `dist/client.js`.
@ -50,15 +44,46 @@ Setting up a connection
-----------------------
```js
let conn = new FacilMap.Client("https://facilmap.org/");
conn.setPadId("myMapId").then(() => {
console.log(conn.padData);
let client = new FacilMap.Client("https://facilmap.org/");
client.setPadId("myMapId").then((padData) => {
console.log(padData);
}).catch((err) => {
console.error(err.stack);
});
```
Using it
--------
A detailed description of all the methods and data types can be found in [API](./API.md).
Change detection
----------------
When the FacilMap server sends an event to the client that an object has been created, changed or deleted, the client emits the
event and also persists it in its properties. So you have two ways to access the map data: By listening to the map events and
persisting the data somewhere else, or by accessing the properties on the Client object.
If you are using a UI framework that relies on a change detection mechanism (such as Vue.js or Angular), you can override the methods
`_set` and `_delete`. facilmap-client consistently uses these to update any data on its properties.
In Vue.js, it could look like this:
```javascript
let client = new FacilMap.Client("https://facilmap.org/");
client._set = Vue.set;
client._delete = Vue.delete;
```
In Angular.js, it could look like this:
```javascript
let client = new FacilMap.Client("https://facilmap.org/");
client._set = (object, key, value) => { $rootScope.$apply(() => { object[key] = value; }); };
client._delete = (object, key) => { $rootScope.$apply(() => { delete object[key]; }); };
```
This way your UI framework will detect changes to any properties on the client, and you can reference values like `client.padData.name`,
`client.disconnected` and `client.loading` in your UI components.

Wyświetl plik

@ -1,13 +1,16 @@
import { Manager, Socket as SocketIO } from "socket.io-client";
import {
Bbox,
BboxWithZoom, EventHandler, EventName, FindOnMapQuery, FindQuery, HistoryEntry, ID, Line, LineCreate,
LineExportRequest, LineTemplateRequest, LineUpdate, MapEvents, Marker, MarkerCreate, MarkerUpdate, MultipleEvents, ObjectWithId,
PadData, PadDataCreate, PadDataUpdate, PadId, RequestData, RequestName, ResponseData, Route, RouteCreate, RouteExportRequest,
RouteInfo,
RouteRequest,
SearchResult,
TrackPoint, Type, TypeCreate, TypeUpdate, View, ViewCreate, ViewUpdate, Writable
} from "facilmap-types";
export interface SocketEvents extends MapEvents {
export interface ClientEvents extends MapEvents {
connect: [];
disconnect: [string];
connect_error: [Error];
@ -26,7 +29,7 @@ export interface SocketEvents extends MapEvents {
emit: { [eventName in RequestName]: [eventName, RequestData<eventName>] }[RequestName]
}
const MANAGER_EVENTS: Array<EventName<SocketEvents>> = ['error', 'reconnect', 'reconnect_attempt', 'reconnect_error', 'reconnect_failed'];
const MANAGER_EVENTS: Array<EventName<ClientEvents>> = ['error', 'reconnect', 'reconnect_attempt', 'reconnect_error', 'reconnect_failed'];
export interface TrackPoints {
[idx: number]: TrackPoint;
@ -41,7 +44,7 @@ export interface RouteWithTrackPoints extends Omit<Route, "trackPoints"> {
trackPoints: TrackPoints;
}
export default class Socket {
export default class Client {
disconnected: boolean = true;
server!: string;
padId: string | undefined = undefined;
@ -58,9 +61,10 @@ export default class Socket {
history: Record<ID, HistoryEntry> = { };
route: RouteWithTrackPoints | undefined = undefined;
serverError: Error | undefined = undefined;
loading: number = 0;
_listeners: {
[E in EventName<SocketEvents>]?: Array<EventHandler<SocketEvents, E>>
[E in EventName<ClientEvents>]?: Array<EventHandler<ClientEvents, E>>
} = { };
_listeningToHistory: boolean = false;
@ -68,17 +72,25 @@ export default class Socket {
this._init(server, padId);
}
_init(server: string, padId: string | undefined) {
_set<O, K extends keyof O>(object: O, key: K, value: O[K]): void {
object[key] = value;
}
_delete<O>(object: O, key: keyof O): void {
delete object[key];
}
_init(server: string, padId: string | undefined): void {
// Needs to be in a separate method so that we can merge this class with a scope object in the frontend.
this.server = server;
this.padId = padId;
this._set(this, 'server', server);
this._set(this, 'padId', padId);
const manager = new Manager(server, { forceNew: true });
this.socket = manager.socket("/");
this._set(this, 'socket', manager.socket("/"));
for(let i of Object.keys(this._handlers) as EventName<SocketEvents>[]) {
this.on(i, this._handlers[i] as EventHandler<SocketEvents, typeof i>);
for(const i of Object.keys(this._handlers) as EventName<ClientEvents>[]) {
this.on(i, this._handlers[i] as EventHandler<ClientEvents, typeof i>);
}
setTimeout(() => {
@ -89,29 +101,27 @@ export default class Socket {
});
}
on<E extends EventName<SocketEvents>>(eventName: E, fn: EventHandler<SocketEvents, E>) {
let listeners = this._listeners[eventName] as Array<EventHandler<SocketEvents, E>> | undefined;
if(!listeners) {
listeners = this._listeners[eventName] = [ ];
on<E extends EventName<ClientEvents>>(eventName: E, fn: EventHandler<ClientEvents, E>): void {
if(!this._listeners[eventName]) {
(MANAGER_EVENTS.includes(eventName) ? this.socket.io : this.socket)
.on(eventName, (...[data]: SocketEvents[E]) => { this._simulateEvent(eventName as any, data); });
.on(eventName, (...[data]: ClientEvents[E]) => { this._simulateEvent(eventName as any, data); });
}
listeners.push(fn);
this._set(this._listeners, eventName, [ ...(this._listeners[eventName] || [] as any), fn ]);
}
once<E extends EventName<SocketEvents>>(eventName: E, fn: EventHandler<SocketEvents, E>) {
let handler = ((data: any) => {
once<E extends EventName<ClientEvents>>(eventName: E, fn: EventHandler<ClientEvents, E>): void {
const handler = ((data: any) => {
this.removeListener(eventName, handler);
(fn as any)(data);
}) as EventHandler<SocketEvents, E>;
}) as EventHandler<ClientEvents, E>;
this.on(eventName, handler);
}
removeListener<E extends EventName<SocketEvents>>(eventName: E, fn: EventHandler<SocketEvents, E>) {
const listeners = this._listeners[eventName] as Array<EventHandler<SocketEvents, E>> | undefined;
removeListener<E extends EventName<ClientEvents>>(eventName: E, fn: EventHandler<ClientEvents, E>): void {
const listeners = this._listeners[eventName] as Array<EventHandler<ClientEvents, E>> | undefined;
if(listeners) {
this._listeners[eventName] = listeners.filter((listener) => (listener !== fn)) as any;
this._set(this._listeners, eventName, listeners.filter((listener) => (listener !== fn)) as any);
}
}
@ -133,52 +143,52 @@ export default class Socket {
}
_handlers: {
[E in EventName<SocketEvents>]?: EventHandler<SocketEvents, E>
[E in EventName<ClientEvents>]?: EventHandler<ClientEvents, E>
} = {
padData: (data) => {
this.padData = data;
this._set(this, 'padData', data);
if(data.writable != null) {
this.readonly = (data.writable == 0);
this.writable = data.writable;
this._set(this, 'readonly', data.writable == 0);
this._set(this, 'writable', data.writable);
}
let id = this.writable == 2 ? data.adminId : this.writable == 1 ? data.writeId : data.id;
const id = this.writable == 2 ? data.adminId : this.writable == 1 ? data.writeId : data.id;
if(id != null)
this.padId = id;
this._set(this, 'padId', id);
},
deletePad: () => {
this.readonly = true;
this.writable = 0;
this.deleted = true;
this._set(this, 'readonly', true);
this._set(this, 'writable', 0);
this._set(this, 'deleted', true);
},
marker: (data) => {
this.markers[data.id] = data;
this._set(this.markers, data.id, data);
},
deleteMarker: (data) => {
delete this.markers[data.id];
this._delete(this.markers, data.id);
},
line: (data) => {
this.lines[data.id] = {
this._set(this.lines, data.id, {
...data,
trackPoints: this.lines[data.id]?.trackPoints || { }
};
});
},
deleteLine: (data) => {
delete this.lines[data.id];
this._delete(this.lines, data.id);
},
linePoints: (data) => {
let line = this.lines[data.id];
const line = this.lines[data.id];
if(line == null)
return console.error("Received line points for non-existing line "+data.id+".");
line.trackPoints = this._mergeTrackPoints(data.reset ? {} : line.trackPoints, data.trackPoints);
this._set(line, 'trackPoints', this._mergeTrackPoints(data.reset ? {} : line.trackPoints, data.trackPoints));
},
routePoints: (data) => {
@ -187,42 +197,42 @@ export default class Socket {
return;
}
this.route.trackPoints = this._mergeTrackPoints(this.route.trackPoints, data);
this._set(this.route, 'trackPoints', this._mergeTrackPoints(this.route.trackPoints, data));
},
view: (data) => {
this.views[data.id] = data;
this._set(this.views, data.id, data);
},
deleteView: (data) => {
delete this.views[data.id];
this._delete(this.views, data.id);
if (this.padData) {
if(this.padData.defaultViewId == data.id)
this.padData.defaultViewId = null;
this._set(this.padData, 'defaultViewId', null);
}
},
type: (data) => {
this.types[data.id] = data;
this._set(this.types, data.id, data);
},
deleteType: (data) => {
delete this.types[data.id];
this._delete(this.types, data.id);
},
disconnect: () => {
this.disconnected = true;
this.markers = { };
this.lines = { };
this.views = { };
this.history = { };
this._set(this, 'disconnected', true);
this._set(this, 'markers', { });
this._set(this, 'lines', { });
this._set(this, 'views', { });
this._set(this, 'history', { });
},
connect: () => {
if(this.padId)
this._setPadId(this.padId);
else
this.disconnected = false; // Otherwise it gets set when padData arrives
this._set(this, 'disconnected', false); // Otherwise it gets set when padData arrives
if(this.bbox)
this.updateBbox(this.bbox);
@ -235,118 +245,126 @@ export default class Socket {
},
history: (data) => {
this.history[data.id] = data;
this._set(this.history, data.id, data);
// TODO: Limit to 50 entries
},
loadStart: () => {
this._set(this, 'loading', this.loading + 1);
},
loadEnd: () => {
this._set(this, 'loading', this.loading - 1);
}
};
setPadId(padId: PadId) {
setPadId(padId: PadId): Promise<void> {
if(this.padId != null)
throw new Error("Pad ID already set.");
return this._setPadId(padId);
}
updateBbox(bbox: BboxWithZoom) {
this.bbox = bbox;
updateBbox(bbox: BboxWithZoom): Promise<void> {
this._set(this, 'bbox', bbox);
return this._emit("updateBbox", bbox).then((obj) => {
this._receiveMultiple(obj);
});
}
createPad(data: PadDataCreate) {
createPad(data: PadDataCreate): Promise<void> {
return this._emit("createPad", data).then((obj) => {
this.readonly = false;
this.writable = 2;
this._set(this, 'readonly', false);
this._set(this, 'writable', 2);
this._receiveMultiple(obj);
});
}
editPad(data: PadDataUpdate) {
editPad(data: PadDataUpdate): Promise<PadData> {
return this._emit("editPad", data);
}
deletePad() {
deletePad(): Promise<void> {
return this._emit("deletePad");
}
listenToHistory() {
listenToHistory(): Promise<void> {
return this._emit("listenToHistory").then((obj) => {
this._listeningToHistory = true;
this._set(this, '_listeningToHistory', true);
this._receiveMultiple(obj);
});
}
stopListeningToHistory() {
this._listeningToHistory = false;
stopListeningToHistory(): Promise<void> {
this._set(this, '_listeningToHistory', false);
return this._emit("stopListeningToHistory");
}
revertHistoryEntry(data: ObjectWithId) {
revertHistoryEntry(data: ObjectWithId): Promise<void> {
return this._emit("revertHistoryEntry", data).then((obj) => {
this.history = { };
this._set(this, 'history', { });
this._receiveMultiple(obj);
});
}
async getMarker(data: ObjectWithId) {
let marker = await this._emit("getMarker", data);
this.markers[marker.id] = marker;
async getMarker(data: ObjectWithId): Promise<Marker> {
const marker = await this._emit("getMarker", data);
this._set(this.markers, marker.id, marker);
return marker;
}
addMarker(data: MarkerCreate) {
addMarker(data: MarkerCreate): Promise<Marker> {
return this._emit("addMarker", data);
}
editMarker(data: ObjectWithId & MarkerUpdate) {
editMarker(data: ObjectWithId & MarkerUpdate): Promise<Marker> {
return this._emit("editMarker", data);
}
deleteMarker(data: ObjectWithId) {
deleteMarker(data: ObjectWithId): Promise<Marker> {
return this._emit("deleteMarker", data);
}
getLineTemplate(data: LineTemplateRequest) {
getLineTemplate(data: LineTemplateRequest): Promise<Line> {
return this._emit("getLineTemplate", data);
}
addLine(data: LineCreate) {
addLine(data: LineCreate): Promise<Line> {
return this._emit("addLine", data);
}
editLine(data: ObjectWithId & LineUpdate) {
editLine(data: ObjectWithId & LineUpdate): Promise<Line> {
return this._emit("editLine", data);
}
deleteLine(data: ObjectWithId) {
deleteLine(data: ObjectWithId): Promise<Line> {
return this._emit("deleteLine", data);
}
exportLine(data: LineExportRequest) {
exportLine(data: LineExportRequest): Promise<string> {
return this._emit("exportLine", data);
}
find(data: FindQuery) {
find(data: FindQuery): Promise<string | SearchResult[]> {
return this._emit("find", data);
}
findOnMap(data: FindOnMapQuery) {
findOnMap(data: FindOnMapQuery): Promise<ResponseData<'findOnMap'>> {
return this._emit("findOnMap", data);
}
getRoute(data: RouteRequest) {
getRoute(data: RouteRequest): Promise<RouteInfo> {
return this._emit("getRoute", data);
}
setRoute(data: RouteCreate) {
setRoute(data: RouteCreate): Promise<RouteWithTrackPoints | undefined> {
return this._emit("setRoute", data).then((route) => {
if(route) { // If unset, a newer submitted route has returned in the meantime
this.route = {
this._set(this, 'route', {
...route,
trackPoints: this._mergeTrackPoints({}, route.trackPoints)
};
});
this._simulateEvent("route", this.route);
}
@ -355,18 +373,18 @@ export default class Socket {
});
}
clearRoute() {
this.route = undefined;
clearRoute(): Promise<void> {
this._set(this, 'route', undefined);
this._simulateEvent("route", undefined);
return this._emit("clearRoute");
}
lineToRoute(data: ObjectWithId) {
lineToRoute(data: ObjectWithId): Promise<RouteWithTrackPoints | undefined> {
return this._emit("lineToRoute", data).then((route) => {
this.route = {
this._set(this, 'route', {
...route,
trackPoints: this._mergeTrackPoints({}, route.trackPoints)
};
});
this._simulateEvent("route", this.route);
@ -374,80 +392,80 @@ export default class Socket {
});
}
exportRoute(data: RouteExportRequest) {
exportRoute(data: RouteExportRequest): Promise<string> {
return this._emit("exportRoute", data);
}
addType(data: TypeCreate) {
addType(data: TypeCreate): Promise<Type> {
return this._emit("addType", data);
}
editType(data: ObjectWithId & TypeUpdate) {
editType(data: ObjectWithId & TypeUpdate): Promise<Type> {
return this._emit("editType", data);
}
deleteType(data: ObjectWithId) {
deleteType(data: ObjectWithId): Promise<Type> {
return this._emit("deleteType", data);
}
addView(data: ViewCreate) {
addView(data: ViewCreate): Promise<View> {
return this._emit("addView", data);
}
editView(data: ObjectWithId & ViewUpdate) {
editView(data: ObjectWithId & ViewUpdate): Promise<View> {
return this._emit("editView", data);
}
deleteView(data: ObjectWithId) {
deleteView(data: ObjectWithId): Promise<View> {
return this._emit("deleteView", data);
}
geoip() {
geoip(): Promise<Bbox | null> {
return this._emit("geoip");
}
disconnect() {
disconnect(): void {
this.socket.offAny();
this.socket.disconnect();
}
_setPadId(padId: string) {
this.padId = padId;
_setPadId(padId: string): Promise<void> {
this._set(this, 'padId', padId);
return this._emit("setPadId", padId).then((obj) => {
this.disconnected = false;
this._set(this, 'disconnected', false);
this._receiveMultiple(obj);
}).catch((err) => {
this.serverError = err;
this._set(this, 'serverError', err);
throw err;
});
}
_receiveMultiple(obj?: MultipleEvents<SocketEvents>) {
_receiveMultiple(obj?: MultipleEvents<ClientEvents>): void {
if (obj) {
for(const i of Object.keys(obj) as EventName<SocketEvents>[])
(obj[i] as Array<SocketEvents[typeof i][0]>).forEach((it) => { this._simulateEvent(i, it as any); });
for(const i of Object.keys(obj) as EventName<ClientEvents>[])
(obj[i] as Array<ClientEvents[typeof i][0]>).forEach((it) => { this._simulateEvent(i, it as any); });
}
}
_simulateEvent<E extends EventName<SocketEvents>>(eventName: E, ...data: SocketEvents[E]) {
const listeners = this._listeners[eventName] as Array<EventHandler<SocketEvents, E>> | undefined;
_simulateEvent<E extends EventName<ClientEvents>>(eventName: E, ...data: ClientEvents[E]): void {
const listeners = this._listeners[eventName] as Array<EventHandler<ClientEvents, E>> | undefined;
if(listeners) {
listeners.forEach(function(listener: EventHandler<SocketEvents, E>) {
listeners.forEach(function(listener: EventHandler<ClientEvents, E>) {
listener(...data);
});
}
}
_mergeTrackPoints(existingTrackPoints: Record<number, TrackPoint> | null, newTrackPoints: TrackPoint[]) {
let ret = { ...(existingTrackPoints || { }) } as TrackPoints;
_mergeTrackPoints(existingTrackPoints: Record<number, TrackPoint> | null, newTrackPoints: TrackPoint[]): TrackPoints {
const ret = { ...(existingTrackPoints || { }) } as TrackPoints;
for(let i=0; i<newTrackPoints.length; i++) {
ret[newTrackPoints[i].idx] = newTrackPoints[i];
}
ret.length = 0;
for(let i in ret) {
for(const i in ret) {
if(i != "length")
ret.length = Math.max(ret.length, parseInt(i) + 1);
}

Wyświetl plik

@ -3,7 +3,7 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl
module.exports = (env, argv) => {
const isDev = argv.mode == "development";
const base = {
return {
entry: `${__dirname}/src/client.ts`,
output: {
filename: "client.js",

Wyświetl plik

@ -1 +0,0 @@
**/*.js

Wyświetl plik

@ -1,23 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
env: {
node: true
},
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/explicit-module-boundary-types": ["warn", { allowArgumentsExplicitlyTypedAsAny: true }],
"@typescript-eslint/triple-slash-reference": "off",
"no-cond-assign": "off",
"@typescript-eslint/no-empty-function": "off"
}
};

Wyświetl plik

@ -10,7 +10,3 @@
width: 100%;
height: 100%;
}
.leaflet-control-locate.leaflet-control-locate a {
font-size: inherit;
}

Wyświetl plik

@ -1,115 +0,0 @@
<div class="modal-header">
<button ng-if="!noCancel" type="button" class="close" ng-click="$dismiss()"><span aria-hidden="true">&times;</span></button>
<h3 class="modal-title">{{create ? 'Start collaborative map' : 'Map settings'}}</h3>
</div>
<div class="modal-body">
<form class="form-horizontal" ng-submit="!writeError && !readError && save()">
<div uib-alert class="alert-danger" ng-show="error">{{error.message || error}}</div>
<div class="form-group" ng-class="{'has-error': !!adminError}">
<label for="pad-link-input" class="col-sm-3 control-label">Admin Link</label>
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">{{urlPrefix}}</span>
<input id="admin-link-input" ng-model="padData.adminId" class="form-control" />
<span class="input-group-btn" ng-if="!create">
<button type="button" class="btn btn-default" ng-click="copy(urlPrefix + padData.adminId)">Copy</button>
</span>
</div>
<span class="help-block" ng-if="adminError">{{adminError}}</span>
<p class="help-block">When opening the map through this link, all parts of the map can be edited, including the map settings, object types and views.</p>
</div>
</div>
<div class="form-group" ng-class="{'has-error': !!writeError}">
<label for="pad-link-input" class="col-sm-3 control-label">Editable Link</label>
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">{{urlPrefix}}</span>
<input id="pad-link-input" ng-model="padData.writeId" class="form-control" />
<span class="input-group-btn" ng-if="!create">
<button type="button" class="btn btn-default" ng-click="copy(urlPrefix + padData.writeId)">Copy</button>
</span>
</div>
<span class="help-block" ng-if="writeError">{{writeError}}</span>
<p class="help-block">When opening the map through this link, markers and lines can be added, changed and deleted, but the map settings, object types and views cannot be modified.</p>
</div>
</div>
<div class="form-group" ng-class="{'has-error': !!readError}">
<label for="pad-rolink-input" class="col-sm-3 control-label">Read-only link</label>
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">{{urlPrefix}}</span>
<input id="pad-rolink-input" ng-model="padData.id" class="form-control" />
<span class="input-group-btn" ng-if="!create">
<button type="button" class="btn btn-default" ng-click="copy(urlPrefix + padData.id)">Copy</button>
</span>
</div>
<span class="help-block" ng-if="readError">{{readError}}</span>
<p class="help-block">When opening the map through this link, markers, lines and views can be seen, but nothing can be changed.</p>
</div>
</div>
<div class="form-group">
<label for="pad-name-input" class="col-sm-3 control-label">Map name</label>
<div class="col-sm-9"><input id="pad-name-input" ng-model="padData.name" class="form-control" /></div>
</div>
<div class="form-group">
<label for="search-engines-input" class="col-sm-3 control-label">Accessible for search engines</label>
<div class="col-sm-9">
<input type="checkbox" id="search-engines-input" ng-model="padData.searchEngines" />
<p class="help-block">If this is enabled, search engines like Google will be allowed to add the read-only version of this map.</p>
</div>
</div>
<div class="form-group" ng-show="padData.searchEngines">
<label for="description-input" class="col-sm-3 control-label">Short description</label>
<div class="col-sm-9">
<input id="description-input" ng-model="padData.description" class="form-control" />
<p class="help-block">This description will be shown under the result in search engines.</p>
</div>
</div>
<div class="form-group">
<label for="cluster-markers-input" class="col-sm-3 control-label">Cluster markers</label>
<div class="col-sm-9">
<input type="checkbox" id="cluster-markers-input" ng-model="padData.clusterMarkers" />
<p class="help-block">If enabled, when there are many markers in one area, they will be replaced by a placeholder at low zoom levels. This improves performance on maps with many markers.</p>
</div>
</div>
<div class="form-group">
<label for="legend1-input" class="col-sm-3 control-label">Legend text</label>
<div class="col-sm-9">
<textarea id="legend1-input" ng-model="padData.legend1" class="form-control"></textarea>
<textarea id="legend2-input" ng-model="padData.legend2" class="form-control"></textarea>
<p class="help-block">Text that will be shown above and below the legend. Can be formatted with <a href="http://commonmark.org/help/" target="_blank">Markdown</a>.</p>
</div>
</div>
<button type="submit" class="hidden"></button>
</form>
<hr />
<form class="form-horizontal" ng-submit="state.deleteConfirmation == 'DELETE' && confirm('Are you sure you want to delete the map “' + padData.name +'”? Deleted maps cannot be restored!') && deletePad()">
<div class="form-group" ng-if="!create">
<label for="delete-input" class="col-sm-3 control-label">Delete map</label>
<div class="col-sm-9">
<div class="input-group">
<input id="delete-input" ng-model="state.deleteConfirmation" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-danger" ng-disabled="state.deleteConfirmation != 'DELETE'">Delete map</button>
</span>
</div>
<p class="help-block">To delete this map, type <code>DELETE</code> into the field.</p>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button ng-if="!noCancel" type="button" class="btn btn-default" ng-click="$dismiss()" ng-disabled="saving">{{create || isModified ? 'Cancel' : 'Close'}}</button>
<button ng-show="create || isModified" type="submit" class="btn btn-primary" ng-click="save()" ng-disabled="writeError || readError || saving">{{create ? 'Create' : 'Save'}}</button>
</div>

Wyświetl plik

@ -1,138 +0,0 @@
import fm from '../../app';
import $ from 'jquery';
import ng from 'angular';
fm.app.factory("fmMapPad", function($uibModal, fmUtils, $rootScope) {
return function(map) {
var ret = {
createPad : function(proposedAdminId, noCancel) {
ret.editPadSettings(true, proposedAdminId, noCancel);
},
editPadSettings : function(create, proposedAdminId, noCancel) {
$uibModal.open({
template: require("./pad-settings.html"),
controller: "fmMapPadSettingsCtrl",
size: "lg",
resolve: {
map: function() { return map; },
create: function() { return create; },
proposedAdminId: function() { return proposedAdminId; },
noCancel: function() { return noCancel; }
},
keyboard: !noCancel,
backdrop: noCancel ? "static" : true
});
}
};
/*
$scope.copyPadId = fmUtils.generateRandomPadId();
$scope.copyPad = function() {
socket.copyPad({ toId: $scope.copyPadId }, function(err) {
if(err) {
$scope.dialogError = err;
return;
}
$scope.closeDialog();
var url = $scope.urlPrefix + $scope.copyPadId;
$scope.showMessage("success", "The pad has been copied to", [ { label: url, url: url } ]);
$scope.copyPadId = fmUtils.generateRandomPadId();
});
};
*/
return ret;
};
});
fm.app.controller("fmMapPadSettingsCtrl", function($scope, map, create, proposedAdminId, noCancel, fmUtils) {
$scope.urlPrefix = fm.URL_PREFIX;
$scope.create = create;
$scope.noCancel = noCancel;
$scope.state = {
deleteConfirmation: ""
};
if(create) {
$scope.padData = {
padName: "New FacilMap",
searchEngines: false,
description: "",
clusterMarkers: false,
adminId: (proposedAdminId || fmUtils.generateRandomPadId(16)),
writeId: fmUtils.generateRandomPadId(14),
id: fmUtils.generateRandomPadId(12)
};
} else {
$scope.padData = ng.copy(map.client.padData);
$scope.$watch(() => (map.client.padData), (newPadData, oldPadData) => {
fmUtils.mergeObject(oldPadData, newPadData, $scope.padData);
updateModified();
}, true);
$scope.$watch("padData", updateModified, true);
function updateModified() {
$scope.isModified = !ng.equals($scope.padData, map.client.padData);
}
}
function validateId(id) {
if(!id || id.length == "")
return "Cannot be empty.";
if(id.indexOf("/") != -1)
return "May not contain a slash.";
}
$scope.$watch("padData.adminId", function(adminId) {
$scope.adminError = validateId(adminId);
});
$scope.$watch("padData.writeId", function(writeId) {
$scope.writeError = validateId(writeId);
});
$scope.$watch("padData.id", function(readId) {
$scope.readError = validateId(readId);
});
$scope.save = function() {
$scope.saving = true;
if(create) {
map.client.createPad($scope.padData).then(function() {
map.client.updateBbox(fmUtils.leafletToFmBbox(map.map.getBounds(), map.map.getZoom()));
$scope.$close();
}).catch(function(err) {
$scope.error = err;
$scope.saving = false;
});
} else {
map.client.editPad($scope.padData).then(function() {
$scope.$close();
}).catch(function(err) {
$scope.error = err;
$scope.saving = false;
});
}
};
$scope.copy = function(text) {
fmUtils.copyToClipboard(text);
};
$scope.deletePad = function() {
$scope.saving = true;
$scope.error = null;
map.client.deletePad().then(() => {
$scope.$close();
}).catch((err) => {
$scope.error = err;
$scope.saving = false;
});
};
});

Wyświetl plik

@ -137,20 +137,6 @@ fm.app.directive("fmShapePicker", function(fmIcons, fmUtils, $compile, $rootScop
return iconShapePicker(true, ...arguments);
});
fm.app.directive("fmIcon", function(fmUtils) {
return {
restrict: 'E',
scope: {
fmIcon: "@"
},
link: function(scope, element, attrs) {
scope.$watch("fmIcon", (icon) => {
element.html(fmUtils.createSymbolHtml("currentColor", 25, scope.fmIcon));
});
}
};
});
fm.app.directive("fmShape", function(fmUtils) {
return {
restrict: 'A',
@ -165,22 +151,6 @@ fm.app.directive("fmShape", function(fmUtils) {
};
});
fm.app.directive("fmTitle", function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(!$(element).is("title"))
return;
scope.$watch(attrs.fmTitle, function(v) {
// We have to call history.replaceState() in order for the new title to end up in the browser history
window.history && history.replaceState({ }, v);
document.title = v;
});
}
};
});
fm.app.directive("fmScrollToView", (fmUtils) => {
return {
restrict: 'A',

Wyświetl plik

@ -0,0 +1,3 @@
module.exports = {
preset: 'ts-jest',
};

Wyświetl plik

@ -30,6 +30,7 @@
"bootstrap-touchspin": "^4.3.0",
"bootstrap-vue": "^2.21.1",
"clipboard": "^2.0.6",
"copy-to-clipboard": "^3.3.1",
"domutils": "^2.4.4",
"facilmap-client": "^2.7.0",
"facilmap-leaflet": "^2.7.0",
@ -44,15 +45,20 @@
"leaflet.heightgraph": "^1.4.0",
"leaflet.locatecontrol": "^0.72.0",
"linkifyjs": "^2.1.9",
"lodash": "^4.17.21",
"markdown": "^0.5.0",
"osmtogeojson": "^3.0.0-beta.4",
"portal-vue": "^2.1.7",
"tablesorter": "^2.31.3",
"vee-validate": "^3.4.5",
"vue": "^2.6.12",
"vue-class-component": "^7.2.6",
"vue-property-decorator": "^9.1.2"
"vue-property-decorator": "^9.1.2",
"vue2-touch-events": "^3.2.0"
},
"devDependencies": {
"@types/copy-webpack-plugin": "^6.4.0",
"@types/jest": "^26.0.20",
"@types/jquery": "^3.5.5",
"@types/leaflet": "^1.5.19",
"@types/leaflet-mouse-position": "^1.2.0",
@ -65,10 +71,12 @@
"facilmap-types": "^2.7.0",
"html-loader": "x",
"html-webpack-plugin": "x",
"jest": "^26.6.3",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
"svgo": "^1.1.1",
"ts-jest": "^26.5.3",
"ts-loader": "^8.0.17",
"ts-node": "^9.1.1",
"typescript": "^4.1.3",

Wyświetl plik

@ -12,7 +12,7 @@ import "./about.scss";
})
export default class About extends Vue {
@Prop({ type: String }) id!: string;
@Prop({ type: String, required: true }) id!: string;
layers = [...Object.values(baseLayers), ...Object.values(overlays)];
fmVersion = packageJson.version;

Wyświetl plik

@ -1,12 +1,14 @@
import { Component, InjectReactive, Prop, ProvideReactive } from "vue-property-decorator";
import { Component, InjectReactive, Prop, ProvideReactive, Watch } from "vue-property-decorator";
import Vue from "vue";
import Client from "facilmap-client";
import "./client.scss";
import WithRender from "./client.vue";
import { PadId } from "facilmap-types";
import { VueDecorator } from "vue-class-component";
const CLIENT_KEY = "fm-client";
export function InjectClient() {
export function InjectClient(): VueDecorator {
return InjectReactive(CLIENT_KEY);
}
@ -27,4 +29,18 @@ export class ClientProvider extends Vue {
this.client = client;
}
beforeDestroy(): void {
this.client.disconnect();
}
@Watch("client.padId")
handlePadIdChange(padId: PadId): void {
this.$emit("padId", padId);
}
@Watch("client.padData.name")
handlePadNameChange(padName: string): void {
this.$emit("padName", padName);
}
}

Wyświetl plik

@ -39,15 +39,17 @@ if(!location.hash || location.hash == "#") {
}
}
/* setTimeout(function() {
var map = angular.element($("facilmap", $element)).controller("facilmap");
export function updatePadId(padId: string): void {
context.activePadId = padId;
if (padId)
history.replaceState(null, "", context.urlPrefix + padId + location.search + location.hash);
}
$scope.$watch(() => (map.client.padData && map.client.padData.name), function(newVal) {
$scope.padName = newVal;
});
export function updatePadName(padName: string): void {
const title = padName ? padName + ' – FacilMap' : 'FacilMap';
$scope.$watch(() => (map.client.padId), function(padId) {
if(padId)
history.replaceState(null, "", fm.URL_PREFIX + padId + location.search + location.hash);
});
}, 0); */
// We have to call history.replaceState() in order for the new title to end up in the browser history
window.history && history.replaceState({ }, title);
document.title = title;
}

Wyświetl plik

@ -2,10 +2,10 @@
<html>
<head>
<meta charset="utf-8">
<title fm-title="padName ? padName + ' – FacilMap' : 'FacilMap'">FacilMap</title>
<title>FacilMap</title>
<% if(!padData || padData.searchEngines) { %>
<meta name="robots" content="index,nofollow" />
<meta name="description" ng-non-bindable content="<%= padData && padData.description || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration." %>" />
<meta name="description" content="<%= padData && padData.description || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration." %>" />
<% } else { %>
<meta name="robots" content="noindex,nofollow" />
<% } %>

Wyświetl plik

@ -1,15 +1,17 @@
import $ from 'jquery';
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { BootstrapVue } from "bootstrap-vue";
import { registerDeobfuscationHandlers } from "../utils/ui";
import Main from './main/main';
import { ClientProvider } from './client/client';
import context from './context';
import context, { updatePadId, updatePadName } from './context';
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import withRender from "./map.vue";
import Vue2TouchEvents from 'vue2-touch-events'
import PortalVue from 'portal-vue'
import Vue2TouchEvents from "vue2-touch-events";
import PortalVue from "portal-vue";
import "../utils/validation";
import { PadId } from 'facilmap-types';
Vue.use(BootstrapVue);
Vue.use(Vue2TouchEvents);
@ -68,5 +70,14 @@ new Vue(withRender({
serverUrl: "/",
padId: context.activePadId
},
methods: {
handlePadIdChange(padId: PadId) {
updatePadId(padId);
},
handlePadNameChange(padName: string) {
updatePadName(padName);
}
},
components: { ClientProvider, Main }
}));

Wyświetl plik

@ -1,3 +1,3 @@
<ClientProvider :serverUrl='serverUrl' :padId='padId'>
<ClientProvider :serverUrl="serverUrl" :padId="padId" @padId="handlePadIdChange" @padName="handlePadNameChange">
<Main/>
</ClientProvider>

Wyświetl plik

@ -0,0 +1,133 @@
import { Component, Prop, Watch } from "vue-property-decorator";
import WithRender from "./pad-settings.vue";
import Vue from "vue";
import { ValidationObserver, ValidationProvider } from "vee-validate";
import context from "../context";
import { PadData, PadDataCreate, PadDataUpdate } from "facilmap-types";
import { clone, generateRandomPadId } from "facilmap-utils";
import Client from "facilmap-client";
import { InjectClient } from "../client/client";
import { mergeObject } from "../../utils/utils";
import { isEqual } from "lodash";
import { getValidationState } from "../../utils/validation";
import copyToClipboard from "copy-to-clipboard";
import FormModal from "../ui/form-modal/form-modal";
@WithRender
@Component({
components: { FormModal, ValidationObserver, ValidationProvider }
})
export default class PadSettings extends Vue {
@InjectClient() client!: Client;
@Prop({ type: String, required: true }) readonly id!: string;
@Prop({ type: String }) readonly proposedAdminId?: string;
@Prop({ type: Boolean }) readonly noCancel?: boolean;
@Prop({ type: Boolean }) readonly isCreate?: boolean;
isSaving = false;
deleteConfirmation = "";
padData: PadDataCreate | PadDataUpdate = null as any;
handleShow(): void {
if(this.isCreate) {
this.padData = {
name: "New FacilMap",
searchEngines: false,
description: "",
clusterMarkers: false,
adminId: (this.proposedAdminId || generateRandomPadId(16)),
writeId: generateRandomPadId(14),
id: generateRandomPadId(12),
legend1: "",
legend2: "",
defaultViewId: null
};
} else {
this.padData = clone(this.client.padData as PadDataUpdate);
}
}
get isModified(): boolean {
return !isEqual(this.padData, this.client.padData);
}
get urlPrefix(): string {
return context.urlPrefix;
}
@Watch("client.padData", { deep: true })
handlePadDataChange(newPadData: PadData, oldPadData: PadData): void {
if (!this.isCreate)
mergeObject(oldPadData, newPadData, this.padData);
}
getValidationState = getValidationState;
/*
$scope.copyPadId = fmUtils.generateRandomPadId();
$scope.copyPad = function() {
socket.copyPad({ toId: $scope.copyPadId }, function(err) {
if(err) {
$scope.dialogError = err;
return;
}
$scope.closeDialog();
var url = $scope.urlPrefix + $scope.copyPadId;
$scope.showMessage("success", "The pad has been copied to", [ { label: url, url: url } ]);
$scope.copyPadId = fmUtils.generateRandomPadId();
});
};
*/
async save(): Promise<void> {
this.isSaving = true;
this.$bvToast.hide("fm-pad-settings-error");
try {
if(this.isCreate)
await this.client.createPad(this.padData as PadDataCreate);
// this.client.updateBbox(leafletToFmBbox(map.map.getBounds(), map.map.getZoom()));
else
await this.client.editPad(this.padData);
this.$bvModal.hide(this.id);
} catch (err) {
console.error(err.stack || err);
this.$bvToast.toast(err.message || err, {
id: "fm-pad-settings-error",
title: this.isCreate ? "Error creating map" : "Error saving map settings",
variant: "danger",
noAutoHide: true
});
} finally {
this.isSaving = false;
}
};
copy(text: string): void {
copyToClipboard(text);
}
async deletePad(): Promise<void> {
this.isSaving = true;
this.$bvToast.hide("fm-pad-settings-error");
try {
await this.client.deletePad();
this.$bvModal.hide(this.id);
} catch (err) {
console.error(err.stack || err);
this.$bvToast.toast(err.message || err, {
id: "fm-pad-settings-error",
title: "Error deleting map",
variant: "danger",
noAutoHide: true
});
} finally {
this.isSaving = false;
}
};
}

Wyświetl plik

@ -0,0 +1,109 @@
<FormModal
:id="id"
:title="isCreate ? 'Start collaborative map' : 'Map settings'"
dialog-class="fm-pad-settings"
:no-cancel="noCancel"
:is-saving="isSaving"
:is-create="isCreate"
:is-modified="isModified"
@submit="save"
@show="handleShow"
>
<template v-if="padData">
<ValidationProvider name="Admin link" v-slot="v" rules="required|padId">
<b-form-group label="Admin link" label-for="admin-link-input" label-cols-sm="3" content-cols-sm="9" :invalid-feedback="v.errors[0]" :state="getValidationState(v)">
<b-input-group :prepend="urlPrefix">
<b-form-input id="admin-link-input" v-model="padData.adminId" :state="getValidationState(v)"></b-form-input>
<b-input-group-append>
<b-button @click="copy(urlPrefix + padData.adminId)">Copy</b-button>
</b-input-group-append>
</b-input-group>
<template #description>
When opening the map through this link, all parts of the map can be edited, including the map settings, object types and views.
</template>
</b-form-group>
</ValidationProvider>
<ValidationProvider name="Editable link" v-slot="v" rules="required|padId">
<b-form-group label="Editable link" label-for="write-link-input" label-cols-sm="3" content-cols-sm="9" :invalid-feedback="v.errors[0]" :state="getValidationState(v)">
<b-input-group :prepend="urlPrefix">
<b-form-input id="write-link-input" v-model="padData.writeId" :state="getValidationState(v)"></b-form-input>
<b-input-group-append>
<b-button @click="copy(urlPrefix + padData.writeId)">Copy</b-button>
</b-input-group-append>
</b-input-group>
<template #description>
When opening the map through this link, markers and lines can be added, changed and deleted, but the map settings, object types and views cannot be modified.
</template>
</b-form-group>
</ValidationProvider>
<ValidationProvider name="Read-only link" v-slot="v" rules="required|padId">
<b-form-group label="Read-only link link" label-for="read-link-input" label-cols-sm="3" content-cols-sm="9" :invalid-feedback="v.errors[0]" :state="getValidationState(v)">
<b-input-group :prepend="urlPrefix">
<b-form-input id="read-link-input" v-model="padData.id" :state="getValidationState(v)"></b-form-input>
<b-input-group-append>
<b-button @click="copy(urlPrefix + padData.id)">Copy</b-button>
</b-input-group-append>
</b-input-group>
<b-form-invalid-feedback>{{v.errors[0]}}</b-form-invalid-feedback>
<template #description>
When opening the map through this link, markers, lines and views can be seen, but nothing can be changed.
</template>
</b-form-group>
</ValidationProvider>
<b-form-group label-for="pad-name-input" label="Map name" label-cols-sm="3" content-cols-sm="9">
<b-form-input id="pad-name-input" v-model="padData.name"></b-form-input>
</b-form-group>
<b-form-group label="Search engines" label-for="search-engines-input" label-cols-sm="3" content-cols-sm="9">
<b-form-checkbox id="search-engines-input" v-model="padData.searchEngines">Accessible for search engines</b-form-checkbox>
<template #description>
If this is enabled, search engines like Google will be allowed to add the read-only version of this map.
</template>
</b-form-group>
<b-form-group v-show="padData.searchEngines" label="Short description" label-for="description-input" label-cols-sm="3" content-cols-sm="9">
<b-form-input id="description-input" v-model="padData.description"></b-form-input>
<template #description>
This description will be shown under the result in search engines.
</template>
</b-form-group>
<b-form-group label="Cluster markers" label-for="cluster-markers-input" label-cols-sm="3" content-cols-sm="9">
<b-form-checkbox id="cluster-markers-input" v-model="padData.clusterMarkers">Cluster markers</b-form-checkbox>
<template #description>
If enabled, when there are many markers in one area, they will be replaced by a placeholder at low zoom levels. This improves performance on maps with many markers.
</template>
</b-form-group>
<b-form-group label="Legend text" label-for="legend1-input" label-cols-sm="3" content-cols-sm="9">
<b-form-textarea id="legend1-input" v-model="padData.legend1"></b-form-textarea>
<b-form-textarea id="legend2-input" v-model="padData.legend2"></b-form-textarea>
<template #description>
Text that will be shown above and below the legend. Can be formatted with <a href="http://commonmark.org/help/" target="_blank">Markdown</a>.
</template>
</b-form-group>
<button type="submit" class="d-none"></button>
</template>
<template #after-form v-if="padData && !isCreate">
<hr/>
<b-form @submit.prevent="deleteConfirmation == 'DELETE' && confirm('Are you sure you want to delete the map ' + padData.name +'? Deleted maps cannot be restored!') && deletePad()">
<b-form-group label="Delete map" label-for="delete-input" label-cols-sm="3" content-cols-sm="9">
<b-input-group>
<b-form-input id="delete-input" v-model="deleteConfirmation" autocomplete="off"></b-form-input>
<b-input-group-append>
<b-button type="submit" variant="danger" :disabled="deleteConfirmation != 'DELETE'">Delete map</b-button>
</b-input-group-append>
</b-input-group>
<template #description>
To delete this map, type <code>DELETE</code> into the field.
</template>
</b-form-group>
</b-form>
</template>
</FormModal>

Wyświetl plik

@ -11,14 +11,14 @@ import $ from "jquery";
})
export default class SearchBox extends Vue {
tab: number = 0;
tab = 0;
touchStartY: number | null = null;
get isNarrow() {
get isNarrow(): boolean {
return context.isNarrow;
}
handleTouchStart(event: TouchEvent) {
handleTouchStart(event: TouchEvent): void {
if(context.isNarrow && event.touches && event.touches[0] && $(event.target as EventTarget).closest("[draggable=true]").length == 0) {
const top = (this.$el as HTMLElement).offsetTop;
this.touchStartY = event.touches[0].clientY - top;
@ -26,7 +26,7 @@ export default class SearchBox extends Vue {
}
}
handleTouchMove(event: TouchEvent) {
handleTouchMove(event: TouchEvent): void {
if(this.touchStartY != null && event.touches[0]) {
const minTop = Math.max(0, ((this.$el as HTMLElement).offsetParent as HTMLElement).offsetHeight - (this.$el as HTMLElement).scrollHeight);
const maxTop = ((this.$el as HTMLElement).offsetParent as HTMLElement).offsetHeight - 70;
@ -35,7 +35,7 @@ export default class SearchBox extends Vue {
}
}
handleTouchEnd(event: TouchEvent) {
handleTouchEnd(event: TouchEvent): void {
if(this.touchStartY != null && event.changedTouches[0]) {
this.touchStartY = null;
}

Wyświetl plik

@ -1,7 +1,7 @@
<div class="fm-search-box" :class="{ isNarrow }" v-model="tab" v-touch:start="handleTouchStart" v-touch:moving="handleTouchMove" v-touch:end="handleTouchEnd">
<b-card no-body>
<b-tabs card align="center">
<b-tab title="" title-item-class="d-none"></b-tab>
<b-tab title=""></b-tab> <!-- title-item-class="d-none" -->
<b-tab title="Test 1">
<p>Test 1</p>
</b-tab>

Wyświetl plik

@ -2,7 +2,7 @@ import Component from "vue-class-component";
import Vue from "vue";
import WithRender from "./toolbox.vue";
import "./toolbox.scss";
import { Prop, Ref } from "vue-property-decorator";
import { Prop } from "vue-property-decorator";
import Client from "facilmap-client";
import { InjectClient } from "../client/client";
import { InjectMapComponents, InjectMapContext, MapComponents, MapContext } from "../leaflet-map/leaflet-map";
@ -12,10 +12,11 @@ import About from "../about/about";
import Sidebar from "../ui/sidebar/sidebar";
import Icon from "../ui/icon/icon";
import context from "../context";
import PadSettings from "../pad/pad-settings";
@WithRender
@Component({
components: { About, Icon, Sidebar }
components: { About, Icon, PadSettings, Sidebar }
})
export default class Toolbox extends Vue {
@ -26,11 +27,11 @@ export default class Toolbox extends Vue {
hasImportUi = true; // TODO
get isNarrow() {
get isNarrow(): boolean {
return context.isNarrow;
}
get links() {
get links(): Record<'osm' | 'google' | 'bing' | 'facilmap', string> {
const v = this.mapContext;
return {
osm: `https://www.openstreetmap.org/#map=${v.zoom}/${v.center.lat}/${v.center.lng}`,
@ -40,7 +41,7 @@ export default class Toolbox extends Vue {
};
}
get filterQuery() {
get filterQuery(): Record<'q' | 'a', string> {
const v = this.mapContext;
if (v.filter) {
return {
@ -52,18 +53,18 @@ export default class Toolbox extends Vue {
}
}
get baseLayers() {
get baseLayers(): Array<{ key: string; name: string; active: boolean }> {
return Object.keys(baseLayers).map((key) => ({
key,
name: baseLayers[key].options.fmName,
name: baseLayers[key].options.fmName!,
active: this.mapContext.layers.baseLayer === key
}));
}
get overlays() {
get overlays(): Array<{ key: string; name: string; active: boolean }> {
return Object.keys(overlays).map((key) => ({
key,
name: overlays[key].options.fmName,
name: overlays[key].options.fmName!,
active: this.mapContext.layers.overlays.includes(key)
}));
}
@ -75,15 +76,15 @@ export default class Toolbox extends Vue {
map.linesUi.addLine(type);
} */
displayView(view: View) {
displayView(view: View): void {
displayView(this.mapComponents.map, view);
}
setBaseLayer(key: string) {
setBaseLayer(key: string): void {
setBaseLayer(this.mapComponents.map, key);
}
toggleOverlay(key: string) {
toggleOverlay(key: string): void {
toggleOverlay(this.mapComponents.map, key);
}
@ -95,10 +96,6 @@ export default class Toolbox extends Vue {
manageViews();
}
editPadSettings() {
map.padUi.editPadSettings();
}
editObjectTypes() {
map.typesUi.editTypes();
}
@ -115,10 +112,6 @@ export default class Toolbox extends Vue {
fmAbout.showAbout(map);
}
startPad() {
map.padUi.createPad();
}
filter() {
fmFilter.showFilterDialog(map.client.filterExpr, map.client.types).then(function(newFilter) {
map.client.setFilter(newFilter);

Wyświetl plik

@ -2,13 +2,13 @@
<a v-if="isNarrow" href="javascript:" class="fm-toolbox-toggle" v-b-toggle.fm-toolbox-sidebar><Icon icon="menu-hamburger"></Icon></a>
<Sidebar id="fm-toolbox-sidebar">
<b-nav-item v-if="!client.padId && interactive" href="javascript:" @click="startPad()">Start collaborative map</b-nav-item>
<b-nav-item-dropdown v-if="!client.readonly && client.padId" text="Add" :disabled="!!client.interaction" right>
<b-nav-item v-if="!client.padId && interactive" href="javascript:" v-b-modal.fm-toolbox-create-pad v-b-toggle.fm-toolbox-sidebar>Start collaborative map</b-nav-item>
<b-nav-item-dropdown v-if="!client.readonly && client.padData" text="Add" :disabled="!!client.interaction" right>
<b-dropdown-item v-for="type in client.types" :disabled="!!client.interaction" href="javascript:" @click="addObject(type)">{{type.name}}</b-dropdown-item>
<b-dropdown-divider v-if="client.writable == 2"></b-dropdown-divider>
<b-dropdown-item v-if="client.writable == 2" :disabled="!!client.interaction" href="javascript:" @click="editObjectTypes()">Manage types</b-dropdown-item>
</b-nav-item-dropdown>
<b-nav-item-dropdown v-if="client.padId && (!client.readonly || (client.views | fmPropertyCount) != 0)" text="Views" right>
<b-nav-item-dropdown v-if="client.padData && (!client.readonly || Object.keys(client.views).length > 0)" text="Views" right>
<b-dropdown-item v-for="(id, view) in client.views" href="javascript:" @click="displayView(view)">{{view.name}}</b-dropdown-item>
<b-dropdown-divider v-if="client.writable == 2"></b-dropdown-divider>
<b-dropdown-item v-if="client.writable == 2" href="javascript:" @click="saveView()">Save current view</b-dropdown-item>
@ -26,19 +26,21 @@
<b-nav-item-dropdown text="Tools" right>
<!--<b-dropdown-item v-if="!client.readonly" @click="openDialog('copy-pad-dialog')">Copy pad</b-dropdown-item>-->
<b-dropdown-item v-if="hasImportUi && interactive" href="javascript:" @click="importFile()">Open file</b-dropdown-item>
<b-dropdown-item v-if="client.padId" :href="`${client.padData.id}/geojson${filterQuery.q}`" title="GeoJSON files store all map information and can thus be used for map backups and be re-imported without any loss.">Export as GeoJSON</b-dropdown-item>
<b-dropdown-item v-if="client.padId" :href="`${client.padData.id}/gpx?useTracks=1${filterQuery.a}`" title="GPX files can be opened with most navigation software. In track mode, any calculated routes are saved in the file.">Export as GPX (tracks)</b-dropdown-item>
<b-dropdown-item v-if="client.padId" :href="`${client.padData.id}/gpx?useTracks=0${filterQuery.a}`" title="GPX files can be opened with most navigation software. In route mode, only the start/end/via points are saved in the file, and the navigation software needs to recalculate the routes.">Export as GPX (routes)</b-dropdown-item>
<b-dropdown-item v-if="client.padId" :href="`${client.padData.id}/table${filterQuery.q}`" target="_blank">Export as table</b-dropdown-item>
<b-dropdown-divider v-if="client.padId"></b-dropdown-divider>
<b-dropdown-item v-if="client.padId" href="javascript:" @click="filter()">Filter</b-dropdown-item>
<b-dropdown-item v-if="client.writable == 2 && client.padId" href="javascript:" @click="editPadSettings()">Settings</b-dropdown-item>
<b-dropdown-item v-if="!client.readonly && client.padId" href="javascript:" @click="showHistory()">Show edit history</b-dropdown-item>
<b-dropdown-divider v-if="client.padId"></b-dropdown-divider>
<b-dropdown-item v-if="client.padData" :href="`${client.padData.id}/geojson${filterQuery.q}`" title="GeoJSON files store all map information and can thus be used for map backups and be re-imported without any loss.">Export as GeoJSON</b-dropdown-item>
<b-dropdown-item v-if="client.padData" :href="`${client.padData.id}/gpx?useTracks=1${filterQuery.a}`" title="GPX files can be opened with most navigation software. In track mode, any calculated routes are saved in the file.">Export as GPX (tracks)</b-dropdown-item>
<b-dropdown-item v-if="client.padData" :href="`${client.padData.id}/gpx?useTracks=0${filterQuery.a}`" title="GPX files can be opened with most navigation software. In route mode, only the start/end/via points are saved in the file, and the navigation software needs to recalculate the routes.">Export as GPX (routes)</b-dropdown-item>
<b-dropdown-item v-if="client.padData" :href="`${client.padData.id}/table${filterQuery.q}`" target="_blank">Export as table</b-dropdown-item>
<b-dropdown-divider v-if="client.padData"></b-dropdown-divider>
<b-dropdown-item v-if="client.padData" href="javascript:" @click="filter()">Filter</b-dropdown-item>
<b-dropdown-item v-if="client.writable == 2 && client.padData" href="javascript:" v-b-modal.fm-toolbox-edit-pad v-b-toggle.fm-toolbox-sidebar>Settings</b-dropdown-item>
<b-dropdown-item v-if="!client.readonly && client.padData" href="javascript:" @click="showHistory()">Show edit history</b-dropdown-item>
<b-dropdown-divider v-if="client.padData"></b-dropdown-divider>
<b-dropdown-item v-b-modal.fm-toolbox-about v-b-toggle.fm-toolbox-sidebar href="javascript:">About FacilMap</b-dropdown-item>
<b-dropdown-item v-if="client.padId" :href="links.facilmap">Exit collaborative map</b-dropdown-item>
<b-dropdown-item v-if="client.padData" :href="links.facilmap">Exit collaborative map</b-dropdown-item>
</b-nav-item-dropdown>
</Sidebar>
<About id="fm-toolbox-about"></About>
<PadSettings v-if="!client.padId" id="fm-toolbox-create-pad" :isCreate="true"></PadSettings>
<PadSettings v-if="client.padData" id="fm-toolbox-edit-pad"></PadSettings>
</div>

Wyświetl plik

@ -0,0 +1,25 @@
import WithRender from "./form-modal.vue";
import Vue from "vue";
import Component from "vue-class-component";
import { ValidationObserver } from "vee-validate";
import { Prop } from "vue-property-decorator";
@WithRender
@Component({
components: { ValidationObserver }
})
export default class FormModal extends Vue {
@Prop({ type: String, required: true }) readonly id!: string;
@Prop({ type: String }) readonly title?: string;
@Prop({ type: String }) readonly dialogClass?: string;
@Prop({ type: Boolean }) readonly noCancel?: boolean;
@Prop({ type: Boolean }) readonly isSaving?: boolean;
@Prop({ type: Boolean }) readonly isCreate?: boolean;
@Prop({ type: Boolean, default: true }) readonly isModified?: boolean;
handleSubmit(e: Event): void {
this.$emit("submit", e);
}
}

Wyświetl plik

@ -0,0 +1,23 @@
<ValidationObserver v-slot="observer">
<b-modal
:id="id"
:title="title"
size="lg"
:dialog-class="dialogClass"
:no-close-on-esc="noCancel" :no-close-on-backdrop="noCancel" :hide-header-close="noCancel" :ok-only="noCancel"
:busy="isSaving"
:ok-disabled="!isCreate && !isModified"
:ok-title="isCreate ? 'Create' : 'Save'"
@ok.prevent="observer.handleSubmit(handleSubmit)"
@show="$emit('show')"
>
<b-form @submit.prevent="observer.handleSubmit(handleSubmit)">
<template>
<slot v-bind="observer"></slot>
</template>
<button type="submit" class="d-none"></button>
</b-form>
<slot name="after-form"></slot>
</b-modal>
</ValidationObserver>

Wyświetl plik

@ -9,7 +9,7 @@ import { createSymbolHtml } from "facilmap-leaflet";
})
export default class Icon extends Vue {
@Prop({ type: String }) icon!: string;
@Prop({ type: String, required: true }) icon!: string;
get iconCode() {
return createSymbolHtml("currentColor", "1.5em", this.icon);

Wyświetl plik

@ -10,7 +10,7 @@ import $ from "jquery";
})
export default class Sidebar extends Vue {
@Prop({ type: String }) readonly id!: string;
@Prop({ type: String, required: true }) readonly id!: string;
touchStartX: number | null = null;
sidebarVisible = false;

Wyświetl plik

@ -0,0 +1,104 @@
import { mergeObject } from "../utils";
test('mergeObject', () => {
interface TestObject {
str?: string;
obj?: {
str?: string;
}
}
function merge(oldObject: TestObject, newObject: TestObject, targetObject: TestObject): TestObject {
mergeObject(oldObject, newObject, targetObject);
return targetObject;
}
expect(merge({ str: "old" }, { str: "old" }, { str: "custom" }))
.toEqual({ str: "custom" });
expect(merge({ }, { }, { str: "custom" }))
.toEqual({ str: "custom" });
expect(merge({ str: "old" }, { str: "old" }, { }))
.toEqual({ });
expect(merge({ }, { }, { }))
.toEqual({ });
expect(merge({ str: "old" }, { str: "new" }, { str: "custom" }))
.toEqual({ str: "new" });
expect(merge({ }, { str: "new" }, { str: "custom" }))
.toEqual({ str: "new" });
expect(merge({ str: "old" }, { }, { str: "custom" }))
.toEqual({ });
expect(merge({ str: "old" }, { str: "new" }, { }))
.toEqual({ str: "new" });
expect(merge({ }, { str: "new" }, { }))
.toEqual({ str: "new" });
expect(merge({ obj: { str: "old" } }, { obj: { str: "old" } }, { obj: { str: "custom" } }))
.toEqual({ obj: { str: "custom" } });
expect(merge({ obj: { } }, { obj: { } }, { obj: { str: "custom" } }))
.toEqual({ obj: { str: "custom" } });
expect(merge({ }, { }, { obj: { str: "custom" } }))
.toEqual({ obj: { str: "custom" } });
expect(merge({ obj: { str: "old" } }, { obj: { str: "old" } }, { obj: { } }))
.toEqual({ obj: { } });
expect(merge({ obj: { } }, { obj: { } }, { obj: { } }))
.toEqual({ obj: { } });
expect(merge({ }, { }, { obj: { } }))
.toEqual({ obj: { } });
expect(merge({ obj: { str: "old" } }, { obj: { str: "old" } }, { }))
.toEqual({ });
expect(merge({ obj: { } }, { obj: { } }, { }))
.toEqual({ });
expect(merge({ obj: { str: "old" } }, { obj: { str: "new" } }, { obj: { str: "custom" } }))
.toEqual({ obj: { str: "new" } });
expect(merge({ obj: { } }, { obj: { str: "new" } }, { obj: { str: "custom" } }))
.toEqual({ obj: { str: "new" } });
expect(merge({ }, { obj: { str: "new" } }, { obj: { str: "custom" } }))
.toEqual({ obj: { str: "new" } });
expect(merge({ obj: { str: "old" } }, { obj: { } }, { obj: { str: "custom" } }))
.toEqual({ obj: { } });
expect(merge({ obj: { str: "old" } }, { }, { obj: { str: "custom" } }))
.toEqual({ });
expect(merge({ obj: { str: "old" } }, { obj: { str: "new" } }, { obj: { } }))
.toEqual({ obj: { str: "new" } });
expect(merge({ obj: { } }, { obj: { str: "new" } }, { obj: { } }))
.toEqual({ obj: { str: "new" } });
expect(merge({ obj: { str: "old" } }, { obj: { str: "new" } }, { }))
.toEqual({ obj: { str: "new" } });
expect(merge({ obj: { } }, { obj: { str: "new" } }, { }))
.toEqual({ obj: { str: "new" } });
});

Wyświetl plik

@ -1,84 +1,16 @@
import $ from 'jquery';
import './ui.scss';
import { deobfuscate } from "facilmap-utils";
import { Colour } from "facilmap-types";
import { RAINBOW_STOPS } from "facilmap-leaflet";
/*
fmUtils.createLineGraphic = function(colour, width, length) {
export function createLineGraphic(colour: Colour, width: number, length: number): string {
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="${length}" height="${width}" version="1.1">` +
(colour == null ? `<defs><linearGradient id="rainbow" x2="100%" y2="0">${fmUtils.RAINBOW_STOPS}</linearGradient></defs>` : ``) +
(colour == null ? `<defs><linearGradient id="rainbow" x2="100%" y2="0">${RAINBOW_STOPS}</linearGradient></defs>` : ``) +
`<rect x="0" y="0" width="${length}" height="${width}" style="fill:${colour == null ? `url(#rainbow)` : `#${colour}`}"/>` +
`</svg>`);
};
fmUtils.copyToClipboard = function(text) {
var el = $('<button type="button"></button>').css("display", "none").appendTo("body");
var c = new Clipboard(el[0], {
text: function() {
return text;
}
});
el.click().remove();
c.destroy();
};
fmUtils.onLongMouseDown = function(map, callback) {
var mouseDownTimeout, pos;
function clear() {
clearTimeout(mouseDownTimeout);
mouseDownTimeout = pos = null;
map.off("mousemove", move);
map.off("mouseup", clear);
}
function move(e) {
if(pos.distanceTo(e.containerPoint) > map.dragging._draggable.options.clickTolerance)
clear();
}
map.on("mousedown", function(e) {
clear();
if(e.originalEvent.which != 1) // Only react to left click
return;
pos = e.containerPoint;
mouseDownTimeout = setTimeout(function() {
callback(e);
}, 1000);
map.on("mousemove", move);
map.on("mouseup", clear);
});
};
fmUtils.scrollIntoView = function(element) {
element = $(element);
let scrollableParent = element.scrollParent();
function getOffset(el) {
let ret = 0;
let t = el;
while(t) {
ret += t.offsetTop;
t = t.offsetParent;
}
return ret;
}
let parentHeight = scrollableParent[0].clientHeight;
let resultTop = getOffset(element[0]) - getOffset(scrollableParent[0]);
let resultBottom = resultTop + element.outerHeight();
if(scrollableParent[0].scrollTop > resultTop)
scrollableParent.animate({scrollTop: resultTop});
else if(scrollableParent[0].scrollTop < resultBottom - parentHeight)
scrollableParent.animate({scrollTop: resultBottom - parentHeight});
};
*/
}
export function registerDeobfuscationHandlers(): void {
const clickHandler = (e: JQuery.ClickEvent) => {

Wyświetl plik

@ -0,0 +1,18 @@
import Vue from "vue";
import { isEqual } from "lodash";
import { clone } from "facilmap-utils";
/**
* Performs a 3-way merge. Takes the difference between oldObject and newObject and applies it to targetObject.
* @param oldObject {Object}
* @param newObject {Object}
* @param targetObject {Object}
*/
export function mergeObject<T extends Record<keyof any, any>>(oldObject: T, newObject: T, targetObject: T): void {
for(const i of new Set<keyof T & (number | string)>([...Object.keys(newObject), ...Object.keys(targetObject)])) {
if(typeof newObject[i] == "object" && newObject[i] != null && targetObject[i] != null)
mergeObject(oldObject && oldObject[i], newObject[i], targetObject[i]);
else if(oldObject == null || !isEqual(oldObject[i], newObject[i]))
Vue.set(targetObject, i, clone(newObject[i]));
}
}

Wyświetl plik

@ -0,0 +1,18 @@
import { extend, withValidation } from "vee-validate";
extend("required", {
validate: (val: any) => !!val,
message: "Must not be empty.",
computesRequired: true
});
extend("padId", {
validate: (id: string) => !id.includes("/"),
message: "May not contain a slash."
});
export type ValidationContext = Parameters<Exclude<Parameters<typeof withValidation>[1], undefined>>[0];
export function getValidationState(v: ValidationContext): boolean | null {
return v.dirty || v.validated ? v.valid : null;
}

Wyświetl plik

@ -38,20 +38,20 @@ async function updateIcons() {
async function cleanIcon(icon: string): Promise<string> {
const optimized = await new svgo().optimize(icon);
var $ = cheerio.load(optimized.data, {
const $ = cheerio.load(optimized.data, {
xmlMode: true
});
for (const el of $("*").toArray()) {
for (const el of $("*").toArray() as cheerio.TagElement[]) {
el.name = el.name.replace(/^svg:/, "");
}
$("metadata,sodipodi\\:namedview,defs,image").remove();
for (const el of $("*").toArray()) {
var $el = $(el);
const $el = $(el);
var fill = $el.css("fill") || $el.attr("fill");
const fill = $el.css("fill") || $el.attr("fill");
if(fill && fill != "none") {
if(fill != "#ffffff" && fill != "#fff") { // This is the background
$el.remove();

Wyświetl plik

@ -23,6 +23,7 @@
},
"scripts": {
"build": "webpack",
"build-module": "webpack --config-name module",
"clean": "rimraf dist",
"dev-server": "webpack serve --mode development --config-name full",
"download-icons": "ts-node ./download-icons.ts",
@ -36,7 +37,7 @@
"filtrex": "^2.1.0",
"leaflet-auto-graticule": "^1.0.9",
"leaflet-draggable-lines": "^1.0.6",
"leaflet-freie-tonne": "^1.0.4",
"leaflet-freie-tonne": "^1.0.5",
"leaflet-geometryutil": "^0.9.3",
"leaflet-hash": "^0.2.1",
"leaflet-highlightable-layers": "^1.0.8",
@ -57,6 +58,7 @@
"@types/webpack-env": "^1.16.0",
"@types/webpack-node-externals": "^2.5.0",
"@types/yauzl": "^2.9.1",
"cheerio": "^1.0.0-rc.5",
"css-loader": "^5.1.0",
"highland": "^2.13.5",
"jest": "^26.6.3",

Wyświetl plik

@ -1,13 +1,13 @@
import Socket, { SocketEvents } from "facilmap-client";
import Client, { ClientEvents } from "facilmap-client";
import { EventHandler } from "facilmap-types";
import { Handler, Map } from "leaflet";
import { leafletToFmBbox } from "./utils/leaflet";
export default class BboxHandler extends Handler {
client: Socket;
client: Client;
constructor(map: Map, client: Socket) {
constructor(map: Map, client: Client) {
super(map);
this.client = client;
}
@ -22,7 +22,7 @@ export default class BboxHandler extends Handler {
}
}
handleEmit: EventHandler<SocketEvents, "emit"> = (name, data) => {
handleEmit: EventHandler<ClientEvents, "emit"> = (name, data) => {
if (["setPadId", "setRoute"].includes(name)) {
this.updateBbox();
}

Wyświetl plik

@ -1,4 +1,4 @@
import Socket, { TrackPoints } from "facilmap-client";
import Client, { TrackPoints } from "facilmap-client";
import { ID, Line, LinePointsEvent, ObjectWithId } from "facilmap-types";
import { FeatureGroup, LayerOptions, Map, PolylineOptions } from "leaflet";
import { HighlightableLayerOptions, HighlightablePolyline } from "leaflet-highlightable-layers";
@ -11,11 +11,11 @@ interface LinesLayerOptions extends LayerOptions {
export default class LinesLayer extends FeatureGroup {
options!: LayerOptions;
client: Socket;
client: Client;
linesById: Record<string, InstanceType<typeof HighlightablePolyline>> = {};
highlightedLinesIds = new Set<ID>();
constructor(client: Socket, options?: LinesLayerOptions) {
constructor(client: Client, options?: LinesLayerOptions) {
super([], options);
this.client = client;
}
@ -60,7 +60,7 @@ export default class LinesLayer extends FeatureGroup {
};
handleFilter = (): void => {
for(const i of Object.keys(this.client.lines) as any as Array<keyof Socket['lines']>) {
for(const i of Object.keys(this.client.lines) as any as Array<keyof Client['lines']>) {
const show = this._map.fmFilterFunc(this.client.lines[i]);
if(this.linesById[i] && !show)
this._deleteLine(this.client.lines[i]);

Wyświetl plik

@ -1,4 +1,4 @@
import Socket from "facilmap-client";
import Client from "facilmap-client";
import { Map, PathOptions, PolylineOptions } from "leaflet";
import { HighlightableLayerOptions, HighlightablePolyline } from "leaflet-highlightable-layers";
import { trackPointsToLatLngArray } from "../utils/leaflet";
@ -10,10 +10,10 @@ interface RouteLayerOptions extends PolylineOptions {
export default class RouteLayer extends HighlightablePolyline {
realOptions!: RouteLayerOptions;
client: Socket;
client: Client;
draggable?: DraggableLines;
constructor(client: Socket, options?: RouteLayerOptions) {
constructor(client: Client, options?: RouteLayerOptions) {
super([], options);
this.client = client;
}

Wyświetl plik

@ -1,5 +1,5 @@
import { Map, MarkerClusterGroup, MarkerClusterGroupOptions } from "leaflet";
import Socket from "facilmap-client";
import Client from "facilmap-client";
import { PadData } from "facilmap-types";
import "leaflet.markercluster";
import "leaflet.markercluster/dist/MarkerCluster.css";
@ -10,10 +10,10 @@ export interface MarkerClusterOptions extends MarkerClusterGroupOptions {
export default class MarkerCluster extends MarkerClusterGroup {
client: Socket;
client: Client;
_maxClusterRadiusBkp: MarkerClusterOptions['maxClusterRadius'];
constructor(client: Socket, options?: MarkerClusterOptions) {
constructor(client: Client, options?: MarkerClusterOptions) {
super({
showCoverageOnHover: false,
maxClusterRadius: 50,

Wyświetl plik

@ -1,4 +1,4 @@
import Socket from 'facilmap-client';
import Client from 'facilmap-client';
import { ID, Marker, ObjectWithId } from 'facilmap-types';
import { Map } from 'leaflet';
import { tooltipOptions } from '../utils/leaflet';
@ -12,11 +12,11 @@ export interface MarkersLayerOptions extends MarkerClusterOptions {
export default class MarkersLayer extends MarkerCluster {
options!: MarkersLayerOptions;
client: Socket;
client: Client;
markersById: Record<string, MarkerLayer> = {};
highlightedMarkerIds = new Set<ID>();
constructor(client: Socket, options?: MarkersLayerOptions) {
constructor(client: Client, options?: MarkersLayerOptions) {
super(client, options);
this.client = client;
}
@ -53,7 +53,7 @@ export default class MarkersLayer extends MarkerCluster {
};
handleFilter = (): void => {
for(const i of Object.keys(this.client.markers) as any as Array<keyof Socket['markers']>) {
for(const i of Object.keys(this.client.markers) as any as Array<keyof Client['markers']>) {
const show = this._map.fmFilterFunc(this.client.markers[i]);
if(this.markersById[i] && !show)
this._deleteMarker(this.client.markers[i]);

Wyświetl plik

@ -14,7 +14,7 @@ for (const key of rawIconsContext.keys() as string[]) {
}
const iconList = Object.keys(rawIcons).map((key) => Object.keys(rawIcons[key])).flat();
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"/>`;
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"/>`;
interface ShapeInfo {
svg: string;
@ -70,7 +70,7 @@ export function getIcon(colour: Colour, size: number, iconName: string): string
const moveX = (sizes[set] - Number(el.getAttribute("width"))) / 2;
const moveY = (sizes[set] - Number(el.getAttribute("height"))) / 2;
return `<g transform="scale(${scale}) translate(${moveX}, ${moveY})" fill="${colour}">${rawIcons[set][iconName]}</g>`;
return `<g transform="scale(${scale}) translate(${moveX}, ${moveY})" fill="${colour}">${el.innerHTML}</g>`;
}
export function getSymbolCode(colour: Colour, size: number, symbol?: Symbol): string {
@ -91,9 +91,9 @@ export function createSymbol(colour: Colour, height: number, symbol?: Symbol): s
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}
export function createSymbolHtml(colour: string, height: number, symbol?: Symbol): string {
return `<svg width="${height}" height="${height}" viewbox="0 0 ${height} ${height}">` +
getSymbolCode(colour, height, symbol) +
export function createSymbolHtml(colour: string, height: number | string, symbol?: Symbol): string {
return `<svg width="${height}" height="${height}" viewbox="0 0 25 25">` +
getSymbolCode(colour, 25, symbol) +
`</svg>`;
}

Wyświetl plik

@ -1,4 +1,4 @@
import Socket from 'facilmap-client';
import Client from 'facilmap-client';
import L, { Evented, Handler, LatLng, Map } from 'leaflet';
import 'leaflet-hash';
import { isEqual } from 'lodash';
@ -24,13 +24,13 @@ interface Query {
export default class HashHandler extends Handler {
socket: Socket;
client: Client;
hash: any;
activeQuery?: Query;
constructor(map: Map, socket: Socket) {
constructor(map: Map, client: Client) {
super(map);
this.socket = socket;
this.client = client;
this.hash = Object.assign(new L.Hash(), {
parseHash: this.parseHash,
@ -77,8 +77,8 @@ export default class HashHandler extends Handler {
this.fireEvent("fmHash", { hash });
const viewMatch = hash.match(/^q=v(\d+)$/i);
if(viewMatch && this.socket.views[viewMatch[1] as any]) {
displayView(this._map, this.socket.views[viewMatch[1] as any]);
if(viewMatch && this.client.views[viewMatch[1] as any]) {
displayView(this._map, this.client.views[viewMatch[1] as any]);
return false;
}
@ -126,12 +126,12 @@ export default class HashHandler extends Handler {
result = "#q=" + encodeURIComponent(this.activeQuery.query);
} else if(!this.activeQuery) {
// Check if we have a saved view open
const defaultView = (this.socket.padData && this.socket.padData.defaultViewId && this.socket.views[this.socket.padData.defaultViewId]);
const defaultView = (this.client.padData && this.client.padData.defaultViewId && this.client.views[this.client.padData.defaultViewId]);
if(isAtView(this._map, defaultView || undefined))
result = "#";
else {
for(const viewId of Object.keys(this.socket.views)) {
if(isAtView(this._map, this.socket.views[viewId as any])) {
for(const viewId of Object.keys(this.client.views)) {
if(isAtView(this._map, this.client.views[viewId as any])) {
result = `#q=v${encodeURIComponent(viewId)}`;
break;
}

Wyświetl plik

@ -1,18 +1,18 @@
import Socket from "facilmap-client";
import Client from "facilmap-client";
import { PadData } from "facilmap-types";
import { UnsavedView } from "./views";
export async function getInitialView(socket: Socket): Promise<UnsavedView | undefined> {
if(socket.padId) {
const padData = socket.padData || await new Promise<PadData>((resolve) => {
socket.on("padData", resolve);
export async function getInitialView(client: Client): Promise<UnsavedView | undefined> {
if(client.padId) {
const padData = client.padData || await new Promise<PadData>((resolve) => {
client.on("padData", resolve);
});
return padData.defaultView;
}
try {
const geoip = await socket.geoip();
const geoip = await client.geoip();
if (geoip) {
return { ...geoip, baseLayer: undefined as any, layers: [] };

Wyświetl plik

@ -21,6 +21,7 @@
"start": "npm run deps && npm run server",
"deps": "npm install",
"server": "ts-node --transpile-only src/server.ts",
"dev-server": "FM_DEV=true ts-node --transpile-only src/server.ts",
"test": "jest",
"types": "tsc --noEmit src/**/*.ts",
"lint": "eslint src/**/*.ts"

Wyświetl plik

@ -82,7 +82,7 @@ class SocketConnection {
} catch (err) {
console.log(err.stack);
callback && callback(err);
callback && callback({ message: err.message, stack: err.stack });
}
});
}

Wyświetl plik

@ -40,10 +40,7 @@ export async function initWebserver(database: Database, port: number, host?: str
getFrontendFile("map.ejs"),
(async () => {
if(req.params && req.params.padId) {
return database.pads.getPadData(req.params.padId).then((padData) => {
// We only look up by read ID. At the moment, we only need the data for the search engine
// meta tags, and those should be only enabled in the read-only version anyways.
return database.pads.getPadDataByAnyId(req.params.padId).then((padData) => {
if (!padData)
throw new Error();
return padData;

Wyświetl plik

@ -0,0 +1,3 @@
module.exports = {
preset: 'ts-jest',
};

Wyświetl plik

@ -32,6 +32,9 @@
"marked": "^2.0.1"
},
"devDependencies": {
"@types/jest": "^26.0.20",
"jest": "^26.6.3",
"ts-jest": "^26.5.3",
"typescript": "^4.2.2"
}
}

Wyświetl plik

@ -1,3 +1,5 @@
import { isEqual } from "lodash";
const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const LENGTH = 12;
@ -99,21 +101,6 @@ export function encodeQueryString(obj: Record<string, string>): string {
return pairs.join("&");
}
/**
* Performs a 3-way merge. Takes the difference between oldObject and newObject and applies it to targetObject.
* @param oldObject {Object}
* @param newObject {Object}
* @param targetObject {Object}
*/
/* export function mergeObject<T extends Record<keyof any, any>>(oldObject: T, newObject: T, targetObject: T): void {
for(const i of new Set([...Object.keys(newObject), ...Object.keys(targetObject)])) {
if(typeof newObject[i] == "object" && newObject[i] != null && targetObject[i] != null)
mergeObject(oldObject && oldObject[i], newObject[i], targetObject[i]);
else if(oldObject == null || !ng.equals(oldObject[i], newObject[i]))
targetObject[i] = ng.copy(newObject[i]);
}
}*/
export function clone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
return obj != null ? JSON.parse(JSON.stringify(obj)) : obj;
}

9036
yarn.lock 100644

Plik diff jest za duży Load Diff