kopia lustrzana https://github.com/FacilMap/facilmap
Status commit
rodzic
cc87787a37
commit
72edadfa84
90
.eslintrc.js
90
.eslintrc.js
|
@ -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"]
|
||||
}
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
*.iml
|
||||
.idea
|
||||
node_modules
|
||||
bower_components
|
||||
frontend/build
|
||||
client/dist
|
||||
*/dist
|
||||
yarn-error.log
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
------
|
||||
|
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
**/*.js
|
|
@ -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"
|
||||
}
|
||||
};
|
|
@ -10,7 +10,3 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.leaflet-control-locate.leaflet-control-locate a {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
<div class="modal-header">
|
||||
<button ng-if="!noCancel" type="button" class="close" ng-click="$dismiss()"><span aria-hidden="true">×</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>
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
});
|
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
$scope.$watch(() => (map.client.padData && map.client.padData.name), function(newVal) {
|
||||
$scope.padName = newVal;
|
||||
});
|
||||
|
||||
$scope.$watch(() => (map.client.padId), function(padId) {
|
||||
if (padId)
|
||||
history.replaceState(null, "", fm.URL_PREFIX + padId + location.search + location.hash);
|
||||
});
|
||||
}, 0); */
|
||||
history.replaceState(null, "", context.urlPrefix + padId + location.search + location.hash);
|
||||
}
|
||||
|
||||
export function updatePadName(padName: string): void {
|
||||
const title = padName ? padName + ' – FacilMap' : 'FacilMap';
|
||||
|
||||
// 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;
|
||||
}
|
|
@ -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" />
|
||||
<% } %>
|
||||
|
|
|
@ -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 }
|
||||
}));
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<ClientProvider :serverUrl='serverUrl' :padId='padId'>
|
||||
<ClientProvider :serverUrl="serverUrl" :padId="padId" @padId="handlePadIdChange" @padName="handlePadNameChange">
|
||||
<Main/>
|
||||
</ClientProvider>
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" } });
|
||||
|
||||
});
|
|
@ -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) => {
|
||||
|
|
|
@ -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]));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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>`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: [] };
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -82,7 +82,7 @@ class SocketConnection {
|
|||
} catch (err) {
|
||||
console.log(err.stack);
|
||||
|
||||
callback && callback(err);
|
||||
callback && callback({ message: err.message, stack: err.stack });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Plik diff jest za duży
Load Diff
Ładowanie…
Reference in New Issue