clean up
This commit is contained in:
parent
b359adaf56
commit
73220f1a20
@ -1,80 +0,0 @@
|
||||
/**
|
||||
* @author Martin Karkowski
|
||||
* @email m.karkowski@zema.de
|
||||
* @create date 2020-03-03 16:29:04
|
||||
* @modify date 2020-03-03 16:33:00
|
||||
* @desc [description]
|
||||
*/
|
||||
|
||||
export interface connectplcInput {
|
||||
plcs: IPLCsConfig;
|
||||
}
|
||||
|
||||
export interface useonlineconfigurationInput {
|
||||
// Name of the PLC
|
||||
name: string | string[];
|
||||
}
|
||||
|
||||
export interface storeconfigInput {
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export type IPLCsConfig = {
|
||||
[index: string]: IPLCConfig;
|
||||
};
|
||||
|
||||
export type IPLCConfig = {
|
||||
options: any;
|
||||
name: string;
|
||||
type: string;
|
||||
modules?: {
|
||||
inputs: { [index: string]: IModuleConfig };
|
||||
outputs: { [index: string]: IModuleConfig };
|
||||
pathToSubscribe: string;
|
||||
pathToPublish: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type IModuleConfig = {
|
||||
type: "inputs" | "outputs";
|
||||
name: string;
|
||||
start: number;
|
||||
len: number;
|
||||
|
||||
pathToPublishBufferContent: string;
|
||||
pathToSubscribeBufferContent: string;
|
||||
|
||||
plc: {
|
||||
name: string;
|
||||
pathToPublishModules: string;
|
||||
pathToSubscribeModules: string;
|
||||
};
|
||||
|
||||
endian?: endianTypes;
|
||||
elements: { [index: string]: IDataSpec };
|
||||
};
|
||||
|
||||
export type validTypes =
|
||||
| "BOOL"
|
||||
| "BYTE"
|
||||
| "WORD"
|
||||
| "DWORD"
|
||||
| "UINT"
|
||||
| "INT"
|
||||
| "DINT"
|
||||
| "UDINT"
|
||||
| "FLOAT"
|
||||
| "DOUBLE";
|
||||
export type endianTypes = "big" | "little";
|
||||
|
||||
export interface IDataSpec {
|
||||
type: validTypes;
|
||||
start: string;
|
||||
pathToData: string;
|
||||
db: "input" | "output" | number;
|
||||
convert?: (value: boolean | number) => any;
|
||||
}
|
||||
|
||||
export type connectplcOutput = null;
|
||||
export type useonlineconfigurationOutput = null;
|
||||
export type storeconfigOutput = null;
|
@ -1,357 +0,0 @@
|
||||
/**
|
||||
* @author Martin Karkowski
|
||||
* @email m.karkowski@zema.de
|
||||
* @create date 2020-03-03 16:19:19
|
||||
* @modify date 2020-03-03 16:19:19
|
||||
* @desc [description]
|
||||
*/
|
||||
|
||||
import { map } from "async";
|
||||
import { Container } from "inversify";
|
||||
import * as des from "../../mod-Descriptors/src/Descriptions";
|
||||
import * as FILESERVER from "../../mod-File-Server/assembly/manual-assembly";
|
||||
import * as PLC from "../../mod-Generic-PLC-Interface/assembly/manual-assembly";
|
||||
import { IPLCsConfig } from "../../mod-Generic-PLC-Interface/type/interfaces";
|
||||
import { generateOptions } from "../helpers/gen.options";
|
||||
import { connectplcInput, connectplcOutput, storeconfigInput, storeconfigOutput, useonlineconfigurationInput, useonlineconfigurationOutput } from "./BeckhoffPLC-Module-Types";
|
||||
import { BeckhoffPLCModule } from "./BeckhoffPLCModule";
|
||||
|
||||
|
||||
|
||||
|
||||
export class ExportedBeckhoffInterface extends des.MODULEBASE {
|
||||
private _instance: PLC.PLC;
|
||||
private _fileServer: FILESERVER.Fileserver;
|
||||
|
||||
/**
|
||||
* Function which is performed during the Startup
|
||||
*
|
||||
* @memberof ExportedPCL
|
||||
*/
|
||||
public onLoad(): void {
|
||||
/**
|
||||
/**
|
||||
* Define the Name, the Description, the Author and the currently used Version.
|
||||
*
|
||||
*/
|
||||
|
||||
this.name = "beckhoff";
|
||||
this.description =
|
||||
"This Module is used to Connect to a PLC with the given Interface";
|
||||
this.author = {
|
||||
forename: "martin",
|
||||
surename: "karkowski",
|
||||
mail: "m.karkowski@zema.de"
|
||||
};
|
||||
this.version = {
|
||||
/** Enter a Number */
|
||||
version: 1.0,
|
||||
date: new Date("2019.01.07")
|
||||
};
|
||||
|
||||
/** Define the required NPM-Modules below */
|
||||
this.requiredNpmPackages = {
|
||||
"node-ads": {
|
||||
link: "https://www.npmjs.com/package/node-ads",
|
||||
license: "none"
|
||||
}
|
||||
};
|
||||
|
||||
/** Create a Reference to it self */
|
||||
const _self = this;
|
||||
|
||||
/** Create the offered Methods HERE below */
|
||||
this.functions.connectplc = new des.FUNCTION(
|
||||
/** The Name of The Function. Everything will be replaced to Lower-Case */
|
||||
"connectplc",
|
||||
/** The Used Description of the Function. You can use multiple Lines */
|
||||
`Creates a Connection to a BECKHOFF PLC and queries the provided IOs`,
|
||||
/** The Input Parameters below */
|
||||
{
|
||||
path: "definitions.connectplcInput"
|
||||
},
|
||||
/** The Output Parameters below */
|
||||
{
|
||||
path: "definitions.connectplcOutput"
|
||||
},
|
||||
|
||||
/* Enter the Function below !
|
||||
*
|
||||
* The Paramter 'parameter' contains the Input Parameters
|
||||
*
|
||||
* In Order to work correctly store the Result of your Operation
|
||||
* in the Parameter as well and return it with the given callback.
|
||||
*
|
||||
* The parameter 'cancelHandler' is called if the function should
|
||||
* be aborted. To cancel the execution please link a cancel-Function.
|
||||
*
|
||||
* Example:
|
||||
* cancelHandler.cancelFunc = function () {
|
||||
* // Do your action here if required...
|
||||
* };
|
||||
*
|
||||
*
|
||||
* The parameter 'callback' is as callback to Show the System, that the
|
||||
* Execution has been finished. It offers the following possiblities.
|
||||
*
|
||||
* 1) error - put an occourd error in here otherwise select 'null'
|
||||
* 2) parameter - put the Adapted Parameter here (see comment above)
|
||||
* 3) result - put the isolated result in here. This is used for automatic conversion
|
||||
*
|
||||
*/
|
||||
function (
|
||||
parameter: connectplcInput,
|
||||
CancelHandler: des.CANCELHANDLE,
|
||||
callback: (error: any, parameter: any, result: connectplcOutput) => void
|
||||
) {
|
||||
/** Enter your Logic down below! */
|
||||
try {
|
||||
_self._instance.loadConfig(parameter, "add", {});
|
||||
callback(null, parameter, null);
|
||||
} catch (error) {
|
||||
/** Adapt the Callback */
|
||||
callback(error, parameter, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.functions.useonlineconfiguration = new des.FUNCTION(
|
||||
/** The Name of The Function. Everything will be replaced to Lower-Case */
|
||||
"useonlineconfiguration",
|
||||
/** The Used Description of the Function. You can use multiple Lines */
|
||||
`This Functions extracts the Configuration of the PLC`,
|
||||
/** The Input Parameters below */
|
||||
{
|
||||
path: "definitions.useonlineconfigurationInput"
|
||||
},
|
||||
/** The Output Parameters below */
|
||||
{
|
||||
path: "definitions.useonlineconfigurationOutput"
|
||||
},
|
||||
|
||||
/* Enter the Function below !
|
||||
*
|
||||
* The Paramter 'parameter' contains the Input Parameters
|
||||
*
|
||||
* In Order to work correctly store the Result of your Operation
|
||||
* in the Parameter as well and return it with the given callback.
|
||||
*
|
||||
* The parameter 'cancelHandler' is called if the function should
|
||||
* be aborted. To cancel the execution please link a cancel-Function.
|
||||
*
|
||||
* Example:
|
||||
* cancelHandler.cancelFunc = function () {
|
||||
* // Do your action here if required...
|
||||
* };
|
||||
*
|
||||
*
|
||||
* The parameter 'callback' is as callback to Show the System, that the
|
||||
* Execution has been finished. It offers the following possiblities.
|
||||
*
|
||||
* 1) error - put an occourd error in here otherwise select 'null'
|
||||
* 2) parameter - put the Adapted Parameter here (see comment above)
|
||||
* 3) result - put the isolated result in here. This is used for automatic conversion
|
||||
*
|
||||
*/
|
||||
async function (
|
||||
parameter: useonlineconfigurationInput,
|
||||
CancelHandler: des.CANCELHANDLE,
|
||||
callback: (error: any, parameter: useonlineconfigurationOutput, result: useonlineconfigurationOutput) => void
|
||||
) {
|
||||
/** Enter your Logic down below! */
|
||||
try {
|
||||
|
||||
if (!Array.isArray(parameter.name)) {
|
||||
parameter.name = [name];
|
||||
}
|
||||
|
||||
const result: Array<null | Error> = await map(parameter.name as string[], async (name) => {
|
||||
const _mod = _self._instance.getModule<BeckhoffPLCModule>(name);
|
||||
if (_mod.type === 'beckhoff-module') {
|
||||
|
||||
_mod.receiveOnlineUpdates.value = true;
|
||||
await _mod.initialized.waitFor((value) => value === true);
|
||||
|
||||
_self._instance.schemaUpdated.forcePublish();
|
||||
} else {
|
||||
const err = new Error('Module isnt a Beckhoff Module');
|
||||
return err
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
|
||||
const errs = result.filter(item => item !== null);
|
||||
|
||||
if (errs.length > 0) {
|
||||
callback(errs, null, null);
|
||||
} else {
|
||||
// ToDO subscribe to the new Config.
|
||||
callback(null, null, null);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
/** Adapt the Callback */
|
||||
callback(error, null, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/** Create the offered Methods HERE below */
|
||||
this.functions.storeconfig = new des.FUNCTION(
|
||||
/** The Name of The Function. Everything will be replaced to Lower-Case */
|
||||
"storeconfig",
|
||||
/** The Used Description of the Function. You can use multiple Lines */
|
||||
`Stores the PLC config.`,
|
||||
/** The Input Parameters below */
|
||||
{
|
||||
path: "definitions.storeconfigInput"
|
||||
},
|
||||
/** The Output Parameters below */
|
||||
{
|
||||
path: "definitions.storeconfigOutput"
|
||||
},
|
||||
|
||||
/* Enter the Function below !
|
||||
*
|
||||
* The Paramter 'parameter' contains the Input Parameters
|
||||
*
|
||||
* In Order to work correctly store the Result of your Operation
|
||||
* in the Parameter as well and return it with the given callback.
|
||||
*
|
||||
* The parameter 'cancelHandler' is called if the function should
|
||||
* be aborted. To cancel the execution please link a cancel-Function.
|
||||
*
|
||||
* Example:
|
||||
* cancelHandler.cancelFunc = function () {
|
||||
* // Do your action here if required...
|
||||
* };
|
||||
*
|
||||
*
|
||||
* The parameter 'callback' is as callback to Show the System, that the
|
||||
* Execution has been finished. It offers the following possiblities.
|
||||
*
|
||||
* 1) error - put an occourd error in here otherwise select 'null'
|
||||
* 2) parameter - put the Adapted Parameter here (see comment above)
|
||||
* 3) result - put the isolated result in here. This is used for automatic conversion
|
||||
*
|
||||
*/
|
||||
async function (
|
||||
parameter: storeconfigInput,
|
||||
CancelHandler: des.CANCELHANDLE,
|
||||
callback: (error: any, parameter: storeconfigInput, result: storeconfigOutput) => void
|
||||
) {
|
||||
/** Enter your Logic down below! */
|
||||
try {
|
||||
const events = _self._instance.getEventSchema();
|
||||
|
||||
// Use the File-Server to Store Files.
|
||||
await _self._fileServer.storeFile({
|
||||
identifier: 'plc-config',
|
||||
additionalOptions: {},
|
||||
content: JSON.stringify(events, undefined, 4),
|
||||
keywords: 'plc,config,' + parameter.filename,
|
||||
name: parameter.filename
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
/** Adapt the Callback */
|
||||
callback(error, parameter, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const plcs: IPLCsConfig = {
|
||||
plc_0001: {
|
||||
name: "plc_0001",
|
||||
options: generateOptions({
|
||||
name: "plc_0001",
|
||||
amsNetIdTarget: "5.72.112.82.1.1",
|
||||
amsNetIdSource: "1.1.1.1.1.1",
|
||||
host: "plc_0001",
|
||||
twinCatVersion: 3
|
||||
}),
|
||||
type: "beckhoff"
|
||||
},
|
||||
plc_0002: {
|
||||
name: "plc_0002",
|
||||
options: generateOptions({
|
||||
name: "plc_0002",
|
||||
amsNetIdTarget: "5.72.111.63.1.1",
|
||||
amsNetIdSource: "1.1.1.1.1.1",
|
||||
host: "plc_0002",
|
||||
twinCatVersion: 3
|
||||
}),
|
||||
type: "beckhoff"
|
||||
},
|
||||
plc_0003: {
|
||||
name: "plc_0003",
|
||||
options: generateOptions({
|
||||
name: "plc_0003",
|
||||
amsNetIdTarget: "5.68.6.216.1.1",
|
||||
amsNetIdSource: "1.1.1.1.1.1",
|
||||
host: "plc-0003",
|
||||
twinCatVersion: 3
|
||||
}),
|
||||
type: "beckhoff"
|
||||
},
|
||||
plc_0004: {
|
||||
name: 'plc_0004',
|
||||
options: generateOptions({
|
||||
name: 'plc_0004',
|
||||
amsNetIdTarget: "5.68.6.206.1.1",
|
||||
amsNetIdSource: "1.1.1.1.1.1",
|
||||
host: "plc-0004",
|
||||
twinCatVersion: 3
|
||||
}),
|
||||
type: "beckhoff"
|
||||
}
|
||||
};
|
||||
|
||||
/** Mark the Function as Auto-Start-Function*/
|
||||
this.autoStart.push({
|
||||
funcname: "beckhoff.connectplc",
|
||||
params: {
|
||||
plcs
|
||||
}
|
||||
},
|
||||
{
|
||||
funcname: "beckhoff.useonlineconfiguration",
|
||||
params: {
|
||||
name: ["plc_0001", "plc_0003"]
|
||||
},
|
||||
delay: 2000
|
||||
},
|
||||
{
|
||||
funcname: "beckhoff.storeconfig",
|
||||
params: {
|
||||
filename: "plc.json"
|
||||
},
|
||||
delay: 10000
|
||||
});
|
||||
}
|
||||
|
||||
/**,
|
||||
* Function which is after all Module of the Kernel are Loaded
|
||||
* By default instances should by created in here using the requested
|
||||
* Modules.
|
||||
*
|
||||
* @memberof ExportedPCL
|
||||
*/
|
||||
public init(_container: Container): void {
|
||||
/** Create an Instance of the Class */
|
||||
this._instance = _container.get<PLC.PLC>(PLC.TYPES.GenericPLC);
|
||||
this._fileServer = _container.get<FILESERVER.Fileserver>(FILESERVER.TYPES.FileServer);
|
||||
}
|
||||
|
||||
/**,
|
||||
* Function which is called if the Kernel stops. By default a Dispose
|
||||
* Methode if available is called.
|
||||
*
|
||||
* @memberof ExportedPCL
|
||||
*/
|
||||
public exit(): void { }
|
||||
}
|
||||
|
||||
export const EXTENSION = ExportedBeckhoffInterface;
|
@ -1,71 +0,0 @@
|
||||
/**
|
||||
* @author Martin Karkowski
|
||||
* @email m.karkowski@zema.de
|
||||
* @create date 2020-09-08 11:12:51
|
||||
* @modify date 2020-09-08 12:20:11
|
||||
* @desc [description]
|
||||
*/
|
||||
|
||||
import { inject, injectable } from "inversify";
|
||||
import { exportMethodToDispatcher, exportsElementsToDispatcher } from "../../../lib/dispatcher/nopeDispatcherDecorators";
|
||||
import { exportMethodToOpenAPI, exportsElementsToOpenAPI } from "../../../lib/openapi/nopeOpenAPIDecorators";
|
||||
import { DESCRIPTION, PLC } from '../../mod-Generic-PLC-Interface/assembly/manual-assembly';
|
||||
import { IPLCsConfig } from "../../mod-Generic-PLC-Interface/type/interfaces";
|
||||
import { SystemLogger } from "../../mod-Logger/src/Unique.Logger";
|
||||
import { IBeckhoffOptions, IBeckhoffPLCManager } from "../type/interfaces";
|
||||
import { BeckhoffPLCModule } from "./BeckhoffPLCModule";
|
||||
|
||||
|
||||
@injectable()
|
||||
@exportsElementsToOpenAPI({})
|
||||
@exportsElementsToDispatcher({})
|
||||
export class BeckhoffPLCManager implements IBeckhoffPLCManager {
|
||||
|
||||
constructor(
|
||||
@inject(DESCRIPTION.TYPES.GenericPLC) protected _genericPLC: PLC
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to add an PLC to the System.
|
||||
*
|
||||
* @param {IBeckhoffOptions} connectionParameters The Connection Parameters of the PLC.
|
||||
* @param {boolean} [disableOnlineConfiguartion] Optional Flag, to disable grabbing the Data online form the PLC.
|
||||
* @return {*} {Promise<void>}
|
||||
* @memberof BeckhoffPLCManager
|
||||
*/
|
||||
@exportMethodToOpenAPI({})
|
||||
@exportMethodToDispatcher({})
|
||||
async addPlc(connectionParameters: IBeckhoffOptions, disableOnlineConfiguartion?: boolean): Promise<void> {
|
||||
|
||||
const params: { plcs: IPLCsConfig } = {
|
||||
plcs: {}
|
||||
}
|
||||
|
||||
params.plcs[connectionParameters.name] = {
|
||||
name: connectionParameters.name,
|
||||
options: connectionParameters,
|
||||
type: 'beckhoff'
|
||||
};
|
||||
|
||||
// Trigger the PLC to Load an Assembly.
|
||||
await this._genericPLC.loadConfig(params, "add", {});
|
||||
|
||||
if (disableOnlineConfiguartion) {
|
||||
const _mod = this._genericPLC.getModule<BeckhoffPLCModule>(name);
|
||||
if (_mod.type === 'beckhoff-module') {
|
||||
_mod.receiveOnlineUpdates.setContent(true);
|
||||
await _mod.initialized.waitFor((value) => value === true);
|
||||
|
||||
|
||||
this._genericPLC.schemaUpdated.forcePublish();
|
||||
} else {
|
||||
const err = new Error('Module isnt a Beckhoff Module');
|
||||
this._logger.error('Module isnt a Beckhoff Module', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected _logger = SystemLogger.logger.getLogger("plc.beckhoff-plc-manager");
|
||||
}
|
@ -1,748 +0,0 @@
|
||||
/**
|
||||
* @author Martin Karkowski
|
||||
* @email m.karkowski@zema.de
|
||||
* @create date 2020-03-02 11:37:50
|
||||
* @modify date 2020-09-08 12:00:05
|
||||
* @desc [description]
|
||||
*/
|
||||
|
||||
import { inject, injectable } from "inversify";
|
||||
import * as ads from "node-ads";
|
||||
import { deepClone, SPLITCHAR } from "../../../lib/helpers/objectMethods";
|
||||
import { IModuleConfig, IPLCModule } from "../../mod-Generic-PLC-Interface/type/interfaces";
|
||||
import { SystemLogger } from "../../mod-Logger/src/Unique.Logger";
|
||||
import * as PUBSUB from "../../mod-Publish-And-Subscribe-System/assembly/manual-assembly";
|
||||
import { generateOptions } from "../helpers/gen.options";
|
||||
|
||||
|
||||
const IOS = [
|
||||
ads.ADSIGRP.IOIMAGE_RWIB,
|
||||
ads.ADSIGRP.IOIMAGE_RWIX,
|
||||
ads.ADSIGRP.IOIMAGE_RISIZE
|
||||
];
|
||||
const CONSIDER_RW = true;
|
||||
if (CONSIDER_RW) {
|
||||
IOS.push(...[ads.ADSIGRP.IOIMAGE_RWOB, ads.ADSIGRP.IOIMAGE_RWOX]);
|
||||
}
|
||||
|
||||
const DEFAULTS = {
|
||||
GVL_NAME: "GVL_IOS.",
|
||||
INPUT_IDENTIFIER: "Input",
|
||||
OUTPUT_IDENTIFIER: "Output"
|
||||
};
|
||||
|
||||
const LOGGERNAME = "plc.beckhoff-module";
|
||||
|
||||
@injectable()
|
||||
export class BeckhoffPLCModule implements IPLCModule {
|
||||
public shareConfig: {};
|
||||
|
||||
public get config(): IModuleConfig {
|
||||
const ret: IModuleConfig = {
|
||||
name: "ios",
|
||||
elements: {},
|
||||
plc: {
|
||||
name: this.adsOptions.getContent().name
|
||||
}
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
public type: 'beckhoff-module' = 'beckhoff-module';
|
||||
|
||||
public ioPathes: Set<string>;
|
||||
|
||||
public ioDefinitions: Map<
|
||||
string,
|
||||
{
|
||||
orginalName: string;
|
||||
module: IPLCModule;
|
||||
path: string;
|
||||
dataType: "number" | "boolean" | "string";
|
||||
type: "input" | "output";
|
||||
}
|
||||
>;
|
||||
|
||||
public bufferPath: string;
|
||||
|
||||
protected _enabled = false;
|
||||
|
||||
protected _getElementType(element: {
|
||||
indexGroup: number;
|
||||
indexOffset: number;
|
||||
size: number;
|
||||
name: string;
|
||||
type: string;
|
||||
comment: string;
|
||||
}) {
|
||||
switch (element.type) {
|
||||
case ads.BOOL.name:
|
||||
this._logger.debug('Element', element.name, '-> boolean');
|
||||
return 'boolean'
|
||||
case ads.BYTE.name:
|
||||
case ads.WORD.name:
|
||||
case ads.DWORD.name:
|
||||
case ads.SINT.name:
|
||||
case ads.USINT.name:
|
||||
case ads.INT.name:
|
||||
case ads.UINT.name:
|
||||
case ads.DINT.name:
|
||||
case ads.UDINT.name:
|
||||
case ads.LINT.name:
|
||||
case ads.ULINT.name:
|
||||
case ads.REAL.name:
|
||||
case ads.LREAL.name:
|
||||
this._logger.debug('Element', element.name, '-> number');
|
||||
return 'number'
|
||||
case ads.STRING.name:
|
||||
this._logger.debug('Element', element.name, '-> string');
|
||||
return 'string'
|
||||
case ads.TIME.name:
|
||||
case ads.TIME_OF_DAY.name:
|
||||
case ads.TOD.name: // TIME_OF_DAY alias
|
||||
case ads.DATE.name:
|
||||
case ads.DATE_AND_TIME.name:
|
||||
case ads.DT.name: // DATE_AND_TIME alias
|
||||
this._logger.debug('Element', element.name, '-> date');
|
||||
this._logger.error('Using unkown type', element.name, element.type);
|
||||
throw Error("Undefined Type");
|
||||
|
||||
default:
|
||||
if (element.type.startsWith(ads.STRING.name)) {
|
||||
this._logger.debug('Element', element.name, '-> string');
|
||||
return "string"
|
||||
}
|
||||
this._logger.error('Using unkown type', element.name, element.type)
|
||||
throw Error("Undefined Type");
|
||||
}
|
||||
}
|
||||
|
||||
protected _getHandleType(element: {
|
||||
indexGroup: number;
|
||||
indexOffset: number;
|
||||
size: number;
|
||||
name: string;
|
||||
type: string;
|
||||
comment: string;
|
||||
}) {
|
||||
|
||||
if (element.type === "STRING(80)")
|
||||
return ads.STRING;
|
||||
if (element.type.startsWith(ads.STRING.name)) {
|
||||
const number = parseFloat(element.type.slice("STRING(".length, element.type.length - 2));
|
||||
|
||||
if (SystemLogger.logger.isLogging(LOGGERNAME, 'debug')) {
|
||||
this._logger.debug(element.name, '-> string with Length', number);
|
||||
}
|
||||
|
||||
return ads.string(number)
|
||||
}
|
||||
|
||||
return ads[element.type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Function to get the Default Value of an IO
|
||||
* @param name Name of the IO
|
||||
*/
|
||||
protected _getDefaultValue(name: string) {
|
||||
const value = this.ioDefinitions.get(name);
|
||||
|
||||
switch (value.dataType) {
|
||||
case "boolean":
|
||||
return false;
|
||||
case "string":
|
||||
return ''
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the PLC IOs.
|
||||
*/
|
||||
public reset(): void {
|
||||
/** Dispose the old Config */
|
||||
for (const element in this.ios) {
|
||||
this._logger.debug(
|
||||
"resetting " +
|
||||
element +
|
||||
" to " +
|
||||
this._getDefaultValue(element).toString()
|
||||
);
|
||||
this.ios[element].setContent(this._getDefaultValue(element));
|
||||
}
|
||||
}
|
||||
|
||||
public name: string;
|
||||
|
||||
/**
|
||||
* Dispose Function, which will shutdown everything correctly.
|
||||
* All Subscription will be unsubscribed, the Client will be closed
|
||||
* etc...
|
||||
*/
|
||||
public dispose() {
|
||||
/** Dispose the old Config */
|
||||
for (const element in this.ios) {
|
||||
this.ios[element].dispose();
|
||||
this.observables.delete(element);
|
||||
}
|
||||
|
||||
/** Unsubscribe every Subscription */
|
||||
for (const _sub of this._subscriptions) {
|
||||
_sub.unsubscribe();
|
||||
}
|
||||
|
||||
if (this._client) {
|
||||
// Close the Ads-Connection
|
||||
this._client.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stopp transmitting changes
|
||||
*/
|
||||
public disbaleSendingUpdates(): void {
|
||||
this._enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable transmitting changes
|
||||
*/
|
||||
public enableSendingUpdates(): void {
|
||||
this._enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the Plc to update the IOs
|
||||
*/
|
||||
public forceSendingUpdate(): void {
|
||||
/** Dispose the old Config */
|
||||
for (const element in this.ios) {
|
||||
this.ios[element].forcePublish();
|
||||
}
|
||||
}
|
||||
|
||||
protected _subscriptions = new Set<
|
||||
PUBSUB.WildcardSubscription<any> | PUBSUB.Subscription<any>
|
||||
>();
|
||||
protected _id: string;
|
||||
protected _logger = SystemLogger.logger.getLogger(LOGGERNAME);
|
||||
|
||||
public observables: Map<string, PUBSUB.Observable<any>>;
|
||||
|
||||
@inject(PUBSUB.TYPES.Observable)
|
||||
public connected: PUBSUB.Observable<boolean>;
|
||||
|
||||
@inject(PUBSUB.TYPES.Observable)
|
||||
public adsOptions: PUBSUB.Observable<{
|
||||
name: string;
|
||||
path: string;
|
||||
host: string;
|
||||
amsNetIdTarget: string;
|
||||
amsNetIdSource: string;
|
||||
twinCatVersion: 2 | 3;
|
||||
port?: number;
|
||||
amsPortSource?: number;
|
||||
amsPortTarget?: number;
|
||||
timeout?: number;
|
||||
maxDelay?: number; // -> Latest time (in ms) after which the event has finished
|
||||
cycleTime?: number; // -> Time (in ms) after which the PLC server checks whether the variable has changed,
|
||||
inputName?: string; // -> Value to Filter Inputs.
|
||||
outputName?: string; // -> Value to Filter Outputs.
|
||||
}>;
|
||||
|
||||
@inject(PUBSUB.TYPES.Observable)
|
||||
public symbols: PUBSUB.Observable<any>;
|
||||
|
||||
@inject(PUBSUB.TYPES.Observable)
|
||||
public initialized: PUBSUB.Observable<boolean>;
|
||||
|
||||
@inject(PUBSUB.TYPES.Observable)
|
||||
public receiveOnlineUpdates: PUBSUB.Observable<boolean>;
|
||||
|
||||
@inject(PUBSUB.TYPES.Observable)
|
||||
public state: PUBSUB.Observable<
|
||||
| "INVALID"
|
||||
| "IDLE"
|
||||
| "RESET"
|
||||
| "INIT"
|
||||
| "START"
|
||||
| "RUN"
|
||||
| "STOP"
|
||||
| "SAVECFG"
|
||||
| "LOADCFG"
|
||||
| "POWERFAILURE"
|
||||
| "POWERGOOD"
|
||||
| "ERROR"
|
||||
| "SHUTDOWN"
|
||||
| "SUSPEND"
|
||||
| "RESUME"
|
||||
| "CONFIG"
|
||||
| "RECONFIG"
|
||||
| "STOPPING"
|
||||
>;
|
||||
|
||||
public _client: any = null;
|
||||
|
||||
protected _started = false;
|
||||
|
||||
/**
|
||||
* Function initalizing the Observables
|
||||
*/
|
||||
initObservables(): void {
|
||||
const _this = this;
|
||||
let start = true;
|
||||
|
||||
this.initialized.setContent(false);
|
||||
|
||||
this.ios = {};
|
||||
|
||||
this._subscriptions.add(
|
||||
this.adsOptions.subscribe((options) => {
|
||||
try {
|
||||
|
||||
// update the Name:
|
||||
_this.name = options.name;
|
||||
|
||||
if (_this._client) {
|
||||
_this._logger.warn("ADS-Client " + options.name + " is Running. Shutting Down.");
|
||||
_this._client.end(() => {
|
||||
_this._client = null;
|
||||
_this.adsOptions.forcePublish();
|
||||
});
|
||||
} else if (_this._client === null) {
|
||||
const _options = generateOptions(options);
|
||||
_this._logger.warn(
|
||||
"Creating new ADS-Client " + options.name + " with the following Info",
|
||||
_options
|
||||
);
|
||||
|
||||
_this._client = ads.connect(_options, () => {
|
||||
_this.connected.setContent(true);
|
||||
});
|
||||
|
||||
_this._client.on("error", (err) => {
|
||||
_this._logger.error(err, "Beckhoff Client " + options.name + " got Error");
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
_this._client.on("error", (err) => {
|
||||
_this._logger.error(err, "Beckhoff Client " + options.name + " got Error");
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.connected.setContent(false);
|
||||
|
||||
// Wait for The ADS-Client to Connect to the PLC.
|
||||
// If it is connected than subscribe to the PLC State.
|
||||
this._subscriptions.add(
|
||||
this.connected.subscribe((state) => {
|
||||
if (state) {
|
||||
|
||||
_this._client.readState((err, result) => {
|
||||
if (err) {
|
||||
_this._logger.error(err, _this.adsOptions.getContent().name + ' Failed Reading device status')
|
||||
} else {
|
||||
_this.state.setContent(ads.ADSSTATE.fromId(result.adsState));
|
||||
_this._logger.warn('Changed State to \"' + ads.ADSSTATE.fromId(result.adsState) + '\"');
|
||||
}
|
||||
});
|
||||
|
||||
// Request updates on new Symbol-Tables.
|
||||
_this._client.notify({
|
||||
indexGroup: ads.ADSIGRP.SYM_VERSION,
|
||||
indexOffset: 0,
|
||||
bytelength: ads.BYTE
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.receiveOnlineUpdates.setContent(false);
|
||||
this.receiveOnlineUpdates.subscribe(value => {
|
||||
if (value && _this.connected.getContent() && _this.state.getContent() === 'RUN') {
|
||||
_this._getConfig(10);
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to the PLC State. Only if goes to Run-Mode
|
||||
// Update the Symbol-Table.
|
||||
this._subscriptions.add(
|
||||
this.state.subscribe((state) => {
|
||||
if (_this.connected.getContent() && state === 'RUN') {
|
||||
|
||||
if (_this.receiveOnlineUpdates.getContent()) {
|
||||
_this._getConfig(10);
|
||||
}
|
||||
|
||||
_this._client.on("notification", (handle) => {
|
||||
// Subscribe to new Symbol-Tables and updated IOs.
|
||||
if (handle.symname) {
|
||||
const name = handle.symname;
|
||||
|
||||
// An Input / Output has been updated
|
||||
if (_this.ios[name]) {
|
||||
if (SystemLogger.logger.isLogging(LOGGERNAME, 'debug')) {
|
||||
_this._logger.debug("updating io " + name + " to:", handle.value);
|
||||
}
|
||||
_this.ios[name].setContent(handle.value, _this._id);
|
||||
}
|
||||
} else {
|
||||
// Only if the Symbol Table has been updated
|
||||
// request the new Symbol-Table.
|
||||
if (!start && _this.receiveOnlineUpdates.getContent()) {
|
||||
|
||||
// A New Configuration has been used => Update the Variables.
|
||||
_this._getConfig(10);
|
||||
|
||||
}
|
||||
start = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
protected _getConfig(counter: number) {
|
||||
const _this = this;
|
||||
this.getConfiguration().catch(err => {
|
||||
if (counter > 0) {
|
||||
this._logger.warn(err, 'Failed getting the Configuration. Retry left ' + counter.toString());
|
||||
setTimeout(_this._getConfig.apply, 1000, _this, counter - 1);
|
||||
} else {
|
||||
this._logger.error(err, 'Failed getting the Configuration');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
protected _considerIO(element: {
|
||||
indexGroup: number;
|
||||
indexOffset: number;
|
||||
size: number;
|
||||
name: string;
|
||||
type: string;
|
||||
comment: string;
|
||||
}): boolean {
|
||||
return (
|
||||
element.name.startsWith(DEFAULTS.GVL_NAME) &&
|
||||
(element.name.includes(
|
||||
this.adsOptions.getContent().inputName || DEFAULTS.INPUT_IDENTIFIER
|
||||
) ||
|
||||
element.name.includes(
|
||||
this.adsOptions.getContent().outputName || DEFAULTS.OUTPUT_IDENTIFIER
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
protected _getType(handle: {
|
||||
symname: string;
|
||||
bytelength: number;
|
||||
transmissionMode?: ads.NOTIFY.ONCHANGE | ads.NOTIFY.CYCLIC;
|
||||
maxDelay?: number; // -> Latest time (in ms) after which the event has finished
|
||||
cycleTime?: number; // -> Time (in ms) after which the PLC server checks whether the variable has changed
|
||||
value?: any;
|
||||
}): "input" | "output" {
|
||||
let type: "input" | "output" = null;
|
||||
// Test if it is an Input or Output
|
||||
if (
|
||||
handle.symname.includes(
|
||||
this.adsOptions.getContent().inputName || DEFAULTS.INPUT_IDENTIFIER
|
||||
)
|
||||
) {
|
||||
type = "input";
|
||||
} else if (
|
||||
handle.symname.includes(
|
||||
this.adsOptions.getContent().outputName || DEFAULTS.OUTPUT_IDENTIFIER
|
||||
)
|
||||
) {
|
||||
type = "output";
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Function to adapt the IO Name.
|
||||
* @param name Name of the IO.
|
||||
*/
|
||||
protected _adaptName(name: string) {
|
||||
return "ios" + SPLITCHAR + name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to get the Name
|
||||
* @param name Name of the IO
|
||||
* @param mode Either extract the IO Name only or Add the Default.gvl_name
|
||||
*/
|
||||
protected _getName(name: string, mode: "reverse" | "foward") {
|
||||
if (mode === "reverse") return name.split(DEFAULTS.GVL_NAME)[1];
|
||||
|
||||
return DEFAULTS.GVL_NAME + name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to create the Path of the Variable
|
||||
* @param name Name of the IO
|
||||
* @param type Type of the IO (input / output)
|
||||
*/
|
||||
protected _getPath(name: string, type: "input" | "output") {
|
||||
return this.adsOptions.getContent().name + SPLITCHAR + type + SPLITCHAR + name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Function which will extract the Data from the PLC
|
||||
*
|
||||
* @param {(
|
||||
* err: any,
|
||||
* config: {
|
||||
* indexGroup: number;
|
||||
* indexOffset: number;
|
||||
* size: number;
|
||||
* name: string;
|
||||
* type: string;
|
||||
* comment: string;
|
||||
* }[]
|
||||
* ) => void} [cb] Callback, which will be called with the Configuraiton
|
||||
* @returns {Promise<{
|
||||
* indexGroup: number;
|
||||
* indexOffset: number;
|
||||
* size: number;
|
||||
* name: string;
|
||||
* type: string;
|
||||
* comment: string;
|
||||
* }[]>} The Configuration of the PLC
|
||||
* @memberof BeckhoffPLCModule
|
||||
*/
|
||||
public getConfiguration(
|
||||
cb?: (
|
||||
err: any,
|
||||
config: {
|
||||
indexGroup: number;
|
||||
indexOffset: number;
|
||||
size: number;
|
||||
name: string;
|
||||
type: string;
|
||||
comment: string;
|
||||
}[]
|
||||
) => void
|
||||
): Promise<{
|
||||
indexGroup: number;
|
||||
indexOffset: number;
|
||||
size: number;
|
||||
name: string;
|
||||
type: string;
|
||||
comment: string;
|
||||
}[]> {
|
||||
const _this = this;
|
||||
|
||||
this.initialized.setContent(false);
|
||||
|
||||
return new Promise<{
|
||||
indexGroup: number;
|
||||
indexOffset: number;
|
||||
size: number;
|
||||
name: string;
|
||||
type: string;
|
||||
comment: string;
|
||||
}[]>((resolve, reject) => {
|
||||
try {
|
||||
_this._client.getSymbols(
|
||||
(
|
||||
err,
|
||||
symbols: Array<{
|
||||
indexGroup: number;
|
||||
indexOffset: number;
|
||||
size: number;
|
||||
name: string;
|
||||
type: string;
|
||||
comment: string;
|
||||
}>
|
||||
) => {
|
||||
if (symbols) {
|
||||
// && IOS.includes(element.indexGroup)
|
||||
const ios = symbols.filter((element) =>
|
||||
_this._considerIO(element)
|
||||
);
|
||||
|
||||
const handles = ios.map((element) => {
|
||||
const _handle: {
|
||||
symname: string;
|
||||
bytelength: number;
|
||||
transmissionMode?: ads.NOTIFY.ONCHANGE | ads.NOTIFY.CYCLIC;
|
||||
maxDelay?: number; // -> Latest time (in ms) after which the event has finished
|
||||
cycleTime?: number; // -> Time (in ms) after which the PLC server checks whether the variable has changed
|
||||
value?: any;
|
||||
} = {
|
||||
symname: element.name,
|
||||
bytelength: _this._getHandleType(element),
|
||||
cycleTime: _this.adsOptions.getContent().cycleTime,
|
||||
maxDelay: _this.adsOptions.getContent().maxDelay
|
||||
};
|
||||
|
||||
let type = _this._getType(_handle);
|
||||
|
||||
_this.ioDefinitions.set(element.name, {
|
||||
orginalName: element.name,
|
||||
module: _this,
|
||||
dataType: _this._getElementType(element),
|
||||
path: _this._getPath(element.name, type),
|
||||
type
|
||||
});
|
||||
|
||||
_this._logger.info("Detected " + element.name);
|
||||
|
||||
_this.ioPathes.add(_this._getPath(element.name, type));
|
||||
|
||||
return _handle;
|
||||
});
|
||||
|
||||
for (const handle of handles) {
|
||||
let type = _this._getType(handle);
|
||||
|
||||
if (type === "input") {
|
||||
// Subscribe only Inputs
|
||||
_this._logger.debug("Defined", handle.symname, "as input");
|
||||
this._client.notify(handle);
|
||||
}
|
||||
|
||||
const name = handle.symname;
|
||||
// const name = this._getName(handle.symname, 'reverse');
|
||||
const path = _this._getPath(name, type);
|
||||
|
||||
_this.ios[name] = _this._pubSub.createObservable({
|
||||
/** Subscribe to the given Path if required */
|
||||
subscribeToPath: path,
|
||||
publishToPath: path,
|
||||
useAutoPathForPublishing: false,
|
||||
useAutoPathForSubscribing: false,
|
||||
forceUsingPath: true
|
||||
});
|
||||
|
||||
_this.ios[name].setContent(_this._getDefaultValue(name));
|
||||
|
||||
if (SystemLogger.logger.isLogging(LOGGERNAME, "debug")) {
|
||||
_this._logger.debug(
|
||||
'Setting "' + name + '" to',
|
||||
_this.ios[name].getContent()
|
||||
);
|
||||
_this._logger.debug(
|
||||
'Setting "' + path + '" to',
|
||||
_this.ios[name].getContent()
|
||||
);
|
||||
}
|
||||
|
||||
_this.observables.set(
|
||||
"ios" + SPLITCHAR + name,
|
||||
_this.ios[name]
|
||||
);
|
||||
|
||||
/** Subscribe to Changes to adapt the Buffer */
|
||||
_this._subscriptions.add(
|
||||
_this.ios[name].subscribe((value, sender) => {
|
||||
/** Only if the Update isn't provided by an IO itself, update the PLC */
|
||||
if (sender != _this._id) {
|
||||
const _handle = deepClone(handle);
|
||||
_handle.value = value;
|
||||
|
||||
_this._client.write(_handle, (err) => {
|
||||
if (err) {
|
||||
_this._logger.error(
|
||||
err,
|
||||
"Failed Updating IO on Beckhoff"
|
||||
);
|
||||
} else if (SystemLogger.logger.isLogging(LOGGERNAME, 'debug')) {
|
||||
_this._logger.debug(
|
||||
"Updated IO \"" + name + "\" to",
|
||||
value
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/** Forcing an Update */
|
||||
_this.ios[name].forcePublish(_this._id);
|
||||
}
|
||||
|
||||
if (typeof cb === "function") {
|
||||
cb(null, ios);
|
||||
}
|
||||
|
||||
resolve(ios);
|
||||
|
||||
_this.initialized.setContent(true);
|
||||
|
||||
return;
|
||||
} else if (typeof cb === "function") {
|
||||
cb(err, null);
|
||||
}
|
||||
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
_this._logger.error(e, 'Something went wrong during initialization');
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Element containing all configured Elements
|
||||
*
|
||||
* @type {{[index:string]: PUBSUB.Observable<any>}}
|
||||
* @memberof SmartBufferBase
|
||||
*/
|
||||
public ios: { [index: string]: PUBSUB.Observable<any> } = {};
|
||||
|
||||
/**
|
||||
* Creates an instance of SmartBufferBase.
|
||||
* @param {PUBSUB.PubSubSystem} _pubSub the Publish and Subscribe System.
|
||||
* @memberof SmartBufferBase
|
||||
*/
|
||||
constructor(
|
||||
@inject(PUBSUB.TYPES.PubSubSystem) protected _pubSub: PUBSUB.PubSubSystem,
|
||||
@inject(IDGEN.TYPES.IdGenerator) _idGen: IDGEN.IdGenerator
|
||||
) {
|
||||
this.observables = new Map<string, PUBSUB.Observable<any>>();
|
||||
|
||||
// Store an id for the Element
|
||||
this._id = _idGen.generateId();
|
||||
|
||||
this.shareConfig = {};
|
||||
|
||||
/** Elements which has to be subscribed */
|
||||
let attributes = [];
|
||||
|
||||
for (const attr of attributes) {
|
||||
this.shareConfig[attr] = {
|
||||
useAutoPathForSubscribing: true
|
||||
};
|
||||
}
|
||||
|
||||
attributes = [];
|
||||
|
||||
for (const attr of attributes) {
|
||||
if (this.shareConfig[attr] === undefined) {
|
||||
this.shareConfig[attr] = {
|
||||
useAutoPathForPublishing: true
|
||||
};
|
||||
} else {
|
||||
this.shareConfig[attr].useAutoPathForPublishing = true;
|
||||
}
|
||||
}
|
||||
|
||||
this._id = _idGen.generateId("_beckhoffClient");
|
||||
this.ioPathes = new Set();
|
||||
this.ioDefinitions = new Map();
|
||||
this.bufferPath = "";
|
||||
this.name = "";
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
/**
|
||||
* @author Martin Karkowski
|
||||
* @email m.karkowski@zema.de
|
||||
* @create date 2020-08-25 15:46:35
|
||||
* @modify date 2020-09-08 12:00:24
|
||||
* @desc [description]
|
||||
*/
|
||||
|
||||
import { inject, injectable } from "inversify";
|
||||
import {
|
||||
IPLCConfig, IPLCModule,
|
||||
IPLCModuleFactory
|
||||
} from "../../mod-Generic-PLC-Interface/type/interfaces";
|
||||
import { SystemLogger } from "../../mod-Logger/src/Unique.Logger";
|
||||
import { TYPES } from "../type/types";
|
||||
import { BeckhoffPLCModule } from "./BeckhoffPLCModule";
|
||||
|
||||
@injectable()
|
||||
export class BeckhoffPLCModuleFactory implements IPLCModuleFactory {
|
||||
readonly name: string;
|
||||
readonly typeIdentifier: string;
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.BeckhoffPLCModuleFactory) private _bufferFactory: () => BeckhoffPLCModule
|
||||
) {
|
||||
this.name = "beckhoff-buffer-factory";
|
||||
this.typeIdentifier = "beckhoff";
|
||||
}
|
||||
|
||||
async createModule(config: IPLCConfig, options: any): Promise<IPLCModule[]> {
|
||||
|
||||
this._logger.info('creating module', config)
|
||||
|
||||
const mod = this._bufferFactory();
|
||||
mod.adsOptions.setContent(config.options);
|
||||
|
||||
return [mod];
|
||||
}
|
||||
|
||||
protected _logger = SystemLogger.logger.getLogger("plc.raw-buffer-factory");
|
||||
}
|
@ -1,926 +0,0 @@
|
||||
/**
|
||||
* @author Martin Karkowski
|
||||
* @email m.karkowski@zema.de
|
||||
* @create date 2020-04-09 11:49:53
|
||||
* @modify date 2020-07-22 21:58:43
|
||||
* @desc [description]
|
||||
*/
|
||||
|
||||
import { OnInit, Input, ViewChild, OnDestroy, Type, Component } from '@angular/core';
|
||||
import { rsetattr, rgetattr, deepClone } from '../../@zema/ZISS-TypeScript-Library/src/Object-Methods';
|
||||
import { ITemplate, IMustacheTemplate } from './interfaces/ITemplate';
|
||||
import { IEditPage } from './edit-pages/edit-pages.interface';
|
||||
import { initEditor } from './user-interface/editor';
|
||||
import { defaultToolbar } from './defaults/default.toolbar';
|
||||
import { IContextConfig, rigthClickActions, generateDefaulContextMenu } from './defaults/default.context-menu';
|
||||
import { generateGraphOptions } from './defaults/default.graph-options';
|
||||
import { generateAddFunction } from './defaults/default.add-edge-method';
|
||||
import { genDefaultDict } from './defaults/default.edit-dict';
|
||||
import { stringify, parse, parseWithFunctions } from '../../@zema/ZISS-TypeScript-Library/src/JSON';
|
||||
import { enableClusterPreview } from './addons/cluster.preview';
|
||||
import 'style-loader!angular2-toaster/toaster.css';
|
||||
import { ZemaServiceProvider } from '../../@zema/zema-service-provider.service';
|
||||
import { writeToClipboard } from '../../@zema/ZISS-Browser-Library/src/clipboard';
|
||||
import { getSubElements, adaptPositions, adaptIDS } from './helpers/data.handlers';
|
||||
import { IUndoRedoGraph } from './interfaces/IGraph';
|
||||
import { IBaseNodeOptions } from '../../@zema/ZISS-Network/type/IBaseNodeOptions';
|
||||
import { IBaseEdgeOptions } from '../../@zema/ZISS-Network/type/IBaseEdgeOptions';
|
||||
import { IGraphToolComponent, ICallbackData } from './interfaces/IGraphTool';import { ILayoutOptions, IBasicLayoutComponent, IRigthClickActions, ISelectionTemplate, IToolbarConfig, IPossiblePanels, ISelectionConfig } from '../gui-components-basic-layout/types/interfaces';
|
||||
import { defaultHotkeys } from './defaults/default.hotkeys';
|
||||
import { BasicLayoutComponent } from '../gui-components-basic-layout/src/layout.component';
|
||||
import { TemplateEditorComponent } from './edit-pages/template.edit-page';
|
||||
import { IVisjsOptions } from './interfaces/IVisjsOptions';
|
||||
import { waitFor, sleep } from '../../@zema/ZISS-TypeScript-Library/src/Async-Helpers';
|
||||
import { NbThemeService, NbGlobalPhysicalPosition } from '@nebular/theme';
|
||||
|
||||
@Component({
|
||||
template: ``
|
||||
})
|
||||
export class BaseGraphEditor<N extends IBaseNodeOptions, E extends IBaseEdgeOptions, D extends ICallbackData<N,E>> implements IGraphToolComponent<N,E,D>,OnInit, OnDestroy {
|
||||
public parseFunctions = true;
|
||||
|
||||
public layoutOptions: ILayoutOptions<ITemplate<N,E> | IMustacheTemplate, D>;
|
||||
|
||||
public options: {
|
||||
addEdgeCallback?: (edgeData: E, callback: (edgeData: E) => void) => void;
|
||||
editOnSelect?: boolean;
|
||||
editOnChange?: boolean;
|
||||
parseFunctions?: boolean;
|
||||
enableContextMenu?: boolean;
|
||||
enableEditing?: boolean;
|
||||
hidePanelOnDeselect?: boolean;
|
||||
hideToolbar?: boolean;
|
||||
hideRightPanel?: boolean;
|
||||
} = {
|
||||
enableEditing: true,
|
||||
enableContextMenu: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* Element for Providing the VIS-JS Options
|
||||
*/
|
||||
@Input()
|
||||
public set visjsOptions(value: IVisjsOptions) {
|
||||
this._visjsOptions = value;
|
||||
if (this.network) {
|
||||
this.network.network.setOptions(value);
|
||||
}
|
||||
}
|
||||
public get visjsOptions(): IVisjsOptions {
|
||||
return this._visjsOptions;
|
||||
}
|
||||
private _visjsOptions: IVisjsOptions = generateGraphOptions();
|
||||
|
||||
@Input()
|
||||
public set nodes(nodes: Array<N | IBaseNodeOptions>) {
|
||||
this._nodes = nodes;
|
||||
|
||||
// If a Network is available, use this one
|
||||
// to Render Elements.
|
||||
if (this.network) {
|
||||
this.network.clearNodes();
|
||||
this.network.addNode(nodes);
|
||||
}
|
||||
}
|
||||
public get nodes() {
|
||||
// If a Network is available, use this one
|
||||
// to return the Elements.
|
||||
if (this.network) {
|
||||
return this.network.nodes;
|
||||
}
|
||||
// Otherwise return an empty Array
|
||||
return [];
|
||||
}
|
||||
private _nodes = new Array<N | IBaseNodeOptions>();
|
||||
|
||||
@Input()
|
||||
public set edges(edges: Array<E | IBaseEdgeOptions>) {
|
||||
this._edges = edges;
|
||||
|
||||
// If a Network is available, use this one
|
||||
// to Render Elements.
|
||||
if (this.network) {
|
||||
this.network.clearEdges();
|
||||
this.network.addEdge(edges);
|
||||
}
|
||||
}
|
||||
public get edges() {
|
||||
if (this.network) {
|
||||
return this.network.edges;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
private _edges = new Array<E | IBaseEdgeOptions>();
|
||||
|
||||
@Input()
|
||||
public set editPanelDict(value: { nodes: { [index: string]: Type<IEditPage<N,E>> }, edges: { [index: string]: Type<IEditPage<N,E>> } }) {
|
||||
if (!value.nodes.default || !value.edges.default) {
|
||||
throw TypeError('A Default Element must be specified');
|
||||
}
|
||||
this._editPanelDict = value;
|
||||
}
|
||||
public get editPanelDict() {
|
||||
return this._editPanelDict;
|
||||
}
|
||||
|
||||
private _editPanelDict = genDefaultDict()
|
||||
|
||||
|
||||
@Input()
|
||||
public set toolbar(config: IToolbarConfig<ICallbackData<IBaseNodeOptions, IBaseEdgeOptions>>) {
|
||||
this._toolbar = config;
|
||||
}
|
||||
public get toolbar() {
|
||||
return this._toolbar;
|
||||
}
|
||||
|
||||
protected _toolbar = defaultToolbar(generateGraphOptions(),{
|
||||
useVersionControl: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Container with Actions
|
||||
*
|
||||
*/
|
||||
public rightClickActions: rigthClickActions = [];
|
||||
|
||||
public contextMenuGenerator: IContextConfig<N,E,D> = generateDefaulContextMenu();
|
||||
|
||||
public network: IUndoRedoGraph<N,E>;
|
||||
public templates: ISelectionConfig<ITemplate<N,E> | IMustacheTemplate>;
|
||||
|
||||
public readonly divID = 'editor';
|
||||
|
||||
/** Element storing the current Mouse-Position */
|
||||
public get mousePos() {
|
||||
if (this.layout && this.layout.currentMousePosition) {
|
||||
return {
|
||||
x: this.layout.currentMousePosition.offsetX,
|
||||
y: this.layout.currentMousePosition.offsetY
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Element containing the Template, which will be added.
|
||||
*/
|
||||
public get template(): ITemplate<N,E> | IMustacheTemplate | null {
|
||||
if (this.layoutOptions.selection){
|
||||
return this.layout.selection.getSelectetTemplate()
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of GraphToolComponent.
|
||||
* @param contextMenuService The Context Menu Service
|
||||
*/
|
||||
constructor(
|
||||
public zemaService: ZemaServiceProvider,
|
||||
protected themeService: NbThemeService
|
||||
) {
|
||||
const _this = this;
|
||||
this.themeService.getJsTheme().subscribe(() => {
|
||||
_this.ngOnDestroy();
|
||||
_this.initEditor().catch(err => _this.zemaService.logger.error(err));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public adaptedData(event, data: IPossiblePanels): D {
|
||||
throw Error('Abstract Class, not implemented');
|
||||
}
|
||||
|
||||
@ViewChild(BasicLayoutComponent, {static: true})
|
||||
public layout: IBasicLayoutComponent<ITemplate<N, E> | IMustacheTemplate, D>;
|
||||
|
||||
public loadJSON(data: string, overwrite = true) {
|
||||
try {
|
||||
/** Read In the JSON File */
|
||||
const content = parse(data, this.parseFunctions);
|
||||
/** Load the Data itself */
|
||||
this.network.loadData(content, overwrite)
|
||||
} catch (e) {
|
||||
this.zemaService.showToast('danger', 'Failed Loading', e, 0);
|
||||
this.layout.openDialogWithText({
|
||||
text: e.toString(),
|
||||
title: 'Failed loading Data to Graph',
|
||||
closeOnBackdropClick: true,
|
||||
dynamicSize: true,
|
||||
buttons: [
|
||||
{
|
||||
callback: close => close(),
|
||||
label: 'OK',
|
||||
status: 'danger'
|
||||
}
|
||||
]
|
||||
})
|
||||
this.zemaService.logger.error(e, 'Failed loading Data to Graph');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Function to open Up an Edit-Window,
|
||||
* Rendering the content of the Component.
|
||||
*/
|
||||
public openEditInterface<C extends IEditPage<N | IBaseNodeOptions, E | IBaseEdgeOptions>>(
|
||||
/*** The Angular Component */
|
||||
component: Type<C>,
|
||||
/** The Stettings, of the Component */
|
||||
settings: {
|
||||
inputTemplate?: ITemplate<N, E>,
|
||||
[index: string]: any,
|
||||
},
|
||||
title: string,
|
||||
/** callback, if the Sucess-Button is pressed */
|
||||
sucessCallback: (data: {
|
||||
template : ITemplate<N | IBaseNodeOptions, E | IBaseEdgeOptions>,
|
||||
callback?: (template: ITemplate<N | IBaseNodeOptions, E | IBaseEdgeOptions>) => ITemplate<N | IBaseNodeOptions, E | IBaseEdgeOptions>
|
||||
}) => void,
|
||||
mode: 'sidebar' | 'popup' = 'popup') {
|
||||
|
||||
const _this = this;
|
||||
|
||||
// Disable the Hotkeys.
|
||||
this.disableHotkeys();
|
||||
this.zemaService.logger.info('Open edit Window',mode)
|
||||
|
||||
switch(mode){
|
||||
case 'sidebar':
|
||||
|
||||
this.layout.openDynamicPanel({
|
||||
title,
|
||||
component: {
|
||||
component: component,
|
||||
inputs: Object.assign(settings,{
|
||||
inputTemplate: settings.inputTemplate,
|
||||
graph: this.network
|
||||
})
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
label: 'Save',
|
||||
status: 'success',
|
||||
callback(instance, close, changePanelVisbility) {
|
||||
if (instance.isValid()) {
|
||||
_this.enableHotkeys();
|
||||
sucessCallback(instance.getAdapted());
|
||||
if (!_this.options.editOnSelect){
|
||||
close();
|
||||
changePanelVisbility(false);
|
||||
}
|
||||
} else {
|
||||
_this.layout.panels.right.showMessage({
|
||||
body: 'Error in Data. Data canot be stored',
|
||||
hideOnClick: true,
|
||||
buttons: 'close'
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Abort',
|
||||
status: 'danger',
|
||||
callback(instance, close, changePanelVisbility) {
|
||||
_this.enableHotkeys();
|
||||
close();
|
||||
changePanelVisbility(false);
|
||||
}
|
||||
}
|
||||
],
|
||||
showOnCreate: true,
|
||||
panel: 'right',
|
||||
append: false
|
||||
})
|
||||
|
||||
break;
|
||||
case 'popup':
|
||||
this.layout.openDialogComponent<C>({
|
||||
title,
|
||||
component: {
|
||||
component,
|
||||
inputs: Object.assign(settings,{
|
||||
inputTemplate: settings.inputTemplate,
|
||||
graph: this.network
|
||||
})
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
label: 'Save',
|
||||
status: 'success',
|
||||
callback(instance, close) {
|
||||
if (instance.isValid()) {
|
||||
_this.enableHotkeys();
|
||||
sucessCallback(instance.getAdapted());
|
||||
close();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Abort',
|
||||
status: 'danger',
|
||||
callback(instance, close) {
|
||||
_this.enableHotkeys();
|
||||
close();
|
||||
}
|
||||
}
|
||||
],
|
||||
closeOnBackdropClick: false,
|
||||
closeOnEsc: false
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public addNode(pos: { x: number, y: number }) {
|
||||
const _this = this;
|
||||
|
||||
switch (this.template.type) {
|
||||
case 'elements':
|
||||
if (this.template.nodes && this.template.nodes.length > 0) {
|
||||
this.template.nodes[0].x = pos.x;
|
||||
this.template.nodes[0].y = pos.y;
|
||||
|
||||
let componentSelector = 'default';
|
||||
|
||||
if (this.template.nodes.length > 0 && this.template.nodes[0].editorComponentSelector) {
|
||||
componentSelector = this.template.nodes[0].editorComponentSelector;
|
||||
}
|
||||
|
||||
this.openEditInterface(
|
||||
this.editPanelDict.nodes[componentSelector],
|
||||
{
|
||||
inputTemplate: deepClone(this.template as ITemplate<N, E>),
|
||||
},
|
||||
'Add Node'
|
||||
, (data) => {
|
||||
let adapted = adaptIDS(data.template);
|
||||
|
||||
if (typeof data.callback === 'function') {
|
||||
adapted = data.callback(adapted);
|
||||
}
|
||||
|
||||
_this.network.addNode(adapted.nodes);
|
||||
_this.network.addEdge(adapted.edges);
|
||||
},
|
||||
'popup'
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
default:
|
||||
this.enableHotkeys();
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to Update the Data of a Node.
|
||||
* @param selection The Selected Node.
|
||||
*/
|
||||
public updateNode(selection: Array<N>) {
|
||||
const _self = this;
|
||||
|
||||
/** Extract the Component, which should be used in the Prompt */
|
||||
let componentSelector = 'default';
|
||||
if (selection.length > 0 && selection[0].editorComponentSelector) {
|
||||
componentSelector = selection[0].editorComponentSelector;
|
||||
}
|
||||
|
||||
const comp = this.editPanelDict.nodes[componentSelector];
|
||||
|
||||
if (comp) {
|
||||
/** Open the Window, with the Edit-Prompt */
|
||||
this.openEditInterface(this.editPanelDict.nodes[componentSelector], {
|
||||
inputTemplate: {
|
||||
nodes: selection,
|
||||
edges: [],
|
||||
type: 'elements'
|
||||
},
|
||||
},
|
||||
'Edit Node',
|
||||
(data) => {
|
||||
_self.network.updateNode(data.template.nodes);
|
||||
});
|
||||
} else {
|
||||
this.zemaService.logger.warn('Editor is Trying to open an Unkown Edit-Component');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function, which is used to Update an Edge
|
||||
* @param edge The Corresponding Edge, which will be updated.
|
||||
*/
|
||||
public updateEdges(edge: E) {
|
||||
const _self = this;
|
||||
|
||||
/** Extract the Component, which should be used in the Prompt */
|
||||
let componentSelector = 'default';
|
||||
if (edge.editorComponentSelector) {
|
||||
componentSelector = edge.editorComponentSelector;
|
||||
}
|
||||
|
||||
const comp = this.editPanelDict.edges[componentSelector];
|
||||
|
||||
if (comp) {
|
||||
/** Open the Window, with the Edit-Prompt */
|
||||
this.openEditInterface(comp, {
|
||||
inputTemplate: {
|
||||
nodes: [],
|
||||
edges: [edge],
|
||||
type: 'elements'
|
||||
}
|
||||
}, 'Edit Edge', (data) => {
|
||||
_self.network.updateEdge(data.template.edges);
|
||||
});
|
||||
} else {
|
||||
throw TypeError('The Element trys to open an Unkown Edit-Component');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private _destroyNetwork: () => void;
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (typeof this._destroyNetwork === 'function')
|
||||
this._destroyNetwork();
|
||||
}
|
||||
|
||||
protected _updateLayoutOptions(options: ILayoutOptions<ITemplate<N,E> | IMustacheTemplate, D>){
|
||||
return options;
|
||||
}
|
||||
|
||||
public async initEditor() {
|
||||
const _this = this;
|
||||
|
||||
let colors: any = null;
|
||||
const subcription = this.themeService.getJsTheme().subscribe((value) => {
|
||||
colors = value.variables;
|
||||
});
|
||||
|
||||
await waitFor(() => (colors !== undefined && colors !== null), {
|
||||
additionalDelay: 100
|
||||
});
|
||||
|
||||
subcription.unsubscribe();
|
||||
|
||||
const layoutOptions: ILayoutOptions<ITemplate<N,E> | IMustacheTemplate, D> = {
|
||||
title: 'Editor',
|
||||
panels: [
|
||||
{
|
||||
type: 'right',
|
||||
id: 'properties',
|
||||
hidden: true,
|
||||
resizable: true,
|
||||
minSize: 300,
|
||||
toggle: ! this.options.hideRightPanel,
|
||||
style: "background-color: "+ colors.bg2
|
||||
}
|
||||
],
|
||||
adaptData(event, panels){
|
||||
return _this.adaptedData(event, panels)
|
||||
},
|
||||
hotkeys: defaultHotkeys<N,E>(),
|
||||
onCopy(data){
|
||||
try {
|
||||
_this.copySelectionToClipboard();
|
||||
data.zemaService.showToast('success', 'Clipboard', 'Copied Content to Clipboard');
|
||||
} catch (e) {
|
||||
data.zemaService.showToast('warning', 'Clipboard', 'Failed Copying To Clipboard');
|
||||
}
|
||||
},
|
||||
onPaste(text, data){
|
||||
try {
|
||||
_this.paste(parseWithFunctions(text),data.network.network.DOMtoCanvas(data.component.mousePos), false);
|
||||
data.zemaService.showToast('success', 'Clipboard', 'Copied Content to Clipboard');
|
||||
} catch (e) {
|
||||
data.zemaService.showToast('warning', 'Clipboard', 'Failed Copying To Clipboard');
|
||||
}
|
||||
},
|
||||
showToggleElements: true
|
||||
}
|
||||
|
||||
if (!this.options.hideToolbar) {
|
||||
layoutOptions.panels.push({
|
||||
type: 'top',
|
||||
id: 'toolbar',
|
||||
toggle: false,
|
||||
});
|
||||
|
||||
layoutOptions.toolbar = {
|
||||
panel: 'top',
|
||||
config: this.toolbar
|
||||
}
|
||||
}
|
||||
|
||||
if (this.templates){
|
||||
|
||||
layoutOptions.selection = {
|
||||
panel: 'left',
|
||||
id: 'selection',
|
||||
templates: this.templates,
|
||||
preview: {
|
||||
id: 'preview',
|
||||
type: 'preview',
|
||||
resizable: true
|
||||
}
|
||||
};
|
||||
|
||||
layoutOptions.panels.push({
|
||||
type: 'left',
|
||||
id: 'left',
|
||||
hidden: false,
|
||||
resizable: true,
|
||||
minSize: 200,
|
||||
maxSize: 500,
|
||||
overflow: 'hidden',
|
||||
toggle: false,
|
||||
});
|
||||
}
|
||||
|
||||
this.layoutOptions = this._updateLayoutOptions(layoutOptions);
|
||||
|
||||
// Wait until the Layout has been initialized
|
||||
await waitFor(async function(){
|
||||
while (!_this.layout) {
|
||||
sleep(10);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
// Wait until the Editor is Ready and then Create the 3D Renderer
|
||||
await this.layout.ready.waitFor((value) => value === true);
|
||||
|
||||
/** Generate the Default Callback for adding a Node */
|
||||
rsetattr(
|
||||
this._visjsOptions,
|
||||
'manipulation.addEdge',
|
||||
generateAddFunction(_this, (data, callback) => {
|
||||
|
||||
/** Test if an Edge-Template exists */
|
||||
if (_this.template.type === 'elements' && _this.template.edges.length === 1 && _this.template.nodes.length === 0) {
|
||||
const edge = Object.assign(
|
||||
_this.template.edges[0],
|
||||
data
|
||||
);
|
||||
|
||||
if (typeof _this.options.addEdgeCallback === 'function') {
|
||||
_this.options.addEdgeCallback(edge, callback);
|
||||
} else {
|
||||
callback(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (!rgetattr(this._visjsOptions, 'manipulation.enabled', false)) {
|
||||
rsetattr(this._visjsOptions, 'manipulation.enabled', false);
|
||||
}
|
||||
|
||||
const editor = initEditor<N,E,D, BaseGraphEditor<N,E,D>>({
|
||||
component: this,
|
||||
element: this.layout.panels.main,
|
||||
networkOptions: this.visjsOptions,
|
||||
renderMinimap: false
|
||||
});
|
||||
|
||||
// Store the Network
|
||||
this.network = editor.network;
|
||||
this._destroyNetwork = editor.destroy;
|
||||
|
||||
// Adapt the Message Function
|
||||
this.network.showMessage = (type, title, body, duration = 5000) => {
|
||||
_this.zemaService.showToast(type, title, body, duration);
|
||||
}
|
||||
|
||||
const editItemIfPossible = (params) => {
|
||||
if (params.nodes.length === 1) {
|
||||
// A Node was Selected. Figure out, whether it is a cluster or not.
|
||||
const nodeID = params.nodes[0];
|
||||
|
||||
if (_this.network.network.isCluster(nodeID)) {
|
||||
// Uncluster the Nodes
|
||||
_this.network.network.openCluster(nodeID);
|
||||
} else {
|
||||
// Open up the Settings Tab
|
||||
const selection = getSubElements(this.network, false);
|
||||
_this.updateNode(selection);
|
||||
}
|
||||
} else if (params.edges.length === 1) {
|
||||
// A Node was Selected. Figure out, whether it is a cluster or not.
|
||||
const edges = _this.network.data.edges.get(params.edges[0]);
|
||||
_this.updateEdges(edges);
|
||||
}
|
||||
}
|
||||
|
||||
// Make the Default behaviour adding new Nodes
|
||||
this.network.on('doubleClick', (params) => {
|
||||
if (this.options.enableEditing){
|
||||
// Test if no Element was Selected
|
||||
if (params.nodes.length === 0 && params.edges.length === 0) {
|
||||
// If so, just add the new Element
|
||||
_this.addNode(params.pointer.canvas);
|
||||
} else {
|
||||
editItemIfPossible(params);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.network.on('oncopy', event => {
|
||||
try {
|
||||
_this.copySelectionToClipboard();
|
||||
_this.zemaService.showToast('success', 'Clipboard', 'Copied Content to Clipboard');
|
||||
} catch (e) {
|
||||
_this.zemaService.showToast('warning', 'Clipboard', 'Failed Copying To Clipboard');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
this.network.on('onpaste', async event => {
|
||||
console.log(event)
|
||||
try {
|
||||
const data = await _this.readDataFromClipboard()
|
||||
_this.paste(parseWithFunctions(data), _this.network.network.DOMtoCanvas(_this.mousePos), false);
|
||||
_this.zemaService.showToast('success', 'Clipboard', 'Copied Content to Clipboard');
|
||||
} catch (e) {
|
||||
_this.zemaService.showToast('warning', 'Clipboard', 'Failed Copying To Clipboard');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
let _mousePosOnContextMenu = this.mousePos;
|
||||
let rightClickActions: IRigthClickActions<ICallbackData<N,E>> = [];
|
||||
const _contextHandler = (params) => {
|
||||
let nodeID = null;
|
||||
if (params.nodes.length === 1) {
|
||||
nodeID = _this.network.network.getNodeAt(params.pointer.DOM);
|
||||
if (!nodeID)
|
||||
nodeID = params.nodes[0];
|
||||
}
|
||||
|
||||
let edgeID = null;
|
||||
if (params.edges.length === 1) {
|
||||
edgeID = params.edges[0];
|
||||
}
|
||||
|
||||
// If nothing is selected, try to select the element,
|
||||
// which is underneath the Pointer
|
||||
if (params.nodes.length === 0) {
|
||||
nodeID = _this.network.network.getNodeAt(params.pointer.DOM);
|
||||
|
||||
// Test if a Node is underneath the Pointer
|
||||
if (nodeID) {
|
||||
// Make shure the Element is selected
|
||||
_this.network.network.selectNodes([nodeID]);
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing is selected, try to select the element,
|
||||
// which is underneath the Pointer
|
||||
if (params.edges.length === 0) {
|
||||
edgeID = _this.network.network.getEdgeAt(params.pointer.DOM);
|
||||
|
||||
// Test if a Node is underneath the Pointer
|
||||
if (edgeID) {
|
||||
// Make shure the Element is selected
|
||||
_this.network.network.selectEdges([edgeID]);
|
||||
}
|
||||
}
|
||||
|
||||
// Test whether multiple Nodes or just a single Node is selected.
|
||||
if (nodeID) {
|
||||
// A single Node is selected
|
||||
rightClickActions = _this.contextMenuGenerator.node(_this, params.pointer.DOM, nodeID);
|
||||
} else if (params.nodes.length > 1) {
|
||||
// The Default Right-Clickmenu must be selected
|
||||
rightClickActions = _this.contextMenuGenerator.default(_this, params.pointer.DOM, params.nodes, params.edges);
|
||||
} else if (edgeID) {
|
||||
// Only 1 Edge is selected
|
||||
rightClickActions = _this.contextMenuGenerator.edge(_this, params.pointer.DOM, edgeID);
|
||||
} else {
|
||||
rightClickActions = _this.contextMenuGenerator.background(_this, params.pointer.DOM);
|
||||
}
|
||||
}
|
||||
|
||||
this.network.on('oncontext', (params) => {
|
||||
if (_this.options.enableContextMenu){
|
||||
// Make shure the Context-Menu is only Opened, if
|
||||
// The event isnt prevented
|
||||
if (params.event.prevent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call creating the Menu Entries.
|
||||
_contextHandler(params)
|
||||
|
||||
// Decide, whether an Element is selected or not
|
||||
params.event.preventDefault();
|
||||
|
||||
// Store the current Position
|
||||
_mousePosOnContextMenu = _this.mousePos;
|
||||
|
||||
// Check after a View seconds whether the Mouse has been move
|
||||
// setTimeout(_contextHandler, 200, params);
|
||||
setTimeout(() => {
|
||||
if (Math.abs(_this.mousePos.x - _mousePosOnContextMenu.x) < 5 && Math.abs(_this.mousePos.y - _mousePosOnContextMenu.y) < 5) {
|
||||
if (rightClickActions.length > 0) {
|
||||
_this.layout.openContextMenu(params.event, rightClickActions);
|
||||
} else {
|
||||
_this.network.network.unselectAll();
|
||||
}
|
||||
} else {
|
||||
rightClickActions = [];
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
});
|
||||
|
||||
// Make shure the Sidepanel is working correctly
|
||||
this.network.on('select', (params) => {
|
||||
if (_this.options.enableEditing){
|
||||
if (_this.options.editOnSelect && _this.options.hidePanelOnDeselect && params.nodes.length === 0 && params.edges.length === 0){
|
||||
_this.layout.panels.right.hide();
|
||||
} else if (_this.options.editOnSelect){
|
||||
editItemIfPossible(params);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
enableClusterPreview(this);
|
||||
|
||||
this.network.addNode(this._nodes);
|
||||
this.network.addEdge(this._edges);
|
||||
|
||||
this.layoutOptions.onResized = () => {
|
||||
editor.resize();
|
||||
}
|
||||
editor.resize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to initalize the Editor
|
||||
*/
|
||||
public ngOnInit() {
|
||||
// const _this = this;
|
||||
// this.initEditor().catch(err => _this.zemaService.logger.error(err, 'Init of Editor Failed.'));
|
||||
}
|
||||
|
||||
public enableHotkeys() {
|
||||
this.layout.hotkeysEnabled = true;
|
||||
}
|
||||
|
||||
public disableHotkeys() {
|
||||
this.layout.hotkeysEnabled = false;
|
||||
// this.network.network.disableEditMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of the current Selection.
|
||||
*
|
||||
* @returns A Template containing the Selected Nodes and Edges.
|
||||
* @memberof GraphToolComponent
|
||||
*/
|
||||
public createTemplateOfSelectedElements() {
|
||||
const selected: ITemplate<N | IBaseNodeOptions,E | IBaseEdgeOptions> = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
type: 'elements'
|
||||
};
|
||||
|
||||
const selection = this.network.network.getSelection(true);
|
||||
|
||||
selected.nodes = deepClone(this.nodes.filter(item => selection.nodes.indexOf(item.id) !== -1));
|
||||
selected.edges = deepClone(this.edges.filter(item => selection.edges.indexOf(item.id) !== -1));
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to paste copied stuff
|
||||
* @param position The Position, where the content should be inserted
|
||||
* @param useExistingNodesForEdges Decide, whether the connections should be although copied
|
||||
*/
|
||||
public paste(
|
||||
template: ITemplate<N,E>,
|
||||
position: {
|
||||
x: number,
|
||||
y: number
|
||||
},
|
||||
useExistingNodesForEdges: boolean) {
|
||||
|
||||
const data = adaptPositions<N,E>(adaptIDS<N,E>(template, useExistingNodesForEdges), position);
|
||||
|
||||
this.network.addNode(data.nodes);
|
||||
this.network.addEdge(data.edges);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function, will copy the Selection to the Clipboard
|
||||
*
|
||||
* @memberof GraphToolComponent
|
||||
*/
|
||||
public copySelectionToClipboard() {
|
||||
this.copyToClipboard(stringify(this.createTemplateOfSelectedElements(), this.parseFunctions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Function, which will paste a String to the Clipboard
|
||||
*
|
||||
* @param {string} content The stringified Content
|
||||
* @memberof GraphToolComponent
|
||||
*/
|
||||
public copyToClipboard(content: string){
|
||||
writeToClipboard(content);
|
||||
}
|
||||
|
||||
public async readDataFromClipboard(){
|
||||
if (navigator && (navigator as any).clipboard) {
|
||||
try {
|
||||
const text = await (navigator as any).clipboard.readText();
|
||||
|
||||
return text;
|
||||
} catch (err) {
|
||||
this.zemaService.showToast('warning', 'Clipboard', 'Failed Pasting Clipboard. Issue with the Rights');
|
||||
this.zemaService.logger.error(err, 'Failed using Clipboard')
|
||||
}
|
||||
} else if (navigator && (navigator as any).permissions) {
|
||||
|
||||
const permissionStatus = await (navigator as any).permissions.query({
|
||||
name: 'clipboard-read'
|
||||
} as any)
|
||||
|
||||
this.zemaService.logger.info('Current Permission State is ',permissionStatus.state)
|
||||
// Will be 'granted', 'denied' or 'prompt':
|
||||
|
||||
const _this = this;
|
||||
// Listen for changes to the permission state
|
||||
permissionStatus.onchange = () => {
|
||||
_this.zemaService.logger.info('Current Permission State is ',permissionStatus.state)
|
||||
};
|
||||
|
||||
this.zemaService.showToast('warning', 'Clipboard', 'Failed Accessing Clipboard');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async pasteFromClipboard() {
|
||||
try {
|
||||
const data = await this.readDataFromClipboard();
|
||||
if (data) {
|
||||
const clipboardData: ITemplate<N, E> = parse(data, this.parseFunctions);
|
||||
this.paste(clipboardData, this.network.network.DOMtoCanvas(this.mousePos), false);
|
||||
this.zemaService.showToast('success', 'Clipboard', 'pasted');
|
||||
}
|
||||
} catch (e) {
|
||||
this.zemaService.showToast('warning', 'Clipboard', 'Failed Pasting Clipboard');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function which will Create a Template for Mustache. This
|
||||
* Template can be added to the Sidebar (After Editing) to enter
|
||||
* makros.
|
||||
*
|
||||
* @returns a Mustache Template
|
||||
* @memberof GraphToolComponent
|
||||
*/
|
||||
public generateTemplateData() {
|
||||
const selected = this.createTemplateOfSelectedElements()
|
||||
|
||||
const template: ISelectionTemplate<IMustacheTemplate> = {
|
||||
keywords: ['replace me'],
|
||||
text: 'Displayed Label - Replace Me',
|
||||
template: {
|
||||
example: {},
|
||||
schema: {},
|
||||
mustache: stringify(selected, true),
|
||||
type: 'mustache'
|
||||
}
|
||||
};
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function which will Create a Template for Mustache. This
|
||||
* Template can be added to the Sidebar (After Editing) to enter
|
||||
* makros. The Template will be copied as String to the clipboard
|
||||
*
|
||||
* @memberof GraphToolComponent
|
||||
*/
|
||||
public copyTemplateDataToClipboard() {
|
||||
// Write the serialized Template to the Clipboard
|
||||
writeToClipboard(stringify(this.generateTemplateData(), this.parseFunctions));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user