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 = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
plugins: [
|
plugins: ['@typescript-eslint', 'import'],
|
||||||
'@typescript-eslint',
|
extends: ['plugin:import/typescript'],
|
||||||
],
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:import/errors',
|
|
||||||
'plugin:import/warnings',
|
|
||||||
'plugin:import/typescript'
|
|
||||||
],
|
|
||||||
env: {
|
env: {
|
||||||
node: true
|
node: true
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/explicit-module-boundary-types": ["warn", { "allowArgumentsExplicitlyTypedAsAny": true }],
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"import/no-unresolved": ["error", { "ignore": [ "geojson" ], "caseSensitive": true }],
|
||||||
"@typescript-eslint/ban-types": "off",
|
"import/no-extraneous-dependencies": ["error"],
|
||||||
"@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/no-unused-vars": ["warn", { "args": "none" }],
|
"@typescript-eslint/no-unused-vars": ["warn", { "args": "none" }],
|
||||||
"@typescript-eslint/no-inferrable-types": "off",
|
"import/no-named-as-default": ["warn"],
|
||||||
"import/no-unresolved": ["error", { "ignore": ["geojson" ] }],
|
"import/no-named-as-default-member": ["warn"],
|
||||||
"import/export": "off"
|
"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
|
*.iml
|
||||||
.idea
|
.idea
|
||||||
node_modules
|
node_modules
|
||||||
bower_components
|
*/dist
|
||||||
frontend/build
|
yarn-error.log
|
||||||
client/dist
|
|
||||||
|
|
|
@ -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
|
If the opening the pad failed ([`setPadId(padId)`](#setpadidpadid) promise got rejected), the error message is stored
|
||||||
in this property.
|
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
|
Events
|
||||||
------
|
------
|
||||||
|
|
|
@ -21,13 +21,7 @@ Setting it up
|
||||||
Install facilmap-client as a dependency using npm or yarn:
|
Install facilmap-client as a dependency using npm or yarn:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install --save facilmap-client
|
npm install -S facilmap-client
|
||||||
```
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn add facilmap-client
|
|
||||||
```
|
```
|
||||||
|
|
||||||
or load the client directly from facilmap.org (along with socket.io, which is needed by 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
|
### 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`.
|
to create the bundle in `dist/client.js`.
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,15 +44,46 @@ Setting up a connection
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
```js
|
```js
|
||||||
let conn = new FacilMap.Client("https://facilmap.org/");
|
let client = new FacilMap.Client("https://facilmap.org/");
|
||||||
conn.setPadId("myMapId").then(() => {
|
client.setPadId("myMapId").then((padData) => {
|
||||||
console.log(conn.padData);
|
console.log(padData);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
Using it
|
Using it
|
||||||
--------
|
--------
|
||||||
|
|
||||||
A detailed description of all the methods and data types can be found in [API](./API.md).
|
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 { Manager, Socket as SocketIO } from "socket.io-client";
|
||||||
import {
|
import {
|
||||||
|
Bbox,
|
||||||
BboxWithZoom, EventHandler, EventName, FindOnMapQuery, FindQuery, HistoryEntry, ID, Line, LineCreate,
|
BboxWithZoom, EventHandler, EventName, FindOnMapQuery, FindQuery, HistoryEntry, ID, Line, LineCreate,
|
||||||
LineExportRequest, LineTemplateRequest, LineUpdate, MapEvents, Marker, MarkerCreate, MarkerUpdate, MultipleEvents, ObjectWithId,
|
LineExportRequest, LineTemplateRequest, LineUpdate, MapEvents, Marker, MarkerCreate, MarkerUpdate, MultipleEvents, ObjectWithId,
|
||||||
PadData, PadDataCreate, PadDataUpdate, PadId, RequestData, RequestName, ResponseData, Route, RouteCreate, RouteExportRequest,
|
PadData, PadDataCreate, PadDataUpdate, PadId, RequestData, RequestName, ResponseData, Route, RouteCreate, RouteExportRequest,
|
||||||
|
RouteInfo,
|
||||||
RouteRequest,
|
RouteRequest,
|
||||||
|
SearchResult,
|
||||||
TrackPoint, Type, TypeCreate, TypeUpdate, View, ViewCreate, ViewUpdate, Writable
|
TrackPoint, Type, TypeCreate, TypeUpdate, View, ViewCreate, ViewUpdate, Writable
|
||||||
} from "facilmap-types";
|
} from "facilmap-types";
|
||||||
|
|
||||||
export interface SocketEvents extends MapEvents {
|
export interface ClientEvents extends MapEvents {
|
||||||
connect: [];
|
connect: [];
|
||||||
disconnect: [string];
|
disconnect: [string];
|
||||||
connect_error: [Error];
|
connect_error: [Error];
|
||||||
|
@ -26,7 +29,7 @@ export interface SocketEvents extends MapEvents {
|
||||||
emit: { [eventName in RequestName]: [eventName, RequestData<eventName>] }[RequestName]
|
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 {
|
export interface TrackPoints {
|
||||||
[idx: number]: TrackPoint;
|
[idx: number]: TrackPoint;
|
||||||
|
@ -41,7 +44,7 @@ export interface RouteWithTrackPoints extends Omit<Route, "trackPoints"> {
|
||||||
trackPoints: TrackPoints;
|
trackPoints: TrackPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Socket {
|
export default class Client {
|
||||||
disconnected: boolean = true;
|
disconnected: boolean = true;
|
||||||
server!: string;
|
server!: string;
|
||||||
padId: string | undefined = undefined;
|
padId: string | undefined = undefined;
|
||||||
|
@ -58,9 +61,10 @@ export default class Socket {
|
||||||
history: Record<ID, HistoryEntry> = { };
|
history: Record<ID, HistoryEntry> = { };
|
||||||
route: RouteWithTrackPoints | undefined = undefined;
|
route: RouteWithTrackPoints | undefined = undefined;
|
||||||
serverError: Error | undefined = undefined;
|
serverError: Error | undefined = undefined;
|
||||||
|
loading: number = 0;
|
||||||
|
|
||||||
_listeners: {
|
_listeners: {
|
||||||
[E in EventName<SocketEvents>]?: Array<EventHandler<SocketEvents, E>>
|
[E in EventName<ClientEvents>]?: Array<EventHandler<ClientEvents, E>>
|
||||||
} = { };
|
} = { };
|
||||||
_listeningToHistory: boolean = false;
|
_listeningToHistory: boolean = false;
|
||||||
|
|
||||||
|
@ -68,17 +72,25 @@ export default class Socket {
|
||||||
this._init(server, padId);
|
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.
|
// 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._set(this, 'server', server);
|
||||||
this.padId = padId;
|
this._set(this, 'padId', padId);
|
||||||
|
|
||||||
const manager = new Manager(server, { forceNew: true });
|
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>[]) {
|
for(const i of Object.keys(this._handlers) as EventName<ClientEvents>[]) {
|
||||||
this.on(i, this._handlers[i] as EventHandler<SocketEvents, typeof i>);
|
this.on(i, this._handlers[i] as EventHandler<ClientEvents, typeof i>);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -89,29 +101,27 @@ export default class Socket {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
on<E extends EventName<SocketEvents>>(eventName: E, fn: EventHandler<SocketEvents, E>) {
|
on<E extends EventName<ClientEvents>>(eventName: E, fn: EventHandler<ClientEvents, E>): void {
|
||||||
let listeners = this._listeners[eventName] as Array<EventHandler<SocketEvents, E>> | undefined;
|
if(!this._listeners[eventName]) {
|
||||||
if(!listeners) {
|
|
||||||
listeners = this._listeners[eventName] = [ ];
|
|
||||||
(MANAGER_EVENTS.includes(eventName) ? this.socket.io : this.socket)
|
(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>) {
|
once<E extends EventName<ClientEvents>>(eventName: E, fn: EventHandler<ClientEvents, E>): void {
|
||||||
let handler = ((data: any) => {
|
const handler = ((data: any) => {
|
||||||
this.removeListener(eventName, handler);
|
this.removeListener(eventName, handler);
|
||||||
(fn as any)(data);
|
(fn as any)(data);
|
||||||
}) as EventHandler<SocketEvents, E>;
|
}) as EventHandler<ClientEvents, E>;
|
||||||
this.on(eventName, handler);
|
this.on(eventName, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListener<E extends EventName<SocketEvents>>(eventName: E, fn: EventHandler<SocketEvents, E>) {
|
removeListener<E extends EventName<ClientEvents>>(eventName: E, fn: EventHandler<ClientEvents, E>): void {
|
||||||
const listeners = this._listeners[eventName] as Array<EventHandler<SocketEvents, E>> | undefined;
|
const listeners = this._listeners[eventName] as Array<EventHandler<ClientEvents, E>> | undefined;
|
||||||
if(listeners) {
|
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: {
|
_handlers: {
|
||||||
[E in EventName<SocketEvents>]?: EventHandler<SocketEvents, E>
|
[E in EventName<ClientEvents>]?: EventHandler<ClientEvents, E>
|
||||||
} = {
|
} = {
|
||||||
padData: (data) => {
|
padData: (data) => {
|
||||||
this.padData = data;
|
this._set(this, 'padData', data);
|
||||||
|
|
||||||
if(data.writable != null) {
|
if(data.writable != null) {
|
||||||
this.readonly = (data.writable == 0);
|
this._set(this, 'readonly', data.writable == 0);
|
||||||
this.writable = data.writable;
|
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)
|
if(id != null)
|
||||||
this.padId = id;
|
this._set(this, 'padId', id);
|
||||||
},
|
},
|
||||||
|
|
||||||
deletePad: () => {
|
deletePad: () => {
|
||||||
this.readonly = true;
|
this._set(this, 'readonly', true);
|
||||||
this.writable = 0;
|
this._set(this, 'writable', 0);
|
||||||
this.deleted = true;
|
this._set(this, 'deleted', true);
|
||||||
},
|
},
|
||||||
|
|
||||||
marker: (data) => {
|
marker: (data) => {
|
||||||
this.markers[data.id] = data;
|
this._set(this.markers, data.id, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteMarker: (data) => {
|
deleteMarker: (data) => {
|
||||||
delete this.markers[data.id];
|
this._delete(this.markers, data.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
line: (data) => {
|
line: (data) => {
|
||||||
this.lines[data.id] = {
|
this._set(this.lines, data.id, {
|
||||||
...data,
|
...data,
|
||||||
trackPoints: this.lines[data.id]?.trackPoints || { }
|
trackPoints: this.lines[data.id]?.trackPoints || { }
|
||||||
};
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteLine: (data) => {
|
deleteLine: (data) => {
|
||||||
delete this.lines[data.id];
|
this._delete(this.lines, data.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
linePoints: (data) => {
|
linePoints: (data) => {
|
||||||
let line = this.lines[data.id];
|
const line = this.lines[data.id];
|
||||||
if(line == null)
|
if(line == null)
|
||||||
return console.error("Received line points for non-existing line "+data.id+".");
|
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) => {
|
routePoints: (data) => {
|
||||||
|
@ -187,42 +197,42 @@ export default class Socket {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.route.trackPoints = this._mergeTrackPoints(this.route.trackPoints, data);
|
this._set(this.route, 'trackPoints', this._mergeTrackPoints(this.route.trackPoints, data));
|
||||||
},
|
},
|
||||||
|
|
||||||
view: (data) => {
|
view: (data) => {
|
||||||
this.views[data.id] = data;
|
this._set(this.views, data.id, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteView: (data) => {
|
deleteView: (data) => {
|
||||||
delete this.views[data.id];
|
this._delete(this.views, data.id);
|
||||||
if (this.padData) {
|
if (this.padData) {
|
||||||
if(this.padData.defaultViewId == data.id)
|
if(this.padData.defaultViewId == data.id)
|
||||||
this.padData.defaultViewId = null;
|
this._set(this.padData, 'defaultViewId', null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
type: (data) => {
|
type: (data) => {
|
||||||
this.types[data.id] = data;
|
this._set(this.types, data.id, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteType: (data) => {
|
deleteType: (data) => {
|
||||||
delete this.types[data.id];
|
this._delete(this.types, data.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnect: () => {
|
disconnect: () => {
|
||||||
this.disconnected = true;
|
this._set(this, 'disconnected', true);
|
||||||
this.markers = { };
|
this._set(this, 'markers', { });
|
||||||
this.lines = { };
|
this._set(this, 'lines', { });
|
||||||
this.views = { };
|
this._set(this, 'views', { });
|
||||||
this.history = { };
|
this._set(this, 'history', { });
|
||||||
},
|
},
|
||||||
|
|
||||||
connect: () => {
|
connect: () => {
|
||||||
if(this.padId)
|
if(this.padId)
|
||||||
this._setPadId(this.padId);
|
this._setPadId(this.padId);
|
||||||
else
|
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)
|
if(this.bbox)
|
||||||
this.updateBbox(this.bbox);
|
this.updateBbox(this.bbox);
|
||||||
|
@ -235,118 +245,126 @@ export default class Socket {
|
||||||
},
|
},
|
||||||
|
|
||||||
history: (data) => {
|
history: (data) => {
|
||||||
this.history[data.id] = data;
|
this._set(this.history, data.id, data);
|
||||||
// TODO: Limit to 50 entries
|
// 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)
|
if(this.padId != null)
|
||||||
throw new Error("Pad ID already set.");
|
throw new Error("Pad ID already set.");
|
||||||
|
|
||||||
return this._setPadId(padId);
|
return this._setPadId(padId);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBbox(bbox: BboxWithZoom) {
|
updateBbox(bbox: BboxWithZoom): Promise<void> {
|
||||||
this.bbox = bbox;
|
this._set(this, 'bbox', bbox);
|
||||||
return this._emit("updateBbox", bbox).then((obj) => {
|
return this._emit("updateBbox", bbox).then((obj) => {
|
||||||
this._receiveMultiple(obj);
|
this._receiveMultiple(obj);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createPad(data: PadDataCreate) {
|
createPad(data: PadDataCreate): Promise<void> {
|
||||||
return this._emit("createPad", data).then((obj) => {
|
return this._emit("createPad", data).then((obj) => {
|
||||||
this.readonly = false;
|
this._set(this, 'readonly', false);
|
||||||
this.writable = 2;
|
this._set(this, 'writable', 2);
|
||||||
|
|
||||||
this._receiveMultiple(obj);
|
this._receiveMultiple(obj);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
editPad(data: PadDataUpdate) {
|
editPad(data: PadDataUpdate): Promise<PadData> {
|
||||||
return this._emit("editPad", data);
|
return this._emit("editPad", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePad() {
|
deletePad(): Promise<void> {
|
||||||
return this._emit("deletePad");
|
return this._emit("deletePad");
|
||||||
}
|
}
|
||||||
|
|
||||||
listenToHistory() {
|
listenToHistory(): Promise<void> {
|
||||||
return this._emit("listenToHistory").then((obj) => {
|
return this._emit("listenToHistory").then((obj) => {
|
||||||
this._listeningToHistory = true;
|
this._set(this, '_listeningToHistory', true);
|
||||||
this._receiveMultiple(obj);
|
this._receiveMultiple(obj);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
stopListeningToHistory() {
|
stopListeningToHistory(): Promise<void> {
|
||||||
this._listeningToHistory = false;
|
this._set(this, '_listeningToHistory', false);
|
||||||
return this._emit("stopListeningToHistory");
|
return this._emit("stopListeningToHistory");
|
||||||
}
|
}
|
||||||
|
|
||||||
revertHistoryEntry(data: ObjectWithId) {
|
revertHistoryEntry(data: ObjectWithId): Promise<void> {
|
||||||
return this._emit("revertHistoryEntry", data).then((obj) => {
|
return this._emit("revertHistoryEntry", data).then((obj) => {
|
||||||
this.history = { };
|
this._set(this, 'history', { });
|
||||||
this._receiveMultiple(obj);
|
this._receiveMultiple(obj);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMarker(data: ObjectWithId) {
|
async getMarker(data: ObjectWithId): Promise<Marker> {
|
||||||
let marker = await this._emit("getMarker", data);
|
const marker = await this._emit("getMarker", data);
|
||||||
this.markers[marker.id] = marker;
|
this._set(this.markers, marker.id, marker);
|
||||||
return marker;
|
return marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
addMarker(data: MarkerCreate) {
|
addMarker(data: MarkerCreate): Promise<Marker> {
|
||||||
return this._emit("addMarker", data);
|
return this._emit("addMarker", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
editMarker(data: ObjectWithId & MarkerUpdate) {
|
editMarker(data: ObjectWithId & MarkerUpdate): Promise<Marker> {
|
||||||
return this._emit("editMarker", data);
|
return this._emit("editMarker", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteMarker(data: ObjectWithId) {
|
deleteMarker(data: ObjectWithId): Promise<Marker> {
|
||||||
return this._emit("deleteMarker", data);
|
return this._emit("deleteMarker", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLineTemplate(data: LineTemplateRequest) {
|
getLineTemplate(data: LineTemplateRequest): Promise<Line> {
|
||||||
return this._emit("getLineTemplate", data);
|
return this._emit("getLineTemplate", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
addLine(data: LineCreate) {
|
addLine(data: LineCreate): Promise<Line> {
|
||||||
return this._emit("addLine", data);
|
return this._emit("addLine", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
editLine(data: ObjectWithId & LineUpdate) {
|
editLine(data: ObjectWithId & LineUpdate): Promise<Line> {
|
||||||
return this._emit("editLine", data);
|
return this._emit("editLine", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteLine(data: ObjectWithId) {
|
deleteLine(data: ObjectWithId): Promise<Line> {
|
||||||
return this._emit("deleteLine", data);
|
return this._emit("deleteLine", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
exportLine(data: LineExportRequest) {
|
exportLine(data: LineExportRequest): Promise<string> {
|
||||||
return this._emit("exportLine", data);
|
return this._emit("exportLine", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
find(data: FindQuery) {
|
find(data: FindQuery): Promise<string | SearchResult[]> {
|
||||||
return this._emit("find", data);
|
return this._emit("find", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
findOnMap(data: FindOnMapQuery) {
|
findOnMap(data: FindOnMapQuery): Promise<ResponseData<'findOnMap'>> {
|
||||||
return this._emit("findOnMap", data);
|
return this._emit("findOnMap", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoute(data: RouteRequest) {
|
getRoute(data: RouteRequest): Promise<RouteInfo> {
|
||||||
return this._emit("getRoute", data);
|
return this._emit("getRoute", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
setRoute(data: RouteCreate) {
|
setRoute(data: RouteCreate): Promise<RouteWithTrackPoints | undefined> {
|
||||||
return this._emit("setRoute", data).then((route) => {
|
return this._emit("setRoute", data).then((route) => {
|
||||||
if(route) { // If unset, a newer submitted route has returned in the meantime
|
if(route) { // If unset, a newer submitted route has returned in the meantime
|
||||||
this.route = {
|
this._set(this, 'route', {
|
||||||
...route,
|
...route,
|
||||||
trackPoints: this._mergeTrackPoints({}, route.trackPoints)
|
trackPoints: this._mergeTrackPoints({}, route.trackPoints)
|
||||||
};
|
});
|
||||||
|
|
||||||
this._simulateEvent("route", this.route);
|
this._simulateEvent("route", this.route);
|
||||||
}
|
}
|
||||||
|
@ -355,18 +373,18 @@ export default class Socket {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clearRoute() {
|
clearRoute(): Promise<void> {
|
||||||
this.route = undefined;
|
this._set(this, 'route', undefined);
|
||||||
this._simulateEvent("route", undefined);
|
this._simulateEvent("route", undefined);
|
||||||
return this._emit("clearRoute");
|
return this._emit("clearRoute");
|
||||||
}
|
}
|
||||||
|
|
||||||
lineToRoute(data: ObjectWithId) {
|
lineToRoute(data: ObjectWithId): Promise<RouteWithTrackPoints | undefined> {
|
||||||
return this._emit("lineToRoute", data).then((route) => {
|
return this._emit("lineToRoute", data).then((route) => {
|
||||||
this.route = {
|
this._set(this, 'route', {
|
||||||
...route,
|
...route,
|
||||||
trackPoints: this._mergeTrackPoints({}, route.trackPoints)
|
trackPoints: this._mergeTrackPoints({}, route.trackPoints)
|
||||||
};
|
});
|
||||||
|
|
||||||
this._simulateEvent("route", this.route);
|
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);
|
return this._emit("exportRoute", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
addType(data: TypeCreate) {
|
addType(data: TypeCreate): Promise<Type> {
|
||||||
return this._emit("addType", data);
|
return this._emit("addType", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
editType(data: ObjectWithId & TypeUpdate) {
|
editType(data: ObjectWithId & TypeUpdate): Promise<Type> {
|
||||||
return this._emit("editType", data);
|
return this._emit("editType", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteType(data: ObjectWithId) {
|
deleteType(data: ObjectWithId): Promise<Type> {
|
||||||
return this._emit("deleteType", data);
|
return this._emit("deleteType", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
addView(data: ViewCreate) {
|
addView(data: ViewCreate): Promise<View> {
|
||||||
return this._emit("addView", data);
|
return this._emit("addView", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
editView(data: ObjectWithId & ViewUpdate) {
|
editView(data: ObjectWithId & ViewUpdate): Promise<View> {
|
||||||
return this._emit("editView", data);
|
return this._emit("editView", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteView(data: ObjectWithId) {
|
deleteView(data: ObjectWithId): Promise<View> {
|
||||||
return this._emit("deleteView", data);
|
return this._emit("deleteView", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
geoip() {
|
geoip(): Promise<Bbox | null> {
|
||||||
return this._emit("geoip");
|
return this._emit("geoip");
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect(): void {
|
||||||
this.socket.offAny();
|
this.socket.offAny();
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
_setPadId(padId: string) {
|
_setPadId(padId: string): Promise<void> {
|
||||||
this.padId = padId;
|
this._set(this, 'padId', padId);
|
||||||
return this._emit("setPadId", padId).then((obj) => {
|
return this._emit("setPadId", padId).then((obj) => {
|
||||||
this.disconnected = false;
|
this._set(this, 'disconnected', false);
|
||||||
|
|
||||||
this._receiveMultiple(obj);
|
this._receiveMultiple(obj);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this.serverError = err;
|
this._set(this, 'serverError', err);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_receiveMultiple(obj?: MultipleEvents<SocketEvents>) {
|
_receiveMultiple(obj?: MultipleEvents<ClientEvents>): void {
|
||||||
if (obj) {
|
if (obj) {
|
||||||
for(const i of Object.keys(obj) as EventName<SocketEvents>[])
|
for(const i of Object.keys(obj) as EventName<ClientEvents>[])
|
||||||
(obj[i] as Array<SocketEvents[typeof i][0]>).forEach((it) => { this._simulateEvent(i, it as any); });
|
(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]) {
|
_simulateEvent<E extends EventName<ClientEvents>>(eventName: E, ...data: ClientEvents[E]): void {
|
||||||
const listeners = this._listeners[eventName] as Array<EventHandler<SocketEvents, E>> | undefined;
|
const listeners = this._listeners[eventName] as Array<EventHandler<ClientEvents, E>> | undefined;
|
||||||
if(listeners) {
|
if(listeners) {
|
||||||
listeners.forEach(function(listener: EventHandler<SocketEvents, E>) {
|
listeners.forEach(function(listener: EventHandler<ClientEvents, E>) {
|
||||||
listener(...data);
|
listener(...data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_mergeTrackPoints(existingTrackPoints: Record<number, TrackPoint> | null, newTrackPoints: TrackPoint[]) {
|
_mergeTrackPoints(existingTrackPoints: Record<number, TrackPoint> | null, newTrackPoints: TrackPoint[]): TrackPoints {
|
||||||
let ret = { ...(existingTrackPoints || { }) } as TrackPoints;
|
const ret = { ...(existingTrackPoints || { }) } as TrackPoints;
|
||||||
|
|
||||||
for(let i=0; i<newTrackPoints.length; i++) {
|
for(let i=0; i<newTrackPoints.length; i++) {
|
||||||
ret[newTrackPoints[i].idx] = newTrackPoints[i];
|
ret[newTrackPoints[i].idx] = newTrackPoints[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.length = 0;
|
ret.length = 0;
|
||||||
for(let i in ret) {
|
for(const i in ret) {
|
||||||
if(i != "length")
|
if(i != "length")
|
||||||
ret.length = Math.max(ret.length, parseInt(i) + 1);
|
ret.length = Math.max(ret.length, parseInt(i) + 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl
|
||||||
module.exports = (env, argv) => {
|
module.exports = (env, argv) => {
|
||||||
const isDev = argv.mode == "development";
|
const isDev = argv.mode == "development";
|
||||||
|
|
||||||
const base = {
|
return {
|
||||||
entry: `${__dirname}/src/client.ts`,
|
entry: `${__dirname}/src/client.ts`,
|
||||||
output: {
|
output: {
|
||||||
filename: "client.js",
|
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%;
|
width: 100%;
|
||||||
height: 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);
|
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) {
|
fm.app.directive("fmShape", function(fmUtils) {
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
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) => {
|
fm.app.directive("fmScrollToView", (fmUtils) => {
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
};
|
|
@ -30,6 +30,7 @@
|
||||||
"bootstrap-touchspin": "^4.3.0",
|
"bootstrap-touchspin": "^4.3.0",
|
||||||
"bootstrap-vue": "^2.21.1",
|
"bootstrap-vue": "^2.21.1",
|
||||||
"clipboard": "^2.0.6",
|
"clipboard": "^2.0.6",
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
"domutils": "^2.4.4",
|
"domutils": "^2.4.4",
|
||||||
"facilmap-client": "^2.7.0",
|
"facilmap-client": "^2.7.0",
|
||||||
"facilmap-leaflet": "^2.7.0",
|
"facilmap-leaflet": "^2.7.0",
|
||||||
|
@ -44,15 +45,20 @@
|
||||||
"leaflet.heightgraph": "^1.4.0",
|
"leaflet.heightgraph": "^1.4.0",
|
||||||
"leaflet.locatecontrol": "^0.72.0",
|
"leaflet.locatecontrol": "^0.72.0",
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"markdown": "^0.5.0",
|
"markdown": "^0.5.0",
|
||||||
"osmtogeojson": "^3.0.0-beta.4",
|
"osmtogeojson": "^3.0.0-beta.4",
|
||||||
|
"portal-vue": "^2.1.7",
|
||||||
"tablesorter": "^2.31.3",
|
"tablesorter": "^2.31.3",
|
||||||
|
"vee-validate": "^3.4.5",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-class-component": "^7.2.6",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/copy-webpack-plugin": "^6.4.0",
|
"@types/copy-webpack-plugin": "^6.4.0",
|
||||||
|
"@types/jest": "^26.0.20",
|
||||||
"@types/jquery": "^3.5.5",
|
"@types/jquery": "^3.5.5",
|
||||||
"@types/leaflet": "^1.5.19",
|
"@types/leaflet": "^1.5.19",
|
||||||
"@types/leaflet-mouse-position": "^1.2.0",
|
"@types/leaflet-mouse-position": "^1.2.0",
|
||||||
|
@ -65,10 +71,12 @@
|
||||||
"facilmap-types": "^2.7.0",
|
"facilmap-types": "^2.7.0",
|
||||||
"html-loader": "x",
|
"html-loader": "x",
|
||||||
"html-webpack-plugin": "x",
|
"html-webpack-plugin": "x",
|
||||||
|
"jest": "^26.6.3",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.32.8",
|
||||||
"sass-loader": "^11.0.1",
|
"sass-loader": "^11.0.1",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
"svgo": "^1.1.1",
|
"svgo": "^1.1.1",
|
||||||
|
"ts-jest": "^26.5.3",
|
||||||
"ts-loader": "^8.0.17",
|
"ts-loader": "^8.0.17",
|
||||||
"ts-node": "^9.1.1",
|
"ts-node": "^9.1.1",
|
||||||
"typescript": "^4.1.3",
|
"typescript": "^4.1.3",
|
||||||
|
|
|
@ -12,7 +12,7 @@ import "./about.scss";
|
||||||
})
|
})
|
||||||
export default class About extends Vue {
|
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)];
|
layers = [...Object.values(baseLayers), ...Object.values(overlays)];
|
||||||
fmVersion = packageJson.version;
|
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 Vue from "vue";
|
||||||
import Client from "facilmap-client";
|
import Client from "facilmap-client";
|
||||||
import "./client.scss";
|
import "./client.scss";
|
||||||
import WithRender from "./client.vue";
|
import WithRender from "./client.vue";
|
||||||
|
import { PadId } from "facilmap-types";
|
||||||
|
import { VueDecorator } from "vue-class-component";
|
||||||
|
|
||||||
const CLIENT_KEY = "fm-client";
|
const CLIENT_KEY = "fm-client";
|
||||||
|
|
||||||
export function InjectClient() {
|
export function InjectClient(): VueDecorator {
|
||||||
return InjectReactive(CLIENT_KEY);
|
return InjectReactive(CLIENT_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,4 +29,18 @@ export class ClientProvider extends Vue {
|
||||||
this.client = client;
|
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() {
|
export function updatePadId(padId: string): void {
|
||||||
var map = angular.element($("facilmap", $element)).controller("facilmap");
|
context.activePadId = padId;
|
||||||
|
|
||||||
|
if (padId)
|
||||||
|
history.replaceState(null, "", context.urlPrefix + padId + location.search + location.hash);
|
||||||
|
}
|
||||||
|
|
||||||
$scope.$watch(() => (map.client.padData && map.client.padData.name), function(newVal) {
|
export function updatePadName(padName: string): void {
|
||||||
$scope.padName = newVal;
|
const title = padName ? padName + ' – FacilMap' : 'FacilMap';
|
||||||
});
|
|
||||||
|
|
||||||
$scope.$watch(() => (map.client.padId), function(padId) {
|
// We have to call history.replaceState() in order for the new title to end up in the browser history
|
||||||
if(padId)
|
window.history && history.replaceState({ }, title);
|
||||||
history.replaceState(null, "", fm.URL_PREFIX + padId + location.search + location.hash);
|
document.title = title;
|
||||||
});
|
}
|
||||||
}, 0); */
|
|
|
@ -2,10 +2,10 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title fm-title="padName ? padName + ' – FacilMap' : 'FacilMap'">FacilMap</title>
|
<title>FacilMap</title>
|
||||||
<% if(!padData || padData.searchEngines) { %>
|
<% if(!padData || padData.searchEngines) { %>
|
||||||
<meta name="robots" content="index,nofollow" />
|
<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 { %>
|
<% } else { %>
|
||||||
<meta name="robots" content="noindex,nofollow" />
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import BootstrapVue from "bootstrap-vue";
|
import { BootstrapVue } from "bootstrap-vue";
|
||||||
import { registerDeobfuscationHandlers } from "../utils/ui";
|
import { registerDeobfuscationHandlers } from "../utils/ui";
|
||||||
import Main from './main/main';
|
import Main from './main/main';
|
||||||
import { ClientProvider } from './client/client';
|
import { ClientProvider } from './client/client';
|
||||||
import context from './context';
|
import context, { updatePadId, updatePadName } from './context';
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import "bootstrap-vue/dist/bootstrap-vue.css";
|
import "bootstrap-vue/dist/bootstrap-vue.css";
|
||||||
import withRender from "./map.vue";
|
import withRender from "./map.vue";
|
||||||
import Vue2TouchEvents from 'vue2-touch-events'
|
import Vue2TouchEvents from "vue2-touch-events";
|
||||||
import PortalVue from 'portal-vue'
|
import PortalVue from "portal-vue";
|
||||||
|
import "../utils/validation";
|
||||||
|
import { PadId } from 'facilmap-types';
|
||||||
|
|
||||||
Vue.use(BootstrapVue);
|
Vue.use(BootstrapVue);
|
||||||
Vue.use(Vue2TouchEvents);
|
Vue.use(Vue2TouchEvents);
|
||||||
|
@ -68,5 +70,14 @@ new Vue(withRender({
|
||||||
serverUrl: "/",
|
serverUrl: "/",
|
||||||
padId: context.activePadId
|
padId: context.activePadId
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
handlePadIdChange(padId: PadId) {
|
||||||
|
updatePadId(padId);
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePadNameChange(padName: string) {
|
||||||
|
updatePadName(padName);
|
||||||
|
}
|
||||||
|
},
|
||||||
components: { ClientProvider, Main }
|
components: { ClientProvider, Main }
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<ClientProvider :serverUrl='serverUrl' :padId='padId'>
|
<ClientProvider :serverUrl="serverUrl" :padId="padId" @padId="handlePadIdChange" @padName="handlePadNameChange">
|
||||||
<Main/>
|
<Main/>
|
||||||
</ClientProvider>
|
</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 {
|
export default class SearchBox extends Vue {
|
||||||
|
|
||||||
tab: number = 0;
|
tab = 0;
|
||||||
touchStartY: number | null = null;
|
touchStartY: number | null = null;
|
||||||
|
|
||||||
get isNarrow() {
|
get isNarrow(): boolean {
|
||||||
return context.isNarrow;
|
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) {
|
if(context.isNarrow && event.touches && event.touches[0] && $(event.target as EventTarget).closest("[draggable=true]").length == 0) {
|
||||||
const top = (this.$el as HTMLElement).offsetTop;
|
const top = (this.$el as HTMLElement).offsetTop;
|
||||||
this.touchStartY = event.touches[0].clientY - top;
|
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]) {
|
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 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;
|
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]) {
|
if(this.touchStartY != null && event.changedTouches[0]) {
|
||||||
this.touchStartY = null;
|
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">
|
<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-card no-body>
|
||||||
<b-tabs card align="center">
|
<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">
|
<b-tab title="Test 1">
|
||||||
<p>Test 1</p>
|
<p>Test 1</p>
|
||||||
</b-tab>
|
</b-tab>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Component from "vue-class-component";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import WithRender from "./toolbox.vue";
|
import WithRender from "./toolbox.vue";
|
||||||
import "./toolbox.scss";
|
import "./toolbox.scss";
|
||||||
import { Prop, Ref } from "vue-property-decorator";
|
import { Prop } from "vue-property-decorator";
|
||||||
import Client from "facilmap-client";
|
import Client from "facilmap-client";
|
||||||
import { InjectClient } from "../client/client";
|
import { InjectClient } from "../client/client";
|
||||||
import { InjectMapComponents, InjectMapContext, MapComponents, MapContext } from "../leaflet-map/leaflet-map";
|
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 Sidebar from "../ui/sidebar/sidebar";
|
||||||
import Icon from "../ui/icon/icon";
|
import Icon from "../ui/icon/icon";
|
||||||
import context from "../context";
|
import context from "../context";
|
||||||
|
import PadSettings from "../pad/pad-settings";
|
||||||
|
|
||||||
@WithRender
|
@WithRender
|
||||||
@Component({
|
@Component({
|
||||||
components: { About, Icon, Sidebar }
|
components: { About, Icon, PadSettings, Sidebar }
|
||||||
})
|
})
|
||||||
export default class Toolbox extends Vue {
|
export default class Toolbox extends Vue {
|
||||||
|
|
||||||
|
@ -26,11 +27,11 @@ export default class Toolbox extends Vue {
|
||||||
|
|
||||||
hasImportUi = true; // TODO
|
hasImportUi = true; // TODO
|
||||||
|
|
||||||
get isNarrow() {
|
get isNarrow(): boolean {
|
||||||
return context.isNarrow;
|
return context.isNarrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
get links() {
|
get links(): Record<'osm' | 'google' | 'bing' | 'facilmap', string> {
|
||||||
const v = this.mapContext;
|
const v = this.mapContext;
|
||||||
return {
|
return {
|
||||||
osm: `https://www.openstreetmap.org/#map=${v.zoom}/${v.center.lat}/${v.center.lng}`,
|
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;
|
const v = this.mapContext;
|
||||||
if (v.filter) {
|
if (v.filter) {
|
||||||
return {
|
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) => ({
|
return Object.keys(baseLayers).map((key) => ({
|
||||||
key,
|
key,
|
||||||
name: baseLayers[key].options.fmName,
|
name: baseLayers[key].options.fmName!,
|
||||||
active: this.mapContext.layers.baseLayer === key
|
active: this.mapContext.layers.baseLayer === key
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
get overlays() {
|
get overlays(): Array<{ key: string; name: string; active: boolean }> {
|
||||||
return Object.keys(overlays).map((key) => ({
|
return Object.keys(overlays).map((key) => ({
|
||||||
key,
|
key,
|
||||||
name: overlays[key].options.fmName,
|
name: overlays[key].options.fmName!,
|
||||||
active: this.mapContext.layers.overlays.includes(key)
|
active: this.mapContext.layers.overlays.includes(key)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -75,15 +76,15 @@ export default class Toolbox extends Vue {
|
||||||
map.linesUi.addLine(type);
|
map.linesUi.addLine(type);
|
||||||
} */
|
} */
|
||||||
|
|
||||||
displayView(view: View) {
|
displayView(view: View): void {
|
||||||
displayView(this.mapComponents.map, view);
|
displayView(this.mapComponents.map, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBaseLayer(key: string) {
|
setBaseLayer(key: string): void {
|
||||||
setBaseLayer(this.mapComponents.map, key);
|
setBaseLayer(this.mapComponents.map, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOverlay(key: string) {
|
toggleOverlay(key: string): void {
|
||||||
toggleOverlay(this.mapComponents.map, key);
|
toggleOverlay(this.mapComponents.map, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,10 +96,6 @@ export default class Toolbox extends Vue {
|
||||||
manageViews();
|
manageViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
editPadSettings() {
|
|
||||||
map.padUi.editPadSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
editObjectTypes() {
|
editObjectTypes() {
|
||||||
map.typesUi.editTypes();
|
map.typesUi.editTypes();
|
||||||
}
|
}
|
||||||
|
@ -115,10 +112,6 @@ export default class Toolbox extends Vue {
|
||||||
fmAbout.showAbout(map);
|
fmAbout.showAbout(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
startPad() {
|
|
||||||
map.padUi.createPad();
|
|
||||||
}
|
|
||||||
|
|
||||||
filter() {
|
filter() {
|
||||||
fmFilter.showFilterDialog(map.client.filterExpr, map.client.types).then(function(newFilter) {
|
fmFilter.showFilterDialog(map.client.filterExpr, map.client.types).then(function(newFilter) {
|
||||||
map.client.setFilter(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>
|
<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">
|
<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 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.padId" text="Add" :disabled="!!client.interaction" right>
|
<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-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-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-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>
|
||||||
<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-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-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>
|
<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-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="!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="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.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.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.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.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.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.padId" :href="`${client.padData.id}/table${filterQuery.q}`" target="_blank">Export as table</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.padId"></b-dropdown-divider>
|
<b-dropdown-divider v-if="client.padData"></b-dropdown-divider>
|
||||||
<b-dropdown-item v-if="client.padId" href="javascript:" @click="filter()">Filter</b-dropdown-item>
|
<b-dropdown-item v-if="client.padData" 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.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.padId" href="javascript:" @click="showHistory()">Show edit history</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.padId"></b-dropdown-divider>
|
<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-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>
|
</b-nav-item-dropdown>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
||||||
<About id="fm-toolbox-about"></About>
|
<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>
|
</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 {
|
export default class Icon extends Vue {
|
||||||
|
|
||||||
@Prop({ type: String }) icon!: string;
|
@Prop({ type: String, required: true }) icon!: string;
|
||||||
|
|
||||||
get iconCode() {
|
get iconCode() {
|
||||||
return createSymbolHtml("currentColor", "1.5em", this.icon);
|
return createSymbolHtml("currentColor", "1.5em", this.icon);
|
||||||
|
|
|
@ -10,7 +10,7 @@ import $ from "jquery";
|
||||||
})
|
})
|
||||||
export default class Sidebar extends Vue {
|
export default class Sidebar extends Vue {
|
||||||
|
|
||||||
@Prop({ type: String }) readonly id!: string;
|
@Prop({ type: String, required: true }) readonly id!: string;
|
||||||
|
|
||||||
touchStartX: number | null = null;
|
touchStartX: number | null = null;
|
||||||
sidebarVisible = false;
|
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 $ from 'jquery';
|
||||||
import './ui.scss';
|
import './ui.scss';
|
||||||
import { deobfuscate } from "facilmap-utils";
|
import { deobfuscate } from "facilmap-utils";
|
||||||
|
import { Colour } from "facilmap-types";
|
||||||
|
import { RAINBOW_STOPS } from "facilmap-leaflet";
|
||||||
|
|
||||||
/*
|
export function createLineGraphic(colour: Colour, width: number, length: number): string {
|
||||||
|
|
||||||
fmUtils.createLineGraphic = function(colour, width, length) {
|
|
||||||
return "data:image/svg+xml,"+encodeURIComponent(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>` +
|
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">` +
|
`<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}`}"/>` +
|
`<rect x="0" y="0" width="${length}" height="${width}" style="fill:${colour == null ? `url(#rainbow)` : `#${colour}`}"/>` +
|
||||||
`</svg>`);
|
`</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 {
|
export function registerDeobfuscationHandlers(): void {
|
||||||
const clickHandler = (e: JQuery.ClickEvent) => {
|
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> {
|
async function cleanIcon(icon: string): Promise<string> {
|
||||||
const optimized = await new svgo().optimize(icon);
|
const optimized = await new svgo().optimize(icon);
|
||||||
|
|
||||||
var $ = cheerio.load(optimized.data, {
|
const $ = cheerio.load(optimized.data, {
|
||||||
xmlMode: true
|
xmlMode: true
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const el of $("*").toArray()) {
|
for (const el of $("*").toArray() as cheerio.TagElement[]) {
|
||||||
el.name = el.name.replace(/^svg:/, "");
|
el.name = el.name.replace(/^svg:/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
$("metadata,sodipodi\\:namedview,defs,image").remove();
|
$("metadata,sodipodi\\:namedview,defs,image").remove();
|
||||||
|
|
||||||
for (const el of $("*").toArray()) {
|
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 && fill != "none") {
|
||||||
if(fill != "#ffffff" && fill != "#fff") { // This is the background
|
if(fill != "#ffffff" && fill != "#fff") { // This is the background
|
||||||
$el.remove();
|
$el.remove();
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
|
"build-module": "webpack --config-name module",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"dev-server": "webpack serve --mode development --config-name full",
|
"dev-server": "webpack serve --mode development --config-name full",
|
||||||
"download-icons": "ts-node ./download-icons.ts",
|
"download-icons": "ts-node ./download-icons.ts",
|
||||||
|
@ -36,7 +37,7 @@
|
||||||
"filtrex": "^2.1.0",
|
"filtrex": "^2.1.0",
|
||||||
"leaflet-auto-graticule": "^1.0.9",
|
"leaflet-auto-graticule": "^1.0.9",
|
||||||
"leaflet-draggable-lines": "^1.0.6",
|
"leaflet-draggable-lines": "^1.0.6",
|
||||||
"leaflet-freie-tonne": "^1.0.4",
|
"leaflet-freie-tonne": "^1.0.5",
|
||||||
"leaflet-geometryutil": "^0.9.3",
|
"leaflet-geometryutil": "^0.9.3",
|
||||||
"leaflet-hash": "^0.2.1",
|
"leaflet-hash": "^0.2.1",
|
||||||
"leaflet-highlightable-layers": "^1.0.8",
|
"leaflet-highlightable-layers": "^1.0.8",
|
||||||
|
@ -57,6 +58,7 @@
|
||||||
"@types/webpack-env": "^1.16.0",
|
"@types/webpack-env": "^1.16.0",
|
||||||
"@types/webpack-node-externals": "^2.5.0",
|
"@types/webpack-node-externals": "^2.5.0",
|
||||||
"@types/yauzl": "^2.9.1",
|
"@types/yauzl": "^2.9.1",
|
||||||
|
"cheerio": "^1.0.0-rc.5",
|
||||||
"css-loader": "^5.1.0",
|
"css-loader": "^5.1.0",
|
||||||
"highland": "^2.13.5",
|
"highland": "^2.13.5",
|
||||||
"jest": "^26.6.3",
|
"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 { EventHandler } from "facilmap-types";
|
||||||
import { Handler, Map } from "leaflet";
|
import { Handler, Map } from "leaflet";
|
||||||
import { leafletToFmBbox } from "./utils/leaflet";
|
import { leafletToFmBbox } from "./utils/leaflet";
|
||||||
|
|
||||||
export default class BboxHandler extends Handler {
|
export default class BboxHandler extends Handler {
|
||||||
|
|
||||||
client: Socket;
|
client: Client;
|
||||||
|
|
||||||
constructor(map: Map, client: Socket) {
|
constructor(map: Map, client: Client) {
|
||||||
super(map);
|
super(map);
|
||||||
this.client = client;
|
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)) {
|
if (["setPadId", "setRoute"].includes(name)) {
|
||||||
this.updateBbox();
|
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 { ID, Line, LinePointsEvent, ObjectWithId } from "facilmap-types";
|
||||||
import { FeatureGroup, LayerOptions, Map, PolylineOptions } from "leaflet";
|
import { FeatureGroup, LayerOptions, Map, PolylineOptions } from "leaflet";
|
||||||
import { HighlightableLayerOptions, HighlightablePolyline } from "leaflet-highlightable-layers";
|
import { HighlightableLayerOptions, HighlightablePolyline } from "leaflet-highlightable-layers";
|
||||||
|
@ -11,11 +11,11 @@ interface LinesLayerOptions extends LayerOptions {
|
||||||
export default class LinesLayer extends FeatureGroup {
|
export default class LinesLayer extends FeatureGroup {
|
||||||
|
|
||||||
options!: LayerOptions;
|
options!: LayerOptions;
|
||||||
client: Socket;
|
client: Client;
|
||||||
linesById: Record<string, InstanceType<typeof HighlightablePolyline>> = {};
|
linesById: Record<string, InstanceType<typeof HighlightablePolyline>> = {};
|
||||||
highlightedLinesIds = new Set<ID>();
|
highlightedLinesIds = new Set<ID>();
|
||||||
|
|
||||||
constructor(client: Socket, options?: LinesLayerOptions) {
|
constructor(client: Client, options?: LinesLayerOptions) {
|
||||||
super([], options);
|
super([], options);
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ export default class LinesLayer extends FeatureGroup {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFilter = (): void => {
|
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]);
|
const show = this._map.fmFilterFunc(this.client.lines[i]);
|
||||||
if(this.linesById[i] && !show)
|
if(this.linesById[i] && !show)
|
||||||
this._deleteLine(this.client.lines[i]);
|
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 { Map, PathOptions, PolylineOptions } from "leaflet";
|
||||||
import { HighlightableLayerOptions, HighlightablePolyline } from "leaflet-highlightable-layers";
|
import { HighlightableLayerOptions, HighlightablePolyline } from "leaflet-highlightable-layers";
|
||||||
import { trackPointsToLatLngArray } from "../utils/leaflet";
|
import { trackPointsToLatLngArray } from "../utils/leaflet";
|
||||||
|
@ -10,10 +10,10 @@ interface RouteLayerOptions extends PolylineOptions {
|
||||||
export default class RouteLayer extends HighlightablePolyline {
|
export default class RouteLayer extends HighlightablePolyline {
|
||||||
|
|
||||||
realOptions!: RouteLayerOptions;
|
realOptions!: RouteLayerOptions;
|
||||||
client: Socket;
|
client: Client;
|
||||||
draggable?: DraggableLines;
|
draggable?: DraggableLines;
|
||||||
|
|
||||||
constructor(client: Socket, options?: RouteLayerOptions) {
|
constructor(client: Client, options?: RouteLayerOptions) {
|
||||||
super([], options);
|
super([], options);
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Map, MarkerClusterGroup, MarkerClusterGroupOptions } from "leaflet";
|
import { Map, MarkerClusterGroup, MarkerClusterGroupOptions } from "leaflet";
|
||||||
import Socket from "facilmap-client";
|
import Client from "facilmap-client";
|
||||||
import { PadData } from "facilmap-types";
|
import { PadData } from "facilmap-types";
|
||||||
import "leaflet.markercluster";
|
import "leaflet.markercluster";
|
||||||
import "leaflet.markercluster/dist/MarkerCluster.css";
|
import "leaflet.markercluster/dist/MarkerCluster.css";
|
||||||
|
@ -10,10 +10,10 @@ export interface MarkerClusterOptions extends MarkerClusterGroupOptions {
|
||||||
|
|
||||||
export default class MarkerCluster extends MarkerClusterGroup {
|
export default class MarkerCluster extends MarkerClusterGroup {
|
||||||
|
|
||||||
client: Socket;
|
client: Client;
|
||||||
_maxClusterRadiusBkp: MarkerClusterOptions['maxClusterRadius'];
|
_maxClusterRadiusBkp: MarkerClusterOptions['maxClusterRadius'];
|
||||||
|
|
||||||
constructor(client: Socket, options?: MarkerClusterOptions) {
|
constructor(client: Client, options?: MarkerClusterOptions) {
|
||||||
super({
|
super({
|
||||||
showCoverageOnHover: false,
|
showCoverageOnHover: false,
|
||||||
maxClusterRadius: 50,
|
maxClusterRadius: 50,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Socket from 'facilmap-client';
|
import Client from 'facilmap-client';
|
||||||
import { ID, Marker, ObjectWithId } from 'facilmap-types';
|
import { ID, Marker, ObjectWithId } from 'facilmap-types';
|
||||||
import { Map } from 'leaflet';
|
import { Map } from 'leaflet';
|
||||||
import { tooltipOptions } from '../utils/leaflet';
|
import { tooltipOptions } from '../utils/leaflet';
|
||||||
|
@ -12,11 +12,11 @@ export interface MarkersLayerOptions extends MarkerClusterOptions {
|
||||||
export default class MarkersLayer extends MarkerCluster {
|
export default class MarkersLayer extends MarkerCluster {
|
||||||
|
|
||||||
options!: MarkersLayerOptions;
|
options!: MarkersLayerOptions;
|
||||||
client: Socket;
|
client: Client;
|
||||||
markersById: Record<string, MarkerLayer> = {};
|
markersById: Record<string, MarkerLayer> = {};
|
||||||
highlightedMarkerIds = new Set<ID>();
|
highlightedMarkerIds = new Set<ID>();
|
||||||
|
|
||||||
constructor(client: Socket, options?: MarkersLayerOptions) {
|
constructor(client: Client, options?: MarkersLayerOptions) {
|
||||||
super(client, options);
|
super(client, options);
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ export default class MarkersLayer extends MarkerCluster {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFilter = (): void => {
|
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]);
|
const show = this._map.fmFilterFunc(this.client.markers[i]);
|
||||||
if(this.markersById[i] && !show)
|
if(this.markersById[i] && !show)
|
||||||
this._deleteMarker(this.client.markers[i]);
|
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 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 {
|
interface ShapeInfo {
|
||||||
svg: string;
|
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 moveX = (sizes[set] - Number(el.getAttribute("width"))) / 2;
|
||||||
const moveY = (sizes[set] - Number(el.getAttribute("height"))) / 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 {
|
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)}`;
|
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSymbolHtml(colour: string, height: number, symbol?: Symbol): string {
|
export function createSymbolHtml(colour: string, height: number | string, symbol?: Symbol): string {
|
||||||
return `<svg width="${height}" height="${height}" viewbox="0 0 ${height} ${height}">` +
|
return `<svg width="${height}" height="${height}" viewbox="0 0 25 25">` +
|
||||||
getSymbolCode(colour, height, symbol) +
|
getSymbolCode(colour, 25, symbol) +
|
||||||
`</svg>`;
|
`</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Socket from 'facilmap-client';
|
import Client from 'facilmap-client';
|
||||||
import L, { Evented, Handler, LatLng, Map } from 'leaflet';
|
import L, { Evented, Handler, LatLng, Map } from 'leaflet';
|
||||||
import 'leaflet-hash';
|
import 'leaflet-hash';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
@ -24,13 +24,13 @@ interface Query {
|
||||||
|
|
||||||
export default class HashHandler extends Handler {
|
export default class HashHandler extends Handler {
|
||||||
|
|
||||||
socket: Socket;
|
client: Client;
|
||||||
hash: any;
|
hash: any;
|
||||||
activeQuery?: Query;
|
activeQuery?: Query;
|
||||||
|
|
||||||
constructor(map: Map, socket: Socket) {
|
constructor(map: Map, client: Client) {
|
||||||
super(map);
|
super(map);
|
||||||
this.socket = socket;
|
this.client = client;
|
||||||
|
|
||||||
this.hash = Object.assign(new L.Hash(), {
|
this.hash = Object.assign(new L.Hash(), {
|
||||||
parseHash: this.parseHash,
|
parseHash: this.parseHash,
|
||||||
|
@ -77,8 +77,8 @@ export default class HashHandler extends Handler {
|
||||||
this.fireEvent("fmHash", { hash });
|
this.fireEvent("fmHash", { hash });
|
||||||
|
|
||||||
const viewMatch = hash.match(/^q=v(\d+)$/i);
|
const viewMatch = hash.match(/^q=v(\d+)$/i);
|
||||||
if(viewMatch && this.socket.views[viewMatch[1] as any]) {
|
if(viewMatch && this.client.views[viewMatch[1] as any]) {
|
||||||
displayView(this._map, this.socket.views[viewMatch[1] as any]);
|
displayView(this._map, this.client.views[viewMatch[1] as any]);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,12 +126,12 @@ export default class HashHandler extends Handler {
|
||||||
result = "#q=" + encodeURIComponent(this.activeQuery.query);
|
result = "#q=" + encodeURIComponent(this.activeQuery.query);
|
||||||
} else if(!this.activeQuery) {
|
} else if(!this.activeQuery) {
|
||||||
// Check if we have a saved view open
|
// 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))
|
if(isAtView(this._map, defaultView || undefined))
|
||||||
result = "#";
|
result = "#";
|
||||||
else {
|
else {
|
||||||
for(const viewId of Object.keys(this.socket.views)) {
|
for(const viewId of Object.keys(this.client.views)) {
|
||||||
if(isAtView(this._map, this.socket.views[viewId as any])) {
|
if(isAtView(this._map, this.client.views[viewId as any])) {
|
||||||
result = `#q=v${encodeURIComponent(viewId)}`;
|
result = `#q=v${encodeURIComponent(viewId)}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import Socket from "facilmap-client";
|
import Client from "facilmap-client";
|
||||||
import { PadData } from "facilmap-types";
|
import { PadData } from "facilmap-types";
|
||||||
import { UnsavedView } from "./views";
|
import { UnsavedView } from "./views";
|
||||||
|
|
||||||
export async function getInitialView(socket: Socket): Promise<UnsavedView | undefined> {
|
export async function getInitialView(client: Client): Promise<UnsavedView | undefined> {
|
||||||
if(socket.padId) {
|
if(client.padId) {
|
||||||
const padData = socket.padData || await new Promise<PadData>((resolve) => {
|
const padData = client.padData || await new Promise<PadData>((resolve) => {
|
||||||
socket.on("padData", resolve);
|
client.on("padData", resolve);
|
||||||
});
|
});
|
||||||
|
|
||||||
return padData.defaultView;
|
return padData.defaultView;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const geoip = await socket.geoip();
|
const geoip = await client.geoip();
|
||||||
|
|
||||||
if (geoip) {
|
if (geoip) {
|
||||||
return { ...geoip, baseLayer: undefined as any, layers: [] };
|
return { ...geoip, baseLayer: undefined as any, layers: [] };
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"start": "npm run deps && npm run server",
|
"start": "npm run deps && npm run server",
|
||||||
"deps": "npm install",
|
"deps": "npm install",
|
||||||
"server": "ts-node --transpile-only src/server.ts",
|
"server": "ts-node --transpile-only src/server.ts",
|
||||||
|
"dev-server": "FM_DEV=true ts-node --transpile-only src/server.ts",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"types": "tsc --noEmit src/**/*.ts",
|
"types": "tsc --noEmit src/**/*.ts",
|
||||||
"lint": "eslint src/**/*.ts"
|
"lint": "eslint src/**/*.ts"
|
||||||
|
|
|
@ -82,7 +82,7 @@ class SocketConnection {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err.stack);
|
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"),
|
getFrontendFile("map.ejs"),
|
||||||
(async () => {
|
(async () => {
|
||||||
if(req.params && req.params.padId) {
|
if(req.params && req.params.padId) {
|
||||||
return database.pads.getPadData(req.params.padId).then((padData) => {
|
return database.pads.getPadDataByAnyId(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.
|
|
||||||
|
|
||||||
if (!padData)
|
if (!padData)
|
||||||
throw new Error();
|
throw new Error();
|
||||||
return padData;
|
return padData;
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
};
|
|
@ -32,6 +32,9 @@
|
||||||
"marked": "^2.0.1"
|
"marked": "^2.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jest": "^26.0.20",
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"ts-jest": "^26.5.3",
|
||||||
"typescript": "^4.2.2"
|
"typescript": "^4.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { isEqual } from "lodash";
|
||||||
|
|
||||||
const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
const LENGTH = 12;
|
const LENGTH = 12;
|
||||||
|
|
||||||
|
@ -99,21 +101,6 @@ export function encodeQueryString(obj: Record<string, string>): string {
|
||||||
return pairs.join("&");
|
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 {
|
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