Browse Source

UI: Upload/Download SCADA symbol support.

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
4d42ba3fe0
  1. 8
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts
  2. 57
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  3. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.scss
  4. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts
  5. 2
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html
  6. 10
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts
  7. 58
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts
  8. 4
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html
  9. 51
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts
  10. 12
      ui-ngx/src/app/shared/components/image/image-dialog.component.ts
  11. 12
      ui-ngx/src/app/shared/components/image/image-gallery.component.ts
  12. 2
      ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html
  13. 31
      ui-ngx/src/app/shared/components/image/upload-image-dialog.component.ts

8
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts

@ -131,8 +131,12 @@ export class ScadaSymbolWidgetComponent implements OnInit, AfterViewInit, OnDest
content: string) {
const simulated = this.ctx.utilsService.widgetEditMode ||
this.ctx.isPreview || (isDefinedAndNotNull(this.settings.simulated) ? this.settings.simulated : false);
this.scadaSymbolObject = new ScadaSymbolObject(rootElement, this.ctx, content,
this.settings.scadaSymbolObjectSettings, this, simulated);
if (content.startsWith('<parsererror')) {
rootElement.innerHTML = content;
} else {
this.scadaSymbolObject = new ScadaSymbolObject(rootElement, this.ctx, content,
this.settings.scadaSymbolObjectSettings, this, simulated);
}
}
}

