This commit is contained in:
Martin Karkowski 2020-12-01 13:03:23 +01:00
parent b359adaf56
commit 73220f1a20
6 changed files with 0 additions and 2223 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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