57
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -191,9 +191,38 @@ export const emptyMetadata = (): ScadaSymbolMetadata => ({
properties: []
});
const svgPartsRegex = /(<svg.*?>)(.*)<\/svg>/gms;
const tbNamespaceRegex = /<svg.*(xmlns:tb="https:\/\/thingsboard.io\/svg").*>/gms;
export const applyTbNamespaceToSvgContent = (svgContent: string): string => {
svgPartsRegex.lastIndex = 0;
let svgRootNode: string;
let innerSvg = '';
let match = svgPartsRegex.exec(svgContent);
if (match != null) {
if (match.length > 1) {
svgRootNode = match[1];
}
if (match.length > 2) {
innerSvg = match[2];
}
}
if (!svgRootNode) {
throw new Error('Invalid SVG document.');
}
tbNamespaceRegex.lastIndex = 0;
match = tbNamespaceRegex.exec(svgRootNode);
if (match === null || !match.length) {
svgRootNode = svgRootNode.slice(0, -1) + ' xmlns:tb="https://thingsboard.io/svg">';
return `${svgRootNode}\n${innerSvg}\n</svg>`;
}
return svgContent;
};
export const parseScadaSymbolMetadataFromContent = (svgContent: string): ScadaSymbolMetadata => {
try {
svgContent = applyTbNamespaceToSvgContent(svgContent);
const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml');
return parseScadaSymbolMetadataFromDom(svgDoc);
} catch (_e) {
@ -216,13 +245,17 @@ const parseScadaSymbolMetadataFromDom = (svgDoc: Document): ScadaSymbolMetadata
};
export const updateScadaSymbolMetadataInContent = (svgContent: string, metadata: ScadaSymbolMetadata): string => {
svgContent = applyTbNamespaceToSvgContent(svgContent);
const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml');
const parsererror = svgDoc.getElementsByTagName('parsererror');
if (parsererror?.length) {
return parsererror[0].outerHTML;
}
updateScadaSymbolMetadataInDom(svgDoc, metadata);
return svgDoc.documentElement.outerHTML;
};
const updateScadaSymbolMetadataInDom = (svgDoc: Document, metadata: ScadaSymbolMetadata) => {
svgDoc.documentElement.setAttribute('xmlns:tb', 'https://thingsboard.io/svg');
let metadataElement: Node;
const elements = svgDoc.getElementsByTagName('tb:metadata');
if (elements?.length) {
@ -237,7 +270,6 @@ const updateScadaSymbolMetadataInDom = (svgDoc: Document, metadata: ScadaSymbolM
metadataElement.appendChild(cdata);
};
const svgPartsRegex = /(<svg .*?>)(.*)<\/svg>/gms;
const tbMetadataRegex = /<tb:metadata>.*<\/tb:metadata>/gs;
export interface ScadaSymbolContentData {
@ -389,6 +421,7 @@ export class ScadaSymbolObject {
this.shapeResize$ = new ResizeObserver(() => {
this.resize();
});
this.svgContent = applyTbNamespaceToSvgContent(this.svgContent);
const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml');
this.metadata = parseScadaSymbolMetadataFromDom(doc);
const defaults = defaultScadaSymbolObjectSettings(this.metadata);
@ -645,12 +678,22 @@ export class ScadaSymbolObject {
}
private renderState(): void {
this.metadata.stateRender(this.context, this.svgShape);
try {
this.metadata.stateRender(this.context, this.svgShape);
} catch (e) {
console.error(e);
}
for (const tag of this.metadata.tags) {
const elements = this.context.tags[tag.tag];// this.svgShape.find(`[tb\\:tag="${tag.tag}"]`);
elements.forEach(element => {
tag.stateRender(this.context, element);
});
const elements = this.context.tags[tag.tag];
if (elements) {
elements.forEach(element => {
try {
tag.stateRender(this.context, element);
} catch (e) {
console.error(e);
}
});
}
}
}

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.scss

@ -13,8 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-scada-symbol-appearance-properties {
gap: 16px;
display: flex;
flex-direction: column;
:host ::ng-deep {
.tb-scada-symbol-appearance-properties {
gap: 16px;
display: flex;
flex-direction: column;
}
}

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts

@ -72,8 +72,7 @@ import { map } from 'rxjs/operators';
useExisting: forwardRef(() => ScadaSymbolObjectSettingsComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
]
})
export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator {

2
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html

@ -39,6 +39,7 @@
<div class="tb-scada-symbol-editor-upload-buttons">
<div class="tb-primary-fill">
<button mat-icon-button
(click)="updateScadaSymbol.emit()"
matTooltip="{{ 'scada.update-symbol' | translate }}"
matTooltipPosition="above">
<mat-icon>file_upload</mat-icon>
@ -46,6 +47,7 @@
</div>
<div class="tb-primary-fill">
<button mat-icon-button
(click)="downloadScadaSymbol.emit()"
matTooltip="{{ 'scada.download-symbol' | translate }}"
matTooltipPosition="above">
<mat-icon>download</mat-icon>

10
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts

@ -17,11 +17,11 @@
import {
AfterViewInit, ChangeDetectorRef,
Component,
ElementRef,
ElementRef, EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
OnInit, Output,
SimpleChanges,
ViewChild,
ViewContainerRef,
@ -60,6 +60,12 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
@Input()
readonly: boolean;
@Output()
updateScadaSymbol = new EventEmitter();
@Output()
downloadScadaSymbol = new EventEmitter();
scadaSymbolEditObject: ScadaSymbolEditObject;
zoomInDisabled = false;

58
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts

@ -56,7 +56,7 @@ export interface ScadaSymbolEditObjectCallbacks {
onZoom?: () => void;
}
const minSymbolZoom = 1;
const minSymbolZoom = 0.75;
const maxSymbolZoom = 4;
export class ScadaSymbolEditObject {
@ -150,7 +150,7 @@ export class ScadaSymbolEditObject {
runner
.zoom(level)
.during(() => {
const box = this.restrictToMargins(this.svgShape.viewbox(), 0);
const box = this.restrictToMargins(this.svgShape.viewbox());
this.svgShape.viewbox(box);
})
.after(() => {
@ -168,7 +168,7 @@ export class ScadaSymbolEditObject {
}
private doSetup() {
this.setupZoomPan(0);
this.setupZoomPan();
(window as any).SVG = svgjs;
forkJoin([
from(import('tooltipster')),
@ -178,33 +178,37 @@ export class ScadaSymbolEditObject {
});
}
private setupZoomPan(margin: number) {
private setupZoomPan() {
this.svgShape.panZoom({
zoomMin: minSymbolZoom,
zoomMax: maxSymbolZoom,
zoomFactor: this.zoomFactor
});
this.svgShape.on('zoom', (e) => {
const {
detail: { level, focus }
} = e as any;
this.svgShape.zoom(level, focus);
this.postZoom(this.callbacks.onZoom, e, margin);
this.postZoom(this.callbacks.onZoom, e);
});
this.svgShape.on('panning', (e) => {
const box = (e as any).detail.box;
this.svgShape.viewbox(this.restrictToMargins(box, margin));
this.svgShape.viewbox(this.restrictToMargins(box));
e.preventDefault();
});
this.svgShape.on('panStart', (_e) => {
if (this.svgShape.zoom() > minSymbolZoom) {
this.hideTooltips();
this.svgShape.node.style.cursor = 'grab';
}
this.hideTooltips();
this.svgShape.node.style.cursor = 'grab';
});
this.svgShape.on('panEnd', (_e) => {
this.restoreTooltips();
this.svgShape.node.style.cursor = 'default';
});
this.svgShape.zoom(minSymbolZoom);
}
private postZoom(callback?: () => void, e?: any, margin = 0) {
const box = this.restrictToMargins(this.svgShape.viewbox(), margin);
private postZoom(callback?: () => void, e?: any) {
const box = this.restrictToMargins(this.svgShape.viewbox());
this.svgShape.viewbox(box);
setTimeout(() => {
this.updateTooltipPositions();
@ -215,16 +219,18 @@ export class ScadaSymbolEditObject {
}
}
private restrictToMargins(box: Box, margin: number): Box {
if (box.x < -margin) {
box.x = -margin;
} else if ((box.x + box.width) > (this.box.width + margin)) {
box.x = this.box.width + margin - box.width;
private restrictToMargins(box: Box): Box {
const marginX = Math.max(box.width - this.box.width, 0);
const marginY = Math.max(box.height - this.box.height, 0);
if (box.x < -marginX) {
box.x = -marginX;
} else if ((box.x + box.width) > (this.box.width + marginX)) {
box.x = this.box.width + marginX - box.width;
}
if (box.y < -margin) {
box.y = -margin;
} else if ((box.y + box.height) > (this.box.height + margin)) {
box.y = this.box.height + margin - box.height;
if (box.y < -marginY) {
box.y = -marginY;
} else if ((box.y + box.height) > (this.box.height + marginY)) {
box.y = this.box.height + marginY - box.height;
}
return box;
}
@ -324,7 +330,6 @@ export class ScadaSymbolEditObject {
this.scale = scale;
this.svgShape.node.style.transform = `scale(${this.scale})`;
this.updateHoverFilterStyle();
this.updateZoomOptions();
this.updateTooltipPositions();
}
if (this.performSetup) {
@ -352,15 +357,6 @@ export class ScadaSymbolEditObject {
);
}
private updateZoomOptions() {
// this.zoomFactor = 2 / this.scale;
this.svgShape.panZoom({
zoomMin: minSymbolZoom,
zoomMax: maxSymbolZoom,
zoomFactor: this.zoomFactor
});
}
private updateTooltipPositions() {
for (const e of this.elements) {
e.updateTooltipPosition();

4
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html

@ -114,7 +114,9 @@
<tb-scada-symbol-editor *ngIf="!previewMode" #symbolEditor
[readonly]="readonly"
[data]="symbolEditorData"
[editObjectCallbacks]="editObjectCallbacks"></tb-scada-symbol-editor>
[editObjectCallbacks]="editObjectCallbacks"
(updateScadaSymbol)="updateScadaSymbol()"
(downloadScadaSymbol)="downloadScadaSymbol()"></tb-scada-symbol-editor>
<tb-dashboard *ngIf="previewMode"
class="tb-absolute-fill"
[aliasController]="aliasController"

51
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts

@ -66,6 +66,11 @@ import {
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import {
UploadImageDialogComponent,
UploadImageDialogData, UploadImageDialogResult
} from '@shared/components/image/upload-image-dialog.component';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'tb-scada-symbol',
@ -143,7 +148,8 @@ export class ScadaSymbolComponent extends PageComponent
private entityService: EntityService,
private utils: UtilsService,
private translate: TranslateService,
private imageService: ImageService) {
private imageService: ImageService,
private dialog: MatDialog) {
super(store);
}
@ -317,6 +323,49 @@ export class ScadaSymbolComponent extends PageComponent
this.symbolEditorDirty = dirty;
}
updateScadaSymbol() {
this.dialog.open<UploadImageDialogComponent, UploadImageDialogData,
UploadImageDialogResult>(UploadImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
imageSubType: this.symbolData.imageResource.resourceSubType,
image: this.symbolData.imageResource
}
}).afterClosed().subscribe((result) => {
if (result?.scadaSymbolContent) {
this.symbolData.scadaSymbolContent = result.scadaSymbolContent;
this.symbolEditorData = {
scadaSymbolContent: this.symbolData.scadaSymbolContent
};
this.symbolEditorDirty = true;
}
});
}
downloadScadaSymbol() {
let metadata: ScadaSymbolMetadata;
if (this.scadaSymbolFormGroup.valid) {
metadata = this.scadaSymbolFormGroup.get('metadata').value;
} else {
metadata = parseScadaSymbolMetadataFromContent(this.origSymbolData.scadaSymbolContent);
}
const linkElement = document.createElement('a');
const scadaSymbolContent = this.prepareScadaSymbolContent(metadata);
const blob = new Blob([scadaSymbolContent], { type: this.symbolData.imageResource.descriptor.mediaType });
const url = URL.createObjectURL(blob);
linkElement.setAttribute('href', url);
linkElement.setAttribute('download', this.symbolData.imageResource.fileName);
const clickEvent = new MouseEvent('click',
{
view: window,
bubbles: true,
cancelable: false
}
);
linkElement.dispatchEvent(clickEvent);
}
private updatePreviewWidgetSettings() {
this.previewWidget = deepClone(this.previewWidget);
this.previewWidget.config.settings.scadaSymbolObjectSettings = this.previewScadaSymbolObjectSettings;

12
ui-ngx/src/app/shared/components/image/image-dialog.component.ts

@ -25,7 +25,7 @@ import { ImageService } from '@core/http/image.service';
import { ImageResource, ImageResourceInfo, imageResourceType } from '@shared/models/resource.models';
import {
UploadImageDialogComponent,
UploadImageDialogData
UploadImageDialogData, UploadImageDialogResult
} from '@shared/components/image/upload-image-dialog.component';
import { UrlHolder } from '@shared/pipe/image.pipe';
import { ImportExportService } from '@shared/import-export/import-export.service';
@ -142,7 +142,7 @@ export class ImageDialogComponent extends
$event.stopPropagation();
}
this.dialog.open<UploadImageDialogComponent, UploadImageDialogData,
ImageResource>(UploadImageDialogComponent, {
UploadImageDialogResult>(UploadImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
@ -150,12 +150,12 @@ export class ImageDialogComponent extends
image: this.image
}
}).afterClosed().subscribe((result) => {
if (result) {
if (result?.image) {
this.imageChanged = true;
this.image = result;
this.image = result.image;
let url;
if (result.base64) {
url = result.base64;
if (result.image.base64) {
url = result.image.base64;
} else {
url = this.image.link;
}

12
ui-ngx/src/app/shared/components/image/image-gallery.component.ts

@ -62,7 +62,7 @@ import { ItemSizeStrategy, ScrollGridComponent } from '@shared/components/grid/s
import { MatDialog } from '@angular/material/dialog';
import {
UploadImageDialogComponent,
UploadImageDialogData
UploadImageDialogData, UploadImageDialogResult
} from '@shared/components/image/upload-image-dialog.component';
import { ImageDialogComponent, ImageDialogData } from '@shared/components/image/image-dialog.component';
import { ImportExportService } from '@shared/import-export/import-export.service';
@ -650,20 +650,20 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
uploadImage(): void {
this.dialog.open<UploadImageDialogComponent, UploadImageDialogData,
ImageResourceInfo>(UploadImageDialogComponent, {
UploadImageDialogResult>(UploadImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
imageSubType: this.imageSubType
}
}).afterClosed().subscribe((result) => {
if (result) {
if (result?.image) {
if (this.selectionMode) {
this.imageSelected.next(result);
this.imageSelected.next(result.image);
} else {
if (this.isScada) {
const type = imageResourceType(result);
const key = encodeURIComponent(result.resourceKey);
const type = imageResourceType(result.image);
const key = encodeURIComponent(result.image.resourceKey);
this.router.navigateByUrl(`resources/scada-symbols/${type}/${key}`);
} else {
this.updateData();

2
ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html

@ -17,7 +17,7 @@
-->
<form [formGroup]="uploadImageFormGroup" (ngSubmit)="upload()" style="width: 560px;">
<mat-toolbar color="primary">
<h2>{{ ( uploadImage ? (isScada ? 'scada.upload-symbol' : 'image.upload-image') : 'image.update-image' ) | translate }}</h2>
<h2>{{ ( uploadImage ? (isScada ? 'scada.upload-symbol' : 'image.upload-image') : (isScada ? 'scada.update-symbol' : 'image.update-image') ) | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"

31
ui-ngx/src/app/shared/components/image/upload-image-dialog.component.ts

@ -46,6 +46,11 @@ export interface UploadImageDialogData {
image?: ImageResourceInfo;
}
export interface UploadImageDialogResult {
image?: ImageResource;
scadaSymbolContent?: string;
}
@Component({
selector: 'tb-upload-image-dialog',
templateUrl: './upload-image-dialog.component.html',
@ -53,7 +58,7 @@ export interface UploadImageDialogData {
styleUrls: []
})
export class UploadImageDialogComponent extends
DialogComponent<UploadImageDialogComponent, ImageResource> implements OnInit, ErrorStateMatcher {
DialogComponent<UploadImageDialogComponent, UploadImageDialogResult> implements OnInit, ErrorStateMatcher {
uploadImageFormGroup: UntypedFormGroup;
@ -75,7 +80,7 @@ export class UploadImageDialogComponent extends
private imageService: ImageService,
@Inject(MAT_DIALOG_DATA) public data: UploadImageDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<UploadImageDialogComponent, ImageResource>,
public dialogRef: MatDialogRef<UploadImageDialogComponent, UploadImageDialogResult>,
public fb: UntypedFormBuilder) {
super(store, router, dialogRef);
}
@ -142,16 +147,22 @@ export class UploadImageDialogComponent extends
this.imageService.uploadImage(file, title, this.data.imageSubType),
blobToBase64(file)
]).subscribe(([imageInfo, base64]) => {
this.dialogRef.close(Object.assign(imageInfo, {base64}));
this.dialogRef.close({image: Object.assign(imageInfo, {base64})});
});
} else {
const image = this.data.image;
forkJoin([
this.imageService.updateImage(imageResourceType(image), image.resourceKey, file),
blobToBase64(file)
]).subscribe(([imageInfo, base64]) => {
this.dialogRef.close(Object.assign(imageInfo, {base64}));
});
if (this.isScada) {
blobToText(file).subscribe(scadaSymbolContent => {
this.dialogRef.close({scadaSymbolContent});
});
} else {
const image = this.data.image;
forkJoin([
this.imageService.updateImage(imageResourceType(image), image.resourceKey, file),
blobToBase64(file)
]).subscribe(([imageInfo, base64]) => {
this.dialogRef.close({image:Object.assign(imageInfo, {base64})});
});
}
}
}
}

Loading…
Cancel
Save