diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 897b7b86ec..1960aa0103 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -27,22 +27,22 @@ "src/assets", { "glob": "worker-html.js", - "input": "./node_modules/ace-builds/src-min/", + "input": "./node_modules/ace-builds/src-noconflict/", "output": "/" }, { "glob": "worker-css.js", - "input": "./node_modules/ace-builds/src-min/", + "input": "./node_modules/ace-builds/src-noconflict/", "output": "/" }, { "glob": "worker-json.js", - "input": "./node_modules/ace-builds/src-min/", + "input": "./node_modules/ace-builds/src-noconflict/", "output": "/" }, { "glob": "worker-javascript.js", - "input": "./node_modules/ace-builds/src-min/", + "input": "./node_modules/ace-builds/src-noconflict/", "output": "/" }, { @@ -102,24 +102,6 @@ "node_modules/js-beautify/js/lib/beautify.js", "node_modules/js-beautify/js/lib/beautify-css.js", "node_modules/js-beautify/js/lib/beautify-html.js", - "node_modules/ace-builds/src-min/ace.js", - "node_modules/ace-builds/src-min/ext-language_tools.js", - "node_modules/ace-builds/src-min/ext-searchbox.js", - "node_modules/ace-builds/src-min/theme-github.js", - "node_modules/ace-builds/src-min/mode-text.js", - "node_modules/ace-builds/src-min/mode-markdown.js", - "node_modules/ace-builds/src-min/mode-html.js", - "node_modules/ace-builds/src-min/mode-css.js", - "node_modules/ace-builds/src-min/mode-json.js", - "node_modules/ace-builds/src-min/mode-java.js", - "node_modules/ace-builds/src-min/mode-javascript.js", - "node_modules/ace-builds/src-min/snippets/text.js", - "node_modules/ace-builds/src-min/snippets/markdown.js", - "node_modules/ace-builds/src-min/snippets/html.js", - "node_modules/ace-builds/src-min/snippets/css.js", - "node_modules/ace-builds/src-min/snippets/json.js", - "node_modules/ace-builds/src-min/snippets/java.js", - "node_modules/ace-builds/src-min/snippets/javascript.js", "node_modules/systemjs/dist/system.js", "node_modules/jstree/dist/jstree.min.js" ], @@ -143,7 +125,11 @@ "hoist-non-react-statics", "classnames", "raf", - "moment-timezone" + "moment-timezone", + "tinycolor2", + "json-schema-defaults", + "leaflet-providers", + "lodash" ] }, "configurations": { diff --git a/ui-ngx/src/app/core/schema-utils.ts b/ui-ngx/src/app/core/schema-utils.ts index 97a525acd5..844ea9548f 100644 --- a/ui-ngx/src/app/core/schema-utils.ts +++ b/ui-ngx/src/app/core/schema-utils.ts @@ -69,16 +69,19 @@ export function addCondition(schema: JsonSettingsSchema, condition: string, excl return { key: element, condition - } + }; } if (typeof element === 'object') { if (element.condition) { - element.condition += ' && ' + condition + element.condition += ' && ' + condition; + } + else { + element.condition = condition; } - else element.condition = condition; } } return element; }); return schema; } + diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts index bdf7073e3e..15962cb9c0 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts @@ -19,10 +19,10 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ActionStatus, AuditLog } from '@shared/models/audit-log.models'; - -import * as ace from 'ace-builds'; +import { Ace } from 'ace-builds'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; +import { getAce } from '@shared/models/ace/ace.models'; export interface AuditLogDetailsDialogData { auditLog: AuditLog; @@ -37,11 +37,9 @@ export class AuditLogDetailsDialogComponent extends DialogComponent = { + let editorOptions: Partial = { mode: 'ace/mode/java', theme: 'ace/theme/github', showGutter: false, @@ -85,14 +83,17 @@ export class AuditLogDetailsDialogComponent extends DialogComponent { + const editor = ace.edit(editorElement, editorOptions); + editor.session.setUseWrapMode(false); + editor.setValue(content, -1); + this.updateEditorSize(editorElement, content, editor); + } + ); } - updateEditorSize(editorElement: any, content: string, editor: ace.Ace.Editor) { + updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) { let newHeight = 200; let newWidth = 600; if (content && content.length > 0) { diff --git a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts index e0359354a9..9ca56d53ef 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts @@ -19,10 +19,11 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import * as ace from 'ace-builds'; +import { Ace } from 'ace-builds'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { ContentType, contentTypesMap } from '@shared/models/constants'; +import { getAce } from '@shared/models/ace/ace.models'; export interface EventContentDialogData { content: string; @@ -39,7 +40,6 @@ export class EventContentDialogComponent extends DialogComponent = { + let editorOptions: Partial = { mode: `ace/mode/${mode}`, theme: 'ace/theme/github', showGutter: false, @@ -85,14 +85,17 @@ export class EventContentDialogComponent extends DialogComponent { + const editor = ace.edit(editorElement, editorOptions); + editor.session.setUseWrapMode(false); + editor.setValue(content, -1); + this.updateEditorSize(editorElement, content, editor); + } + ); } - updateEditorSize(editorElement: any, content: string, editor: ace.Ace.Editor) { + updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) { let newHeight = 400; let newWidth = 600; if (content && content.length > 0) { diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index b683c5cb17..9c61d18e05 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -220,7 +220,7 @@ export class KeyFilterDialogComponent extends let keyNameObservable: Observable>; switch (this.keyFilterFormGroup.get('key.type').value) { case EntityKeyType.ENTITY_FIELD: - keyNameObservable = of(Object.values(entityFields).map(entityField => entityField.keyName).sort()); + keyNameObservable = of(Object.keys(entityFields).map(itm => entityFields[itm]).map(entityField => entityField.keyName).sort()); break; case EntityKeyType.ATTRIBUTE: keyNameObservable = this.deviceProfileService.getDeviceProfileDevicesAttributesKeys( diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts index 54eab79b60..16c924b711 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts @@ -32,11 +32,15 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { CustomActionDescriptor } from '@shared/models/widget.models'; -import * as ace from 'ace-builds'; +import { Ace } from 'ace-builds'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { css_beautify, html_beautify } from 'js-beautify'; import { ResizeObserver } from '@juggle/resize-observer'; import { CustomPrettyActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { forkJoin, from } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; +import { getAce } from '@shared/models/ace/ace.models'; @Component({ selector: 'tb-custom-action-pretty-resources-tabs', @@ -64,11 +68,11 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl htmlFullscreen = false; cssFullscreen = false; - aceEditors: ace.Ace.Editor[] = []; + aceEditors: Ace.Editor[] = []; editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {}; aceResize$: ResizeObserver; - htmlEditor: ace.Ace.Editor; - cssEditor: ace.Ace.Editor; + htmlEditor: Ace.Editor; + cssEditor: Ace.Editor; setValuesPending = false; customPrettyActionEditorCompleter = CustomPrettyActionEditorCompleter; @@ -80,11 +84,12 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl } ngOnInit(): void { - this.initAceEditors(); - if (this.setValuesPending) { - this.setAceEditorValues(); - this.setValuesPending = false; - } + this.initAceEditors().subscribe(() => { + if (this.setValuesPending) { + this.setAceEditorValues(); + this.setValuesPending = false; + } + }); } ngOnDestroy(): void { @@ -94,7 +99,7 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl ngOnChanges(changes: SimpleChanges): void { for (const propName of Object.keys(changes)) { if (propName === 'action') { - if (this.aceEditors.length) { + if (this.aceEditors.length === 2) { this.setAceEditorValues(); } else { this.setValuesPending = true; @@ -152,34 +157,46 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl } } - private initAceEditors() { + private initAceEditors(): Observable { this.aceResize$ = new ResizeObserver((entries) => { entries.forEach((entry) => { const editor = this.aceEditors.find(aceEditor => aceEditor.container === entry.target); this.onAceEditorResize(editor); }); }); - this.htmlEditor = this.createAceEditor(this.htmlInputElmRef, 'html'); - this.htmlEditor.on('input', () => { - const editorValue = this.htmlEditor.getValue(); - if (this.action.customHtml !== editorValue) { - this.action.customHtml = editorValue; - this.notifyActionUpdated(); - } - }); - this.cssEditor = this.createAceEditor(this.cssInputElmRef, 'css'); - this.cssEditor.on('input', () => { - const editorValue = this.cssEditor.getValue(); - if (this.action.customCss !== editorValue) { - this.action.customCss = editorValue; - this.notifyActionUpdated(); - } - }); + const editorsObservables: Observable[] = []; + + editorsObservables.push(this.createAceEditor(this.htmlInputElmRef, 'html').pipe( + tap((editor) => { + this.htmlEditor = editor; + this.htmlEditor.on('input', () => { + const editorValue = this.htmlEditor.getValue(); + if (this.action.customHtml !== editorValue) { + this.action.customHtml = editorValue; + this.notifyActionUpdated(); + } + }); + }) + )); + + editorsObservables.push(this.createAceEditor(this.cssInputElmRef, 'css').pipe( + tap((editor) => { + this.cssEditor = editor; + this.cssEditor.on('input', () => { + const editorValue = this.cssEditor.getValue(); + if (this.action.customCss !== editorValue) { + this.action.customCss = editorValue; + this.notifyActionUpdated(); + } + }); + }) + )); + return forkJoin(editorsObservables); } - private createAceEditor(editorElementRef: ElementRef, mode: string): ace.Ace.Editor { + private createAceEditor(editorElementRef: ElementRef, mode: string): Observable { const editorElement = editorElementRef.nativeElement; - let editorOptions: Partial = { + let editorOptions: Partial = { mode: `ace/mode/${mode}`, showGutter: true, showPrintMargin: true @@ -190,11 +207,15 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl enableLiveAutocompletion: true }; editorOptions = {...editorOptions, ...advancedOptions}; - const aceEditor = ace.edit(editorElement, editorOptions); - aceEditor.session.setUseWrapMode(true); - this.aceEditors.push(aceEditor); - this.aceResize$.observe(editorElement); - return aceEditor; + return getAce().pipe( + map((ace) => { + const aceEditor = ace.edit(editorElement, editorOptions); + aceEditor.session.setUseWrapMode(true); + this.aceEditors.push(aceEditor); + this.aceResize$.observe(editorElement); + return aceEditor; + }) + ); } private setAceEditorValues() { @@ -202,7 +223,7 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); } - private onAceEditorResize(aceEditor: ace.Ace.Editor) { + private onAceEditorResize(aceEditor: Ace.Editor) { if (this.editorsResizeCafs[aceEditor.id]) { this.editorsResizeCafs[aceEditor.id](); delete this.editorsResizeCafs[aceEditor.id]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts index 542c196775..c3e9482ff9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts @@ -103,7 +103,7 @@ export class GatewayFormComponent extends PageComponent implements OnInit, OnDes gatewayType: string; gatewayConfigurationGroup: FormGroup; securityTypes = SecurityTypeTranslationMap; - gatewayLogLevels = Object.values(GatewayLogLevel); + gatewayLogLevels = Object.keys(GatewayLogLevel).map(itm => GatewayLogLevel[itm]); connectorTypes = Object.keys(ConnectorType); storageTypes = StorageTypeTranslationMap; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts new file mode 100644 index 0000000000..7d04b0b869 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts @@ -0,0 +1,445 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { FormattedData, MapProviders, ReplaceInfo } from '@home/components/widget/lib/maps/map-models'; +import { + createLabelFromDatasource, + hashCode, + isDefined, + isDefinedAndNotNull, isFunction, + isNumber, + isUndefined, + padValue +} from '@core/utils'; +import { Observable, Observer, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Datasource, DatasourceData } from '@shared/models/widget.models'; +import _ from 'lodash'; +import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes'; +import { addCondition, mergeSchemes } from '@core/schema-utils'; + +export function getProviderSchema(mapProvider: MapProviders, ignoreImageMap = false) { + const providerSchema = _.cloneDeep(mapProviderSchema); + if (mapProvider) { + providerSchema.schema.properties.provider.default = mapProvider; + } + if (ignoreImageMap) { + providerSchema.form[0].items = providerSchema.form[0]?.items.filter(item => item.value !== 'image-map'); + } + return mergeSchemes([providerSchema, + ...Object.keys(providerSets)?.map( + (key: string) => { + const setting = providerSets[key]; + return addCondition(setting?.schema, `model.provider === '${setting.name}'`); + })]); +} + +export function getRatio(firsMoment: number, secondMoment: number, intermediateMoment: number): number { + return (intermediateMoment - firsMoment) / (secondMoment - firsMoment); +} + +export function interpolateOnLineSegment( + pointA: FormattedData, + pointB: FormattedData, + latKeyName: string, + lngKeyName: string, + ratio: number +): { [key: string]: number } { + return { + [latKeyName]: (pointA[latKeyName] + (pointB[latKeyName] - pointA[latKeyName]) * ratio), + [lngKeyName]: (pointA[lngKeyName] + (pointB[lngKeyName] - pointA[lngKeyName]) * ratio) + }; +} + +export function findAngle(startPoint: FormattedData, endPoint: FormattedData, latKeyName: string, lngKeyName: string): number { + if (isUndefined(startPoint) || isUndefined(endPoint)) { + return 0; + } + let angle = -Math.atan2(endPoint[latKeyName] - startPoint[latKeyName], endPoint[lngKeyName] - startPoint[lngKeyName]); + angle = angle * 180 / Math.PI; + return parseInt(angle.toFixed(2), 10); +} + + +export function getDefCenterPosition(position) { + if (typeof (position) === 'string') { + return position.split(','); + } + if (typeof (position) === 'object') { + return position; + } + return [0, 0]; +} + + +const imageAspectMap = {}; + +function imageLoader(imageUrl: string): Observable { + return new Observable((observer: Observer) => { + const image = document.createElement('img'); // support IE + image.style.position = 'absolute'; + image.style.left = '-99999px'; + image.style.top = '-99999px'; + image.onload = () => { + observer.next(image); + document.body.removeChild(image); + observer.complete(); + }; + image.onerror = err => { + observer.error(err); + document.body.removeChild(image); + observer.complete(); + }; + document.body.appendChild(image); + image.src = imageUrl; + }); +} + +export function aspectCache(imageUrl: string): Observable { + if (imageUrl?.length) { + const hash = hashCode(imageUrl); + let aspect = imageAspectMap[hash]; + if (aspect) { + return of(aspect); + } + return imageLoader(imageUrl).pipe(map(image => { + aspect = image.width / image.height; + imageAspectMap[hash] = aspect; + return aspect; + })); + } +} + +export type TranslateFunc = (key: string, defaultTranslation?: string) => string; + +const varsRegex = /\${([^}]*)}/g; +const linkActionRegex = /([^<]*)<\/link-act>/g; +const buttonActionRegex = /([^<]*)<\/button-act>/g; + +function createLinkElement(actionName: string, actionText: string): string { + return `${actionText}`; +} + +function createButtonElement(actionName: string, actionText: string) { + return ``; +} + +function parseTemplate(template: string, data: { $datasource?: Datasource, [key: string]: any }, + translateFn?: TranslateFunc) { + let res = ''; + try { + if (translateFn) { + template = translateFn(template); + } + template = createLabelFromDatasource(data.$datasource, template); + + let match = /\${([^}]*)}/g.exec(template); + while (match !== null) { + const variable = match[0]; + let label = match[1]; + let valDec = 2; + const splitValues = label.split(':'); + if (splitValues.length > 1) { + label = splitValues[0]; + valDec = parseFloat(splitValues[1]); + } + + if (label.startsWith('#')) { + const keyIndexStr = label.substring(1); + const n = Math.floor(Number(keyIndexStr)); + if (String(n) === keyIndexStr && n >= 0) { + label = data.$datasource.dataKeys[n].label; + } + } + + const value = data[label] || ''; + let textValue: string; + if (isNumber(value)) { + textValue = padValue(value, valDec); + } else { + textValue = value; + } + template = template.replace(variable, textValue); + match = /\${([^}]*)}/g.exec(template); + } + + let actionTags: string; + let actionText: string; + let actionName: string; + let action: string; + + match = linkActionRegex.exec(template); + while (match !== null) { + [actionTags, actionName, actionText] = match; + action = createLinkElement(actionName, actionText); + template = template.replace(actionTags, action); + match = linkActionRegex.exec(template); + } + + match = buttonActionRegex.exec(template); + while (match !== null) { + [actionTags, actionName, actionText] = match; + action = createButtonElement(actionName, actionText); + template = template.replace(actionTags, action); + match = buttonActionRegex.exec(template); + } + + // const compiled = _.template(template); + // res = compiled(data); + res = template; + } catch (ex) { + console.log(ex, template); + } + return res; +} + +export function processPattern(template: string, data: { $datasource?: Datasource, [key: string]: any }): Array { + const replaceInfo = []; + try { + const reg = /\${([^}]*)}/g; + let match = reg.exec(template); + while (match !== null) { + const variableInfo: ReplaceInfo = { + dataKeyName: '', + valDec: 2, + variable: '' + }; + const variable = match[0]; + let label = match[1]; + let valDec = 2; + const splitValues = label.split(':'); + if (splitValues.length > 1) { + label = splitValues[0]; + valDec = parseFloat(splitValues[1]); + } + + variableInfo.variable = variable; + variableInfo.valDec = valDec; + + if (label.startsWith('#')) { + const keyIndexStr = label.substring(1); + const n = Math.floor(Number(keyIndexStr)); + if (String(n) === keyIndexStr && n >= 0) { + variableInfo.dataKeyName = data.$datasource.dataKeys[n].label; + } + } else { + variableInfo.dataKeyName = label; + } + replaceInfo.push(variableInfo); + + match = reg.exec(template); + } + } catch (ex) { + console.log(ex, template); + } + return replaceInfo; +} + +export function fillPattern(markerLabelText: string, replaceInfoLabelMarker: Array, data: FormattedData) { + let text = createLabelFromDatasource(data.$datasource, markerLabelText); + if (replaceInfoLabelMarker) { + for (const variableInfo of replaceInfoLabelMarker) { + let txtVal = ''; + if (variableInfo.dataKeyName && isDefinedAndNotNull(data[variableInfo.dataKeyName])) { + const varData = data[variableInfo.dataKeyName]; + if (isNumber(varData)) { + txtVal = padValue(varData, variableInfo.valDec); + } else { + txtVal = varData; + } + } + text = text.replace(variableInfo.variable, txtVal); + } + } + return text; +} + +function prepareProcessPattern(template: string, translateFn?: TranslateFunc): string { + if (translateFn) { + template = translateFn(template); + } + let actionTags: string; + let actionText: string; + let actionName: string; + let action: string; + + let match = linkActionRegex.exec(template); + while (match !== null) { + [actionTags, actionName, actionText] = match; + action = createLinkElement(actionName, actionText); + template = template.replace(actionTags, action); + match = linkActionRegex.exec(template); + } + + match = buttonActionRegex.exec(template); + while (match !== null) { + [actionTags, actionName, actionText] = match; + action = createButtonElement(actionName, actionText); + template = template.replace(actionTags, action); + match = buttonActionRegex.exec(template); + } + return template; +} + +export const parseWithTranslation = { + + translateFn: null, + + translate(key: string, defaultTranslation?: string): string { + if (this.translateFn) { + return this.translateFn(key, defaultTranslation); + } else { + throw console.error('Translate not assigned'); + } + }, + parseTemplate(template: string, data: object, forceTranslate = false): string { + return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this)); + }, + prepareProcessPattern(template: string, forceTranslate = false): string { + return prepareProcessPattern(forceTranslate ? this.translate(template) : template, this.translate.bind(this)); + }, + setTranslate(translateFn: TranslateFunc) { + this.translateFn = translateFn; + } +}; + +export function parseData(input: DatasourceData[]): FormattedData[] { + return _(input).groupBy(el => el?.datasource?.entityName) + .values().value().map((entityArray, i) => { + const obj: FormattedData = { + entityName: entityArray[0]?.datasource?.entityName, + entityId: entityArray[0]?.datasource?.entityId, + entityType: entityArray[0]?.datasource?.entityType, + $datasource: entityArray[0]?.datasource, + dsIndex: i, + deviceType: null + }; + entityArray.filter(el => el.data.length).forEach(el => { + const indexDate = el?.data?.length ? el.data.length - 1 : 0; + obj[el?.dataKey?.label] = el?.data[indexDate][1]; + obj[el?.dataKey?.label + '|ts'] = el?.data[indexDate][0]; + if (el?.dataKey?.label === 'type') { + obj.deviceType = el?.data[indexDate][1]; + } + }); + return obj; + }); +} + +export function parseArray(input: DatasourceData[]): FormattedData[][] { + return _(input).groupBy(el => el?.datasource?.entityName) + .values().value().map((entityArray) => + entityArray[0].data.map((el, i) => { + const obj: FormattedData = { + entityName: entityArray[0]?.datasource?.entityName, + entityId: entityArray[0]?.datasource?.entityId, + entityType: entityArray[0]?.datasource?.entityType, + $datasource: entityArray[0]?.datasource, + dsIndex: i, + time: el[0], + deviceType: null + }; + entityArray.filter(e => e.data.length && e.data[i]).forEach(entity => { + obj[entity?.dataKey?.label] = entity?.data[i][1]; + obj[entity?.dataKey?.label + '|ts'] = entity?.data[0][0]; + if (entity?.dataKey?.label === 'type') { + obj.deviceType = entity?.data[0][1]; + } + }); + return obj; + }) + ); +} + +export function parseFunction(source: any, params: string[] = ['def']): (...args: any[]) => any { + let res = null; + if (source?.length) { + try { + res = new Function(...params, source); + } + catch (err) { + res = null; + } + } + return res; +} + +export function safeExecute(func: (...args: any[]) => any, params = []) { + let res = null; + if (func && typeof (func) === 'function') { + try { + res = func(...params); + } + catch (err) { + console.log('error in external function:', err); + res = null; + } + } + return res; +} + +export function functionValueCalculator(useFunction: boolean, func: (...args: any[]) => any, params = [], defaultValue: any) { + let res; + if (useFunction && isDefined(func) && isFunction(func)) { + try { + res = func(...params); + if (!isDefinedAndNotNull(res) || res === '') { + res = defaultValue; + } + } catch (err) { + res = defaultValue; + console.log('error in external function:', err); + } + } else { + res = defaultValue; + } + return res; +} + +export function calculateNewPointCoordinate(coordinate: number, imageSize: number): number { + let pointCoordinate = coordinate / imageSize; + if (pointCoordinate < 0) { + pointCoordinate = 0; + } else if (pointCoordinate > 1) { + pointCoordinate = 1; + } + return pointCoordinate; +} + +export function createLoadingDiv(loadingText: string): JQuery { + return $(` +
+ ${loadingText} +
+ `); +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 5430aa2e69..54845c68c1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -43,12 +43,14 @@ import { Observable, of } from 'rxjs'; import { Polyline } from './polyline'; import { Polygon } from './polygon'; import { - createLoadingDiv, createTooltip, +} from '@home/components/widget/lib/maps/maps-utils'; +import { + createLoadingDiv, parseArray, parseData, safeExecute -} from '@home/components/widget/lib/maps/maps-utils'; +} from '@home/components/widget/lib/maps/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; import { deepClone, isDefinedAndNotNull, isEmptyStr, isString } from '@core/utils'; @@ -141,7 +143,7 @@ export default abstract class LeafletMap { const header = document.createElement('p'); header.appendChild(document.createTextNode('Select entity:')); header.setAttribute('style', 'font-size: 14px; margin: 8px 0'); - datasourcesList.append(header); + datasourcesList.appendChild(header); this.datasources.forEach(ds => { const dsItem = document.createElement('p'); dsItem.appendChild(document.createTextNode(ds.entityName)); @@ -158,9 +160,9 @@ export default abstract class LeafletMap { this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options); }); }; - datasourcesList.append(dsItem); + datasourcesList.appendChild(dsItem); }); - datasourcesList.append(document.createElement('br')); + datasourcesList.appendChild(document.createElement('br')); const deleteBtn = document.createElement('a'); deleteBtn.appendChild(document.createTextNode('Discard changes')); deleteBtn.onclick = () => { @@ -170,7 +172,7 @@ export default abstract class LeafletMap { this.addMarkers.splice(markerIndex, 1); } }; - datasourcesList.append(deleteBtn); + datasourcesList.appendChild(deleteBtn); const popup = L.popup(); popup.setContent(datasourcesList); newMarker.bindPopup(popup).openPopup(); @@ -222,7 +224,7 @@ export default abstract class LeafletMap { const header = document.createElement('p'); header.appendChild(document.createTextNode('Select entity:')); header.setAttribute('style', 'font-size: 14px; margin: 8px 0'); - datasourcesList.append(header); + datasourcesList.appendChild(header); this.datasources.forEach(ds => { const dsItem = document.createElement('p'); dsItem.appendChild(document.createTextNode(ds.entityName)); @@ -238,9 +240,9 @@ export default abstract class LeafletMap { this.deletePolygon(ds.entityName); }); }; - datasourcesList.append(dsItem); + datasourcesList.appendChild(dsItem); }); - datasourcesList.append(document.createElement('br')); + datasourcesList.appendChild(document.createElement('br')); const deleteBtn = document.createElement('a'); deleteBtn.appendChild(document.createTextNode('Discard changes')); deleteBtn.onclick = () => { @@ -250,7 +252,7 @@ export default abstract class LeafletMap { this.addPolygons.splice(polygonIndex, 1); } }; - datasourcesList.append(deleteBtn); + datasourcesList.appendChild(deleteBtn); const popup = L.popup(); popup.setContent(datasourcesList); newPolygon.bindPopup(popup).openPopup(); @@ -289,7 +291,7 @@ export default abstract class LeafletMap { if (!this.loadingDiv) { this.loadingDiv = createLoadingDiv(this.ctx.translate.instant('common.loading')); } - this.$container.append(this.loadingDiv[0]); + this.$container.appendChild(this.loadingDiv[0]); } else { if (this.loadingDiv) { this.loadingDiv.remove(); @@ -609,14 +611,13 @@ export default abstract class LeafletMap { } updatePoints(pointsData: FormattedData[][], getTooltip: (point: FormattedData) => string) { - if(pointsData.length) { + if (pointsData.length) { if (this.points) { this.map.removeLayer(this.points); } this.points = new FeatureGroup(); } - for(let i = 0; i < pointsData.length; i++) { - const pointsList = pointsData[i]; + for (const pointsList of pointsData) { pointsList.filter(pdata => !!this.convertPosition(pdata)).forEach(data => { const point = L.circleMarker(this.convertPosition(data), { color: this.options.pointColor, @@ -630,7 +631,7 @@ export default abstract class LeafletMap { this.points.addLayer(point); }); } - if(pointsData.length) { + if (pointsData.length) { this.map.addLayer(this.points); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index ed238dcfe2..6f0e629c54 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -14,7 +14,6 @@ /// limitations under the License. /// -import { LatLngTuple } from 'leaflet'; import { Datasource } from '@app/shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import tinycolor from 'tinycolor2'; @@ -47,7 +46,7 @@ export type MapSettings = { provider?: MapProviders; credentials?: any; // declare credentials format gmApiKey?: string; - defaultCenterPosition?: LatLngTuple; + defaultCenterPosition?: [number, number]; markerClusteringSetting?; useDefaultCenterPosition?: boolean; gmDefaultMapType?: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts index d321e04953..5d3c7f64a3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts @@ -15,9 +15,10 @@ /// import { JsonSettingsSchema } from '@shared/models/widget.models'; -import { MapProviders } from './map-models'; +import { MapProviders } from '@home/components/widget/lib/maps/map-models'; export interface MapWidgetInterface { + map?: any; resize(); update(); onInit(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index 950e73e96d..327db2ff02 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -31,20 +31,25 @@ import { routeMapSettingsSchema } from './schemes'; import { MapWidgetInterface, MapWidgetStaticInterface } from './map-widget.interface'; -import { addCondition, addGroupInfo, addToSchema, initSchema, mergeSchemes } from '@core/schema-utils'; +import { + addCondition, + addGroupInfo, + addToSchema, + initSchema, + mergeSchemes +} from '@core/schema-utils'; import { WidgetContext } from '@app/modules/home/models/widget-component.models'; -import { getDefCenterPosition, parseFunction, parseWithTranslation } from './maps-utils'; +import { getDefCenterPosition, getProviderSchema, parseFunction, parseWithTranslation } from './common-maps-utils'; import { Datasource, DatasourceData, JsonSettingsSchema, WidgetActionDescriptor } from '@shared/models/widget.models'; import { EntityId } from '@shared/models/id/entity-id'; import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; import { AttributeService } from '@core/http/attribute.service'; import { TranslateService } from '@ngx-translate/core'; import { UtilsService } from '@core/services/utils.service'; -import _ from 'lodash'; import { EntityDataPageLink } from '@shared/models/query/query.models'; import { isDefined } from '@core/utils'; import { forkJoin, Observable, of } from 'rxjs'; -import { providerSets } from '@home/components/widget/lib/maps/providers'; +import { providerClass } from '@home/components/widget/lib/maps/providers'; // @dynamic export class MapWidgetController implements MapWidgetInterface { @@ -70,7 +75,7 @@ export class MapWidgetController implements MapWidgetInterface { this.settings.markerClick = this.getDescriptors('markerClick'); this.settings.polygonClick = this.getDescriptors('polygonClick'); - const MapClass = providerSets[this.provider]?.MapClass; + const MapClass = providerClass[this.provider]; if (!MapClass) { return; } @@ -101,19 +106,7 @@ export class MapWidgetController implements MapWidgetInterface { } public static getProvidersSchema(mapProvider: MapProviders, ignoreImageMap = false) { - const providerSchema = _.cloneDeep(mapProviderSchema); - if (mapProvider) { - providerSchema.schema.properties.provider.default = mapProvider; - } - if (ignoreImageMap) { - providerSchema.form[0].items = providerSchema.form[0]?.items.filter(item => item.value !== 'image-map'); - } - return mergeSchemes([providerSchema, - ...Object.keys(providerSets)?.map( - (key: string) => { - const setting = providerSets[key]; - return addCondition(setting?.schema, `model.provider === '${setting.name}'`); - })]); + return getProviderSchema(mapProvider, ignoreImageMap); } public static settingsSchema(mapProvider: MapProviders, drawRoutes: boolean): JsonSettingsSchema { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts index e9f1f5dadd..5e275030f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts @@ -15,20 +15,8 @@ /// import L from 'leaflet'; -import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings, ReplaceInfo } from './map-models'; -import { Datasource, DatasourceData } from '@app/shared/models/widget.models'; -import _ from 'lodash'; -import { Observable, Observer, of } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { - createLabelFromDatasource, - hashCode, - isDefined, - isDefinedAndNotNull, isFunction, - isNumber, - isUndefined, - padValue -} from '@core/utils'; +import { MarkerSettings, PolygonSettings, PolylineSettings } from './map-models'; +import { Datasource } from '@app/shared/models/widget.models'; export function createTooltip(target: L.Layer, settings: MarkerSettings | PolylineSettings | PolygonSettings, @@ -68,400 +56,3 @@ export function bindPopupActions(popup: L.Popup, settings: MarkerSettings | Poly } }); } - -export function getRatio(firsMoment: number, secondMoment: number, intermediateMoment: number): number { - return (intermediateMoment - firsMoment) / (secondMoment - firsMoment); -} - -export function interpolateOnLineSegment( - pointA: FormattedData, - pointB: FormattedData, - latKeyName: string, - lngKeyName: string, - ratio: number -): { [key: string]: number } { - return { - [latKeyName]: (pointA[latKeyName] + (pointB[latKeyName] - pointA[latKeyName]) * ratio), - [lngKeyName]: (pointA[lngKeyName] + (pointB[lngKeyName] - pointA[lngKeyName]) * ratio) - }; -} - -export function findAngle(startPoint: FormattedData, endPoint: FormattedData, latKeyName: string, lngKeyName: string): number { - if (isUndefined(startPoint) || isUndefined(endPoint)) { - return 0; - } - let angle = -Math.atan2(endPoint[latKeyName] - startPoint[latKeyName], endPoint[lngKeyName] - startPoint[lngKeyName]); - angle = angle * 180 / Math.PI; - return parseInt(angle.toFixed(2), 10); -} - - -export function getDefCenterPosition(position) { - if (typeof (position) === 'string') { - return position.split(','); - } - if (typeof (position) === 'object') { - return position; - } - return [0, 0]; -} - - -const imageAspectMap = {}; - -function imageLoader(imageUrl: string): Observable { - return new Observable((observer: Observer) => { - const image = document.createElement('img'); // support IE - image.style.position = 'absolute'; - image.style.left = '-99999px'; - image.style.top = '-99999px'; - image.onload = () => { - observer.next(image); - document.body.removeChild(image); - observer.complete(); - }; - image.onerror = err => { - observer.error(err); - document.body.removeChild(image); - observer.complete(); - }; - document.body.appendChild(image); - image.src = imageUrl; - }); -} - -export function aspectCache(imageUrl: string): Observable { - if (imageUrl?.length) { - const hash = hashCode(imageUrl); - let aspect = imageAspectMap[hash]; - if (aspect) { - return of(aspect); - } - return imageLoader(imageUrl).pipe(map(image => { - aspect = image.width / image.height; - imageAspectMap[hash] = aspect; - return aspect; - })); - } -} - -export type TranslateFunc = (key: string, defaultTranslation?: string) => string; - -const varsRegex = /\${([^}]*)}/g; -const linkActionRegex = /([^<]*)<\/link-act>/g; -const buttonActionRegex = /([^<]*)<\/button-act>/g; - -function createLinkElement(actionName: string, actionText: string): string { - return `${actionText}`; -} - -function createButtonElement(actionName: string, actionText: string) { - return ``; -} - -function parseTemplate(template: string, data: { $datasource?: Datasource, [key: string]: any }, - translateFn?: TranslateFunc) { - let res = ''; - try { - if (translateFn) { - template = translateFn(template); - } - template = createLabelFromDatasource(data.$datasource, template); - - let match = /\${([^}]*)}/g.exec(template); - while (match !== null) { - const variable = match[0]; - let label = match[1]; - let valDec = 2; - const splitValues = label.split(':'); - if (splitValues.length > 1) { - label = splitValues[0]; - valDec = parseFloat(splitValues[1]); - } - - if (label.startsWith('#')) { - const keyIndexStr = label.substring(1); - const n = Math.floor(Number(keyIndexStr)); - if (String(n) === keyIndexStr && n >= 0) { - label = data.$datasource.dataKeys[n].label; - } - } - - const value = data[label] || ''; - let textValue: string; - if (isNumber(value)) { - textValue = padValue(value, valDec); - } else { - textValue = value; - } - template = template.replace(variable, textValue); - match = /\${([^}]*)}/g.exec(template); - } - - let actionTags: string; - let actionText: string; - let actionName: string; - let action: string; - - match = linkActionRegex.exec(template); - while (match !== null) { - [actionTags, actionName, actionText] = match; - action = createLinkElement(actionName, actionText); - template = template.replace(actionTags, action); - match = linkActionRegex.exec(template); - } - - match = buttonActionRegex.exec(template); - while (match !== null) { - [actionTags, actionName, actionText] = match; - action = createButtonElement(actionName, actionText); - template = template.replace(actionTags, action); - match = buttonActionRegex.exec(template); - } - - // const compiled = _.template(template); - // res = compiled(data); - res = template; - } catch (ex) { - console.log(ex, template); - } - return res; -} - -export function processPattern(template: string, data: { $datasource?: Datasource, [key: string]: any }): Array { - const replaceInfo = []; - try { - const reg = /\${([^}]*)}/g; - let match = reg.exec(template); - while (match !== null) { - const variableInfo: ReplaceInfo = { - dataKeyName: '', - valDec: 2, - variable: '' - }; - const variable = match[0]; - let label = match[1]; - let valDec = 2; - const splitValues = label.split(':'); - if (splitValues.length > 1) { - label = splitValues[0]; - valDec = parseFloat(splitValues[1]); - } - - variableInfo.variable = variable; - variableInfo.valDec = valDec; - - if (label.startsWith('#')) { - const keyIndexStr = label.substring(1); - const n = Math.floor(Number(keyIndexStr)); - if (String(n) === keyIndexStr && n >= 0) { - variableInfo.dataKeyName = data.$datasource.dataKeys[n].label; - } - } else { - variableInfo.dataKeyName = label; - } - replaceInfo.push(variableInfo); - - match = reg.exec(template); - } - } catch (ex) { - console.log(ex, template); - } - return replaceInfo; -} - -export function fillPattern(markerLabelText: string, replaceInfoLabelMarker: Array, data: FormattedData) { - let text = createLabelFromDatasource(data.$datasource, markerLabelText); - if (replaceInfoLabelMarker) { - for (const variableInfo of replaceInfoLabelMarker) { - let txtVal = ''; - if (variableInfo.dataKeyName && isDefinedAndNotNull(data[variableInfo.dataKeyName])) { - const varData = data[variableInfo.dataKeyName]; - if (isNumber(varData)) { - txtVal = padValue(varData, variableInfo.valDec); - } else { - txtVal = varData; - } - } - text = text.replace(variableInfo.variable, txtVal); - } - } - return text; -} - -function prepareProcessPattern(template: string, translateFn?: TranslateFunc): string { - if (translateFn) { - template = translateFn(template); - } - let actionTags: string; - let actionText: string; - let actionName: string; - let action: string; - - let match = linkActionRegex.exec(template); - while (match !== null) { - [actionTags, actionName, actionText] = match; - action = createLinkElement(actionName, actionText); - template = template.replace(actionTags, action); - match = linkActionRegex.exec(template); - } - - match = buttonActionRegex.exec(template); - while (match !== null) { - [actionTags, actionName, actionText] = match; - action = createButtonElement(actionName, actionText); - template = template.replace(actionTags, action); - match = buttonActionRegex.exec(template); - } - return template; -} - -export const parseWithTranslation = { - - translateFn: null, - - translate(key: string, defaultTranslation?: string): string { - if (this.translateFn) { - return this.translateFn(key, defaultTranslation); - } else { - throw console.error('Translate not assigned'); - } - }, - parseTemplate(template: string, data: object, forceTranslate = false): string { - return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this)); - }, - prepareProcessPattern(template: string, forceTranslate = false): string { - return prepareProcessPattern(forceTranslate ? this.translate(template) : template, this.translate.bind(this)); - }, - setTranslate(translateFn: TranslateFunc) { - this.translateFn = translateFn; - } -}; - -export function parseData(input: DatasourceData[]): FormattedData[] { - return _(input).groupBy(el => el?.datasource?.entityName) - .values().value().map((entityArray, i) => { - const obj: FormattedData = { - entityName: entityArray[0]?.datasource?.entityName, - entityId: entityArray[0]?.datasource?.entityId, - entityType: entityArray[0]?.datasource?.entityType, - $datasource: entityArray[0]?.datasource, - dsIndex: i, - deviceType: null - }; - entityArray.filter(el => el.data.length).forEach(el => { - const indexDate = el?.data?.length ? el.data.length - 1 : 0; - obj[el?.dataKey?.label] = el?.data[indexDate][1]; - obj[el?.dataKey?.label + '|ts'] = el?.data[indexDate][0]; - if (el?.dataKey?.label === 'type') { - obj.deviceType = el?.data[indexDate][1]; - } - }); - return obj; - }); -} - -export function parseArray(input: DatasourceData[]): FormattedData[][] { - return _(input).groupBy(el => el?.datasource?.entityName) - .values().value().map((entityArray) => - entityArray[0].data.map((el, i) => { - const obj: FormattedData = { - entityName: entityArray[0]?.datasource?.entityName, - entityId: entityArray[0]?.datasource?.entityId, - entityType: entityArray[0]?.datasource?.entityType, - $datasource: entityArray[0]?.datasource, - dsIndex: i, - time: el[0], - deviceType: null - }; - entityArray.filter(e => e.data.length && e.data[i]).forEach(entity => { - obj[entity?.dataKey?.label] = entity?.data[i][1]; - obj[entity?.dataKey?.label + '|ts'] = entity?.data[0][0]; - if (entity?.dataKey?.label === 'type') { - obj.deviceType = entity?.data[0][1]; - } - }); - return obj; - }) - ); -} - -export function parseFunction(source: any, params: string[] = ['def']): (...args: any[]) => any { - let res = null; - if (source?.length) { - try { - res = new Function(...params, source); - } - catch (err) { - res = null; - } - } - return res; -} - -export function safeExecute(func: (...args: any[]) => any, params = []) { - let res = null; - if (func && typeof (func) === 'function') { - try { - res = func(...params); - } - catch (err) { - console.log('error in external function:', err); - res = null; - } - } - return res; -} - -export function functionValueCalculator(useFunction: boolean, func: (...args: any[]) => any, params = [], defaultValue: any) { - let res; - if (useFunction && isDefined(func) && isFunction(func)) { - try { - res = func(...params); - if (!isDefinedAndNotNull(res) || res === '') { - res = defaultValue; - } - } catch (err) { - res = defaultValue; - console.log('error in external function:', err); - } - } else { - res = defaultValue; - } - return res; -} - -export function calculateNewPointCoordinate(coordinate: number, imageSize: number): number { - let pointCoordinate = coordinate / imageSize; - if (pointCoordinate < 0) { - pointCoordinate = 0; - } else if (pointCoordinate > 1) { - pointCoordinate = 1; - } - return pointCoordinate; -} - -export function createLoadingDiv(loadingText: string): JQuery { - return $(` -
- ${loadingText} -
- `); -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index 5f9e5e8362..d98ad94591 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -17,14 +17,16 @@ import L, { Icon, LeafletMouseEvent } from 'leaflet'; import { FormattedData, MarkerSettings } from './map-models'; import { - aspectCache, bindPopupActions, createTooltip, +} from './maps-utils'; +import { + aspectCache, fillPattern, parseWithTranslation, processPattern, safeExecute -} from './maps-utils'; +} from './common-maps-utils'; import tinycolor from 'tinycolor2'; import { isDefined, isDefinedAndNotNull } from '@core/utils'; import LeafletMap from './leaflet-map'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts index dc2361a5fc..1b04e15842 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts @@ -15,7 +15,8 @@ /// import L, { LatLngExpression, LeafletMouseEvent } from 'leaflet'; -import { createTooltip, functionValueCalculator, parseWithTranslation, safeExecute } from './maps-utils'; +import { createTooltip } from './maps-utils'; +import { functionValueCalculator, parseWithTranslation, safeExecute } from './common-maps-utils'; import 'leaflet-editable/src/Leaflet.Editable'; import { FormattedData, PolygonSettings } from './map-models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts index 8c4997933c..318217270b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts @@ -18,7 +18,7 @@ import L, { PolylineDecoratorOptions } from 'leaflet'; import 'leaflet-polylinedecorator'; import { FormattedData, PolylineSettings } from './map-models'; -import { functionValueCalculator, safeExecute } from '@home/components/widget/lib/maps/maps-utils'; +import { functionValueCalculator } from '@home/components/widget/lib/maps/common-maps-utils'; export class Polyline { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts index 825ace1227..7c00e9d557 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts @@ -19,7 +19,7 @@ import LeafletMap from '../leaflet-map'; import { MapImage, PosFuncton, UnitedMapSettings } from '../map-models'; import { Observable, ReplaySubject } from 'rxjs'; import { filter, map, mergeMap } from 'rxjs/operators'; -import { aspectCache, calculateNewPointCoordinate, parseFunction } from '@home/components/widget/lib/maps/maps-utils'; +import { aspectCache, calculateNewPointCoordinate, parseFunction } from '@home/components/widget/lib/maps/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; import { DataSet, DatasourceType, widgetType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts index 9c6bb63ddf..41778405b7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts @@ -14,11 +14,6 @@ /// limitations under the License. /// -import { - googleMapSettingsSchema, hereMapSettingsSchema, imageMapSettingsSchema, - openstreetMapSettingsSchema, - tencentMapSettingsSchema -} from '@home/components/widget/lib/maps/schemes'; import { OpenStreetMap } from './openstreet-map'; import { TencentMap } from './tencent-map'; import { GoogleMap } from './google-map'; @@ -26,38 +21,11 @@ import { HEREMap } from './here-map'; import { ImageMap } from './image-map'; import { Type } from '@angular/core'; import LeafletMap from '@home/components/widget/lib/maps/leaflet-map'; -import { JsonSettingsSchema } from '@shared/models/widget.models'; -interface IProvider { - MapClass: Type; - schema: JsonSettingsSchema; - name: string; -} - -export const providerSets: { [key: string]: IProvider } = { - 'openstreet-map': { - MapClass: OpenStreetMap, - schema: openstreetMapSettingsSchema, - name: 'openstreet-map' - }, - 'tencent-map': { - MapClass: TencentMap, - schema: tencentMapSettingsSchema, - name: 'tencent-map' - }, - 'google-map': { - MapClass: GoogleMap, - schema: googleMapSettingsSchema, - name: 'google-map' - }, - here: { - MapClass: HEREMap, - schema: hereMapSettingsSchema, - name: 'here' - }, - 'image-map': { - MapClass: ImageMap, - schema: imageMapSettingsSchema, - name: 'image-map' - } +export const providerClass: { [key: string]: Type } = { + 'openstreet-map': OpenStreetMap, + 'tencent-map': TencentMap, + 'google-map': GoogleMap, + here: HEREMap, + 'image-map': ImageMap }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts index 3506978c4a..e9f831be68 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts @@ -14,6 +14,8 @@ /// limitations under the License. /// +import { JsonSettingsSchema } from '@shared/models/widget.models'; + export const googleMapSettingsSchema = { schema: { @@ -1096,3 +1098,31 @@ export const tripAnimationSchema = { ] }] }; + +interface IProvider { + schema: JsonSettingsSchema; + name: string; +} + +export const providerSets: { [key: string]: IProvider } = { + 'openstreet-map': { + schema: openstreetMapSettingsSchema, + name: 'openstreet-map' + }, + 'tencent-map': { + schema: tencentMapSettingsSchema, + name: 'tencent-map' + }, + 'google-map': { + schema: googleMapSettingsSchema, + name: 'google-map' + }, + here: { + schema: hereMapSettingsSchema, + name: 'here' + }, + 'image-map': { + schema: imageMapSettingsSchema, + name: 'image-map' + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts index f5f48c86c1..68eb5d08e6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts @@ -21,7 +21,7 @@ import { UtilsService } from '@core/services/utils.service'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { isDefined, isNumber } from '@core/utils'; -import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; +import { CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; import * as tinycolor_ from 'tinycolor2'; import { ResizeObserver } from '@juggle/resize-observer'; import GenericOptions = CanvasGauges.GenericOptions; @@ -102,7 +102,7 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { private textMeasure: JQuery; private canvasBarElement: HTMLElement; - private canvasBar: CanvasDigitalGauge; + private canvasBar: any; // CanvasDigitalGauge; private knobResize$: ResizeObserver; @@ -165,7 +165,6 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { animation: false }; - this.canvasBar = new CanvasDigitalGauge(canvasBarData).draw(); this.knob.on('click', (e) => { if (this.moving) { @@ -277,9 +276,6 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { }); - const initialValue = isDefined(settings.initialValue) ? settings.initialValue : this.minValue; - this.setValue(initialValue); - const subscription = this.ctx.defaultSubscription; const rpcEnabled = subscription.rpcEnabled; @@ -297,13 +293,22 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { if (settings.setValueMethod && settings.setValueMethod.length) { this.setValueMethod = settings.setValueMethod; } - if (!rpcEnabled) { - this.onError('Target device is not set!'); - } else { - if (!this.isSimulated) { - this.rpcRequestValue(); + + import('@home/components/widget/lib/canvas-digital-gauge').then( + (gauge) => { + this.canvasBar = new gauge.CanvasDigitalGauge(canvasBarData).draw(); + const initialValue = isDefined(settings.initialValue) ? settings.initialValue : this.minValue; + this.setValue(initialValue); + if (!rpcEnabled) { + this.onError('Target device is not set!'); + } else { + if (!this.isSimulated) { + this.rpcRequestValue(); + } + } } - } + ); + } private degreeToRatio(degree: number): number { @@ -328,7 +333,9 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { const height = this.knobContainer.height(); const size = Math.min(width, height); this.knob.css({width: size, height: size}); - this.canvasBar.update({width: size, height: size} as GenericOptions); + if (this.canvasBar) { + this.canvasBar.update({width: size, height: size} as GenericOptions); + } this.setFontSize(this.knobTitle, this.title, this.knobTitleContainer.height(), this.knobTitleContainer.width()); this.setFontSize(this.knobError, this.error, this.knobErrorContainer.height(), this.knobErrorContainer.width()); const minmaxHeight = this.knobMinmaxContainer.height(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 46e6d7d51e..f9393a9e79 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -546,7 +546,7 @@ class TimeseriesDatasource implements DataSource { } } } else { - rows = Object.values(rowsMap); + rows = Object.keys(rowsMap).map(itm => rowsMap[itm]); } return rows; } diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts index 8de3269bb0..5af3b5c5b7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts @@ -27,28 +27,28 @@ import { SecurityContext, ViewChild } from '@angular/core'; -import { MapWidgetController, TbMapWidgetV2 } from '../lib/maps/map-widget2'; -import { FormattedData, MapProviders, TripAnimationSettings } from '../lib/maps/map-models'; +import { FormattedData, MapProviders, TripAnimationSettings } from '@home/components/widget/lib/maps/map-models'; import { addCondition, addGroupInfo, addToSchema, initSchema } from '@app/core/schema-utils'; -import { mapPolygonSchema, pathSchema, pointSchema, tripAnimationSchema } from '../lib/maps/schemes'; +import { mapPolygonSchema, pathSchema, pointSchema, tripAnimationSchema } from '@home/components/widget/lib/maps/schemes'; import { DomSanitizer } from '@angular/platform-browser'; import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { - findAngle, + findAngle, getProviderSchema, getRatio, interpolateOnLineSegment, parseArray, parseFunction, parseWithTranslation, safeExecute -} from '../lib/maps/maps-utils'; +} from '@home/components/widget/lib/maps/common-maps-utils'; import { JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models'; import moment from 'moment'; import { isUndefined } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; +import { MapWidgetInterface } from '@home/components/widget/lib/maps/map-widget.interface'; -interface dataMap { - [key: string] : FormattedData +interface DataMap { + [key: string]: FormattedData; } @Component({ @@ -67,7 +67,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy @ViewChild('map') mapContainer; - mapWidget: MapWidgetController; + mapWidget: MapWidgetInterface; historicalData: FormattedData[][]; normalizationStep: number; interpolatedTimeData = []; @@ -85,7 +85,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy static getSettingsSchema(): JsonSettingsSchema { const schema = initSchema(); - addToSchema(schema, TbMapWidgetV2.getProvidersSchema(null, true)); + addToSchema(schema, getProviderSchema(null, true)); addGroupInfo(schema, 'Map Provider Settings'); addToSchema(schema, tripAnimationSchema); addGroupInfo(schema, 'Trip Animation Settings'); @@ -106,7 +106,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy buttonColor: tinycolor(this.widgetConfig.color).setAlpha(0.54).toRgbString(), disabledButtonColor: tinycolor(this.widgetConfig.color).setAlpha(0.3).toRgbString(), rotationAngle: 0 - } + }; this.settings = { ...settings, ...this.ctx.settings }; this.useAnchors = this.settings.showPoints && this.settings.usePointAsAnchor; this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']); @@ -122,15 +122,19 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy } this.mapWidget.map.map?.invalidateSize(); this.cd.detectChanges(); - } + }; } ngAfterViewInit() { - this.mapWidget = new MapWidgetController(MapProviders.openstreet, false, this.ctx, this.mapContainer.nativeElement); - this.mapResize$ = new ResizeObserver(() => { - this.mapWidget.resize(); - }); - this.mapResize$.observe(this.mapContainer.nativeElement); + import('@home/components/widget/lib/maps/map-widget2').then( + (mod) => { + this.mapWidget = new mod.MapWidgetController(MapProviders.openstreet, false, this.ctx, this.mapContainer.nativeElement); + this.mapResize$ = new ResizeObserver(() => { + this.mapWidget.resize(); + }); + this.mapResize$.observe(this.mapContainer.nativeElement); + } + ); } ngOnDestroy() { @@ -142,8 +146,8 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy timeUpdated(time: number) { this.currentTime = time; const currentPosition = this.interpolatedTimeData - .map(dataSource => dataSource[time]) - for(let j = 0; j < this.interpolatedTimeData.length; j++) { + .map(dataSource => dataSource[time]); + for (let j = 0; j < this.interpolatedTimeData.length; j++) { if (isUndefined(currentPosition[j])) { const timePoints = Object.keys(this.interpolatedTimeData[j]).map(item => parseInt(item, 10)); for (let i = 1; i < timePoints.length; i++) { @@ -155,13 +159,13 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy ...beforePosition, time, ...interpolateOnLineSegment(beforePosition, afterPosition, this.settings.latKeyName, this.settings.lngKeyName, ratio) - } + }; break; } } } } - for(let j = 0; j < this.interpolatedTimeData.length; j++) { + for (let j = 0; j < this.interpolatedTimeData.length; j++) { if (isUndefined(currentPosition[j])) { currentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time); } @@ -179,7 +183,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy } this.mapWidget.map.updateMarkers(currentPosition, true, (trip) => { this.activeTrip = trip; - this.timeUpdated(this.currentTime) + this.timeUpdated(this.currentTime); }); } } @@ -187,14 +191,14 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy setActiveTrip() { } - private calculateLastPoints(dataSource: dataMap, time: number): FormattedData { + private calculateLastPoints(dataSource: DataMap, time: number): FormattedData { const timeArr = Object.keys(dataSource); - let index = timeArr.findIndex((dtime, index) => { + let index = timeArr.findIndex((dtime) => { return Number(dtime) >= time; }); - if(index !== -1) { - if(Number(timeArr[index]) !== time && index !== 0) { + if (index !== -1) { + if (Number(timeArr[index]) !== time && index !== 0) { index--; } } else { @@ -210,7 +214,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy this.maxTime = dataSource[dataSource.length - 1]?.time || -Infinity; this.interpolatedTimeData[index] = this.interpolateArray(dataSource); }); - if(!this.activeTrip){ + if (!this.activeTrip) { this.activeTrip = this.interpolatedTimeData.map(dataSource => dataSource[this.minTime]).filter(ds => ds)[0]; } if (this.useAnchors) { @@ -230,7 +234,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy private calcMainTooltip(points: FormattedData[]): void { const tooltips = []; - for (let point of points) { + for (const point of points) { tooltips.push(this.sanitizer.sanitize(SecurityContext.HTML, this.calcTooltip(point))); } this.mainTooltips = tooltips; @@ -259,7 +263,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy } const timeStamp = Object.keys(result); for (let i = 0; i < timeStamp.length - 1; i++) { - result[timeStamp[i]].rotationAngle += findAngle(result[timeStamp[i]], result[timeStamp[i + 1]], latKeyName, lngKeyName) + result[timeStamp[i]].rotationAngle += findAngle(result[timeStamp[i]], result[timeStamp[i + 1]], latKeyName, lngKeyName); } return result; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 04dc845450..01ebb4b39c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -17,7 +17,7 @@ import { Inject, Injectable, Optional, Type } from '@angular/core'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { WidgetService } from '@core/http/widget.service'; -import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; +import { forkJoin, from, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; import { ErrorWidgetType, MissingWidgetType, @@ -30,7 +30,7 @@ import cssjs from '@core/css/css'; import { UtilsService } from '@core/services/utils.service'; import { ResourcesService } from '@core/services/resources.service'; import { Widget, widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models'; -import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; +import { catchError, map, mergeMap, switchMap, tap } from 'rxjs/operators'; import { isFunction, isUndefined } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; @@ -42,6 +42,10 @@ import { WidgetTypeId } from '@app/shared/models/id/widget-type-id'; import { TenantId } from '@app/shared/models/id/tenant-id'; import { SharedModule } from '@shared/shared.module'; import { MODULES_MAP } from '@shared/public-api'; +import * as tinycolor_ from 'tinycolor2'; +import moment from 'moment'; + +const tinycolor = tinycolor_; // @dynamic @Injectable() @@ -106,20 +110,77 @@ export class WidgetComponentService { } const initSubject = new ReplaySubject(); this.init$ = initSubject.asObservable(); - const loadDefaultWidgetInfoTasks = [ - this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule, WidgetComponentsModule]), - this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule, WidgetComponentsModule]), - ]; - forkJoin(loadDefaultWidgetInfoTasks).subscribe( + + (window as any).tinycolor = tinycolor; + (window as any).cssjs = cssjs; + (window as any).moment = moment; + + const widgetModulesTasks: Observable[] = []; + widgetModulesTasks.push(from(import('@home/components/widget/lib/flot-widget')).pipe( + tap((mod) => { + (window as any).TbFlot = mod.TbFlot; + })) + ); + widgetModulesTasks.push(from(import('@home/components/widget/lib/analogue-compass')).pipe( + tap((mod) => { + (window as any).TbAnalogueCompass = mod.TbAnalogueCompass; + })) + ); + widgetModulesTasks.push(from(import('@home/components/widget/lib/analogue-radial-gauge')).pipe( + tap((mod) => { + (window as any).TbAnalogueRadialGauge = mod.TbAnalogueRadialGauge; + })) + ); + widgetModulesTasks.push(from(import('@home/components/widget/lib/analogue-linear-gauge')).pipe( + tap((mod) => { + (window as any).TbAnalogueLinearGauge = mod.TbAnalogueLinearGauge; + })) + ); + widgetModulesTasks.push(from(import('@home/components/widget/lib/digital-gauge')).pipe( + tap((mod) => { + (window as any).TbCanvasDigitalGauge = mod.TbCanvasDigitalGauge; + })) + ); + widgetModulesTasks.push(from(import('@home/components/widget/lib/maps/map-widget2')).pipe( + tap((mod) => { + (window as any).TbMapWidgetV2 = mod.TbMapWidgetV2; + })) + ); + widgetModulesTasks.push(from(import('@home/components/widget/trip-animation/trip-animation.component')).pipe( + tap((mod) => { + (window as any).TbTripAnimationWidget = mod.TbTripAnimationWidget; + })) + ); + + forkJoin(widgetModulesTasks).subscribe( () => { - initSubject.next(); + const loadDefaultWidgetInfoTasks = [ + this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule, WidgetComponentsModule]), + this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule, WidgetComponentsModule]), + ]; + forkJoin(loadDefaultWidgetInfoTasks).subscribe( + () => { + initSubject.next(); + }, + (e) => { + let errorMessages = ['Failed to load default widget types!']; + if (e && e.length) { + errorMessages = errorMessages.concat(e); + } + console.error('Failed to load default widget types!'); + initSubject.error({ + widgetInfo: this.errorWidgetType, + errorMessages + }); + } + ); }, (e) => { - let errorMessages = ['Failed to load default widget types!']; + let errorMessages = ['Failed to load widget modules!']; if (e && e.length) { errorMessages = errorMessages.concat(e); } - console.error('Failed to load default widget types!'); + console.error('Failed to load widget modules!'); initSubject.error({ widgetInfo: this.errorWidgetType, errorMessages diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index 905efe2adb..46338a2dfe 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts @@ -32,7 +32,8 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { Hotkey } from 'angular2-hotkeys'; import { TranslateService } from '@ngx-translate/core'; import { getCurrentIsLoading } from '@app/core/interceptors/load.selectors'; -import * as ace from 'ace-builds'; +import { Ace } from 'ace-builds'; +import { getAce, Range } from '@shared/models/ace/ace.models'; import { css_beautify, html_beautify } from 'js-beautify'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { WINDOW } from '@core/services/window.service'; @@ -44,10 +45,12 @@ import { SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult } from '@home/pages/widget/save-widget-type-as-dialog.component'; -import { Subscription } from 'rxjs'; +import { forkJoin, from, Subscription } from 'rxjs'; import { ResizeObserver } from '@juggle/resize-observer'; import Timeout = NodeJS.Timeout; import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { map, tap } from 'rxjs/operators'; // @dynamic @Component({ @@ -119,13 +122,13 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe javascriptFullscreen = false; iFrameFullscreen = false; - aceEditors: ace.Ace.Editor[] = []; + aceEditors: Ace.Editor[] = []; editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {}; - htmlEditor: ace.Ace.Editor; - cssEditor: ace.Ace.Editor; - jsonSettingsEditor: ace.Ace.Editor; - dataKeyJsonSettingsEditor: ace.Ace.Editor; - jsEditor: ace.Ace.Editor; + htmlEditor: Ace.Editor; + cssEditor: Ace.Editor; + jsonSettingsEditor: Ace.Editor; + dataKeyJsonSettingsEditor: Ace.Editor; + jsEditor: Ace.Editor; aceResize$: ResizeObserver; onWindowMessageListener = this.onWindowMessage.bind(this); @@ -277,51 +280,85 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe this.onAceEditorResize(editor); }); }); - this.htmlEditor = this.createAceEditor(this.htmlInputElmRef, 'html'); - this.htmlEditor.on('input', () => { - const editorValue = this.htmlEditor.getValue(); - if (this.widget.templateHtml !== editorValue) { - this.widget.templateHtml = editorValue; - this.isDirty = true; + + const editorsObservables: Observable[] = []; + + + editorsObservables.push(this.createAceEditor(this.htmlInputElmRef, 'html').pipe( + tap((editor) => { + this.htmlEditor = editor; + this.htmlEditor.on('input', () => { + const editorValue = this.htmlEditor.getValue(); + if (this.widget.templateHtml !== editorValue) { + this.widget.templateHtml = editorValue; + this.isDirty = true; + } + }); + }) + )); + + editorsObservables.push(this.createAceEditor(this.cssInputElmRef, 'css').pipe( + tap((editor) => { + this.cssEditor = editor; + this.cssEditor.on('input', () => { + const editorValue = this.cssEditor.getValue(); + if (this.widget.templateCss !== editorValue) { + this.widget.templateCss = editorValue; + this.isDirty = true; + } + }); + }) + )); + + editorsObservables.push(this.createAceEditor(this.settingsJsonInputElmRef, 'json').pipe( + tap((editor) => { + this.jsonSettingsEditor = editor; + this.jsonSettingsEditor.on('input', () => { + const editorValue = this.jsonSettingsEditor.getValue(); + if (this.widget.settingsSchema !== editorValue) { + this.widget.settingsSchema = editorValue; + this.isDirty = true; + } + }); + }) + )); + + editorsObservables.push(this.createAceEditor(this.dataKeySettingsJsonInputElmRef, 'json').pipe( + tap((editor) => { + this.dataKeyJsonSettingsEditor = editor; + this.dataKeyJsonSettingsEditor.on('input', () => { + const editorValue = this.dataKeyJsonSettingsEditor.getValue(); + if (this.widget.dataKeySettingsSchema !== editorValue) { + this.widget.dataKeySettingsSchema = editorValue; + this.isDirty = true; + } + }); + }) + )); + + editorsObservables.push(this.createAceEditor(this.javascriptInputElmRef, 'javascript').pipe( + tap((editor) => { + this.jsEditor = editor; + this.jsEditor.on('input', () => { + const editorValue = this.jsEditor.getValue(); + if (this.widget.controllerScript !== editorValue) { + this.widget.controllerScript = editorValue; + this.isDirty = true; + } + }); + this.jsEditor.on('change', () => { + this.cleanupJsErrors(); + }); + this.jsEditor.completers = [widgetEditorCompleter, ...(this.jsEditor.completers || [])]; + }) + )); + + forkJoin(editorsObservables).subscribe( + () => { + this.setAceEditorValues(); } - }); - this.cssEditor = this.createAceEditor(this.cssInputElmRef, 'css'); - this.cssEditor.on('input', () => { - const editorValue = this.cssEditor.getValue(); - if (this.widget.templateCss !== editorValue) { - this.widget.templateCss = editorValue; - this.isDirty = true; - } - }); - this.jsonSettingsEditor = this.createAceEditor(this.settingsJsonInputElmRef, 'json'); - this.jsonSettingsEditor.on('input', () => { - const editorValue = this.jsonSettingsEditor.getValue(); - if (this.widget.settingsSchema !== editorValue) { - this.widget.settingsSchema = editorValue; - this.isDirty = true; - } - }); - this.dataKeyJsonSettingsEditor = this.createAceEditor(this.dataKeySettingsJsonInputElmRef, 'json'); - this.dataKeyJsonSettingsEditor.on('input', () => { - const editorValue = this.dataKeyJsonSettingsEditor.getValue(); - if (this.widget.dataKeySettingsSchema !== editorValue) { - this.widget.dataKeySettingsSchema = editorValue; - this.isDirty = true; - } - }); - this.jsEditor = this.createAceEditor(this.javascriptInputElmRef, 'javascript'); - this.jsEditor.on('input', () => { - const editorValue = this.jsEditor.getValue(); - if (this.widget.controllerScript !== editorValue) { - this.widget.controllerScript = editorValue; - this.isDirty = true; - } - }); - this.jsEditor.on('change', () => { - this.cleanupJsErrors(); - }); - this.jsEditor.completers = [widgetEditorCompleter, ...(this.jsEditor.completers || [])]; - this.setAceEditorValues(); + ); + } private setAceEditorValues() { @@ -332,9 +369,9 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe this.jsEditor.setValue(this.widget.controllerScript ? this.widget.controllerScript : '', -1); } - private createAceEditor(editorElementRef: ElementRef, mode: string): ace.Ace.Editor { + private createAceEditor(editorElementRef: ElementRef, mode: string): Observable { const editorElement = editorElementRef.nativeElement; - let editorOptions: Partial = { + let editorOptions: Partial = { mode: `ace/mode/${mode}`, showGutter: true, showPrintMargin: true @@ -345,14 +382,18 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe enableLiveAutocompletion: true }; editorOptions = {...editorOptions, ...advancedOptions}; - const aceEditor = ace.edit(editorElement, editorOptions); - aceEditor.session.setUseWrapMode(true); - this.aceEditors.push(aceEditor); - this.aceResize$.observe(editorElement); - return aceEditor; + return getAce().pipe( + map((ace) => { + const aceEditor = ace.edit(editorElement, editorOptions); + aceEditor.session.setUseWrapMode(true); + this.aceEditors.push(aceEditor); + this.aceResize$.observe(editorElement); + return aceEditor; + }) + ); } - private onAceEditorResize(aceEditor: ace.Ace.Editor) { + private onAceEditorResize(aceEditor: Ace.Editor) { if (this.editorsResizeCafs[aceEditor.id]) { this.editorsResizeCafs[aceEditor.id](); delete this.editorsResizeCafs[aceEditor.id]; @@ -443,11 +484,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe if (details.columnNumber) { column = details.columnNumber; } - const errorMarkerId = this.jsEditor.session.addMarker(new ace.Range(line, 0, line, Infinity), + const errorMarkerId = this.jsEditor.session.addMarker(new Range(line, 0, line, Infinity), 'ace_active-line', 'screenLine'); this.errorMarkers.push(errorMarkerId); const annotations = this.jsEditor.session.getAnnotations(); - const errorAnnotation: ace.Ace.Annotation = { + const errorAnnotation: Ace.Annotation = { row: line, column, text: details.message, diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 5515ba055f..0fa827e4b7 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -25,7 +25,8 @@ import { ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; -import * as ace from 'ace-builds'; +import { Ace } from 'ace-builds'; +import { getAce, Range } from '@shared/models/ace/ace.models'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; import { Store } from '@ngrx/store'; @@ -60,7 +61,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, @ViewChild('javascriptEditor', {static: true}) javascriptEditorElmRef: ElementRef; - private jsEditor: ace.Ace.Editor; + private jsEditor: Ace.Editor; private editorsResizeCaf: CancelAnimationFrame; private editorResize$: ResizeObserver; private ignoreChange = false; @@ -136,7 +137,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, }); } const editorElement = this.javascriptEditorElmRef.nativeElement; - let editorOptions: Partial = { + let editorOptions: Partial = { mode: 'ace/mode/javascript', showGutter: true, showPrintMargin: true, @@ -150,22 +151,27 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, }; editorOptions = {...editorOptions, ...advancedOptions}; - this.jsEditor = ace.edit(editorElement, editorOptions); - this.jsEditor.session.setUseWrapMode(true); - this.jsEditor.setValue(this.modelValue ? this.modelValue : '', -1); - this.jsEditor.on('change', () => { - if (!this.ignoreChange) { - this.cleanupJsErrors(); - this.updateView(); + getAce().subscribe( + (ace) => { + this.jsEditor = ace.edit(editorElement, editorOptions); + this.jsEditor.session.setUseWrapMode(true); + this.jsEditor.setValue(this.modelValue ? this.modelValue : '', -1); + this.jsEditor.setReadOnly(this.disabled); + this.jsEditor.on('change', () => { + if (!this.ignoreChange) { + this.cleanupJsErrors(); + this.updateView(); + } + }); + if (this.editorCompleter) { + this.jsEditor.completers = [this.editorCompleter, ...(this.jsEditor.completers || [])]; + } + this.editorResize$ = new ResizeObserver(() => { + this.onAceEditorResize(); + }); + this.editorResize$.observe(editorElement); } - }); - if (this.editorCompleter) { - this.jsEditor.completers = [this.editorCompleter, ...(this.jsEditor.completers || [])]; - } - this.editorResize$ = new ResizeObserver(() => { - this.onAceEditorResize(); - }); - this.editorResize$.observe(editorElement); + ); } ngOnDestroy(): void { @@ -294,11 +300,11 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, if (details.columnNumber) { column = details.columnNumber; } - const errorMarkerId = this.jsEditor.session.addMarker(new ace.Range(line, 0, line, Infinity), + const errorMarkerId = this.jsEditor.session.addMarker(new Range(line, 0, line, Infinity), 'ace_active-line', 'screenLine'); this.errorMarkers.push(errorMarkerId); const annotations = this.jsEditor.session.getAnnotations(); - const errorAnnotation: ace.Ace.Annotation = { + const errorAnnotation: Ace.Annotation = { row: line, column, text: details.message, diff --git a/ui-ngx/src/app/shared/components/json-content.component.ts b/ui-ngx/src/app/shared/components/json-content.component.ts index 3339863cd2..cbe5368a8a 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.ts +++ b/ui-ngx/src/app/shared/components/json-content.component.ts @@ -26,7 +26,7 @@ import { ViewChild } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; -import * as ace from 'ace-builds'; +import { Ace } from 'ace-builds'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; import { Store } from '@ngrx/store'; @@ -35,6 +35,7 @@ import { ContentType, contentTypesMap } from '@shared/models/constants'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { guid } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; +import { getAce } from '@shared/models/ace/ace.models'; @Component({ selector: 'tb-json-content', @@ -58,7 +59,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid @ViewChild('jsonEditor', {static: true}) jsonEditorElmRef: ElementRef; - private jsonEditor: ace.Ace.Editor; + private jsonEditor: Ace.Editor; private editorsResizeCaf: CancelAnimationFrame; private editorResize$: ResizeObserver; private ignoreChange = false; @@ -123,7 +124,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid if (this.contentType) { mode = contentTypesMap.get(this.contentType).code; } - let editorOptions: Partial = { + let editorOptions: Partial = { mode: `ace/mode/${mode}`, showGutter: true, showPrintMargin: false, @@ -137,22 +138,27 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid }; editorOptions = {...editorOptions, ...advancedOptions}; - this.jsonEditor = ace.edit(editorElement, editorOptions); - this.jsonEditor.session.setUseWrapMode(true); - this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1); - this.jsonEditor.on('change', () => { - if (!this.ignoreChange) { - this.cleanupJsonErrors(); - this.updateView(); + getAce().subscribe( + (ace) => { + this.jsonEditor = ace.edit(editorElement, editorOptions); + this.jsonEditor.session.setUseWrapMode(true); + this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1); + this.jsonEditor.setReadOnly(this.disabled || this.readonly); + this.jsonEditor.on('change', () => { + if (!this.ignoreChange) { + this.cleanupJsonErrors(); + this.updateView(); + } + }); + this.jsonEditor.on('blur', () => { + this.contentValid = !this.validateContent || this.doValidate(true); + }); + this.editorResize$ = new ResizeObserver(() => { + this.onAceEditorResize(); + }); + this.editorResize$.observe(editorElement); } - }); - this.jsonEditor.on('blur', () => { - this.contentValid = !this.validateContent || this.doValidate(true); - }); - this.editorResize$ = new ResizeObserver(() => { - this.onAceEditorResize(); - }); - this.editorResize$.observe(editorElement); + ); } ngOnDestroy(): void { diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx index 7a631623a0..ae2e112601 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx @@ -16,10 +16,20 @@ import * as React from 'react'; import ThingsboardBaseComponent from './json-form-base-component'; import reactCSS from 'reactcss'; -import ReactAce from 'react-ace'; import Button from '@material-ui/core/Button'; import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; import { IEditorProps } from 'react-ace/src/types'; +import { map, mergeMap } from 'rxjs/operators'; +import { loadAceDependencies } from '@shared/models/ace/ace.models'; +import { from } from 'rxjs'; + +const ReactAce = React.lazy(() => { + return loadAceDependencies().pipe( + mergeMap(() => { + return from(import('react-ace')); + }) + ).toPromise(); +}); interface ThingsboardAceEditorProps extends JsonFormFieldProps { mode: string; @@ -153,22 +163,24 @@ class ThingsboardAceEditor extends React.Component - + Loading...}> + +
{this.props.error}
diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.ts b/ui-ngx/src/app/shared/components/json-object-edit.component.ts index 503b12c1a8..8913f309e4 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.ts +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.ts @@ -16,7 +16,7 @@ import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; -import * as ace from 'ace-builds'; +import { Ace } from 'ace-builds'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; import { Store } from '@ngrx/store'; @@ -24,6 +24,7 @@ import { AppState } from '@core/core.state'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { guid } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; +import { getAce } from '@shared/models/ace/ace.models'; @Component({ selector: 'tb-json-object-edit', @@ -47,7 +48,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va @ViewChild('jsonEditor', {static: true}) jsonEditorElmRef: ElementRef; - private jsonEditor: ace.Ace.Editor; + private jsonEditor: Ace.Editor; private editorsResizeCaf: CancelAnimationFrame; private editorResize$: ResizeObserver; @@ -102,7 +103,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va ngOnInit(): void { const editorElement = this.jsonEditorElmRef.nativeElement; - let editorOptions: Partial = { + let editorOptions: Partial = { mode: 'ace/mode/json', showGutter: true, showPrintMargin: false, @@ -116,19 +117,24 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va }; editorOptions = {...editorOptions, ...advancedOptions}; - this.jsonEditor = ace.edit(editorElement, editorOptions); - this.jsonEditor.session.setUseWrapMode(false); - this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1); - this.jsonEditor.on('change', () => { - if (!this.ignoreChange) { - this.cleanupJsonErrors(); - this.updateView(); + getAce().subscribe( + (ace) => { + this.jsonEditor = ace.edit(editorElement, editorOptions); + this.jsonEditor.session.setUseWrapMode(false); + this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1); + this.jsonEditor.setReadOnly(this.disabled || this.readonly); + this.jsonEditor.on('change', () => { + if (!this.ignoreChange) { + this.cleanupJsonErrors(); + this.updateView(); + } + }); + this.editorResize$ = new ResizeObserver(() => { + this.onAceEditorResize(); + }); + this.editorResize$.observe(editorElement); } - }); - this.editorResize$ = new ResizeObserver(() => { - this.onAceEditorResize(); - }); - this.editorResize$.observe(editorElement); + ); } ngOnDestroy(): void { diff --git a/ui-ngx/src/app/shared/models/ace/ace.models.ts b/ui-ngx/src/app/shared/models/ace/ace.models.ts new file mode 100644 index 0000000000..17a39e493f --- /dev/null +++ b/ui-ngx/src/app/shared/models/ace/ace.models.ts @@ -0,0 +1,327 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Ace } from 'ace-builds'; +import { Observable } from 'rxjs/internal/Observable'; +import { forkJoin, from, of } from 'rxjs'; +import { map, mergeMap, tap } from 'rxjs/operators'; + +let aceDependenciesLoaded = false; +let aceModule: any; + +export function loadAceDependencies(): Observable { + if (aceDependenciesLoaded) { + return of(null); + } else { + const aceObservables: Observable[] = []; + aceObservables.push(from(import('ace-builds/src-noconflict/ext-language_tools'))); + aceObservables.push(from(import('ace-builds/src-noconflict/ext-searchbox'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-java'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-css'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-json'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-javascript'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-text'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-html'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/javascript'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html'))); + aceObservables.push(from(import('ace-builds/src-noconflict/theme-github'))); + return forkJoin(aceObservables).pipe( + tap(() => { + aceDependenciesLoaded = true; + }) + ); + } +} + +export function getAce(): Observable { + if (aceModule) { + return of(aceModule); + } else { + return from(import('ace')).pipe( + mergeMap((module) => { + return loadAceDependencies().pipe( + map(() => module) + ); + }), + tap((module) => { + aceModule = module; + }) + ); + } +} + +export class Range implements Ace.Range { + + public start: Ace.Point; + public end: Ace.Point; + + constructor(startRow: number, startColumn: number, endRow: number, endColumn: number) { + this.start = { + row: startRow, + column: startColumn + }; + + this.end = { + row: endRow, + column: endColumn + }; + } + + static fromPoints(start: Ace.Point, end: Ace.Point): Ace.Range { + return new Range(start.row, start.column, end.row, end.column); + } + + clipRows(firstRow: number, lastRow: number): Ace.Range { + let end: Ace.Point; + let start: Ace.Point; + if (this.end.row > lastRow) { + end = {row: lastRow + 1, column: 0}; + } else if (this.end.row < firstRow) { + end = {row: firstRow, column: 0}; + } + + if (this.start.row > lastRow) { + start = {row: lastRow + 1, column: 0}; + } else if (this.start.row < firstRow) { + start = {row: firstRow, column: 0}; + } + return Range.fromPoints(start || this.start, end || this.end); + } + + clone(): Ace.Range { + return Range.fromPoints(this.start, this.end); + } + + collapseRows(): Ace.Range { + if (this.end.column === 0) { + return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row - 1), 0); + } else { + return new Range(this.start.row, 0, this.end.row, 0); + } + } + + compare(row: number, column: number): number { + if (!this.isMultiLine()) { + if (row === this.start.row) { + return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0); + } + } + + if (row < this.start.row) { + return -1; + } + + if (row > this.end.row) { + return 1; + } + + if (this.start.row === row) { + return column >= this.start.column ? 0 : -1; + } + + if (this.end.row === row) { + return column <= this.end.column ? 0 : 1; + } + + return 0; + } + + compareEnd(row: number, column: number): number { + if (this.end.row === row && this.end.column === column) { + return 1; + } else { + return this.compare(row, column); + } + } + + compareInside(row: number, column: number): number { + if (this.end.row === row && this.end.column === column) { + return 1; + } else if (this.start.row === row && this.start.column === column) { + return -1; + } else { + return this.compare(row, column); + } + } + + comparePoint(p: Ace.Point): number { + return this.compare(p.row, p.column); + } + + compareRange(range: Ace.Range): number { + let cmp: number; + const end = range.end; + const start = range.start; + + cmp = this.compare(end.row, end.column); + if (cmp === 1) { + cmp = this.compare(start.row, start.column); + if (cmp === 1) { + return 2; + } else if (cmp === 0) { + return 1; + } else { + return 0; + } + } else if (cmp === -1) { + return -2; + } else { + cmp = this.compare(start.row, start.column); + if (cmp === -1) { + return -1; + } else if (cmp === 1) { + return 42; + } else { + return 0; + } + } + } + + compareStart(row: number, column: number): number { + if (this.start.row === row && this.start.column === column) { + return -1; + } else { + return this.compare(row, column); + } + } + + contains(row: number, column: number): boolean { + return this.compare(row, column) === 0; + } + + containsRange(range: Ace.Range): boolean { + return this.comparePoint(range.start) === 0 && this.comparePoint(range.end) === 0; + } + + extend(row: number, column: number): Ace.Range { + const cmp = this.compare(row, column); + let end: Ace.Point; + let start: Ace.Point; + if (cmp === 0) { + return this; + } else if (cmp === -1) { + start = {row, column}; + } else { + end = {row, column}; + } + return Range.fromPoints(start || this.start, end || this.end); + } + + inside(row: number, column: number): boolean { + if (this.compare(row, column) === 0) { + if (this.isEnd(row, column) || this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + } + + insideEnd(row: number, column: number): boolean { + if (this.compare(row, column) === 0) { + if (this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + } + + insideStart(row: number, column: number): boolean { + if (this.compare(row, column) === 0) { + if (this.isEnd(row, column)) { + return false; + } else { + return true; + } + } + return false; + } + + intersects(range: Ace.Range): boolean { + const cmp = this.compareRange(range); + return (cmp === -1 || cmp === 0 || cmp === 1); + } + + isEmpty(): boolean { + return (this.start.row === this.end.row && this.start.column === this.end.column); + } + + isEnd(row: number, column: number): boolean { + return this.end.row === row && this.end.column === column; + } + + isEqual(range: Ace.Range): boolean { + return this.start.row === range.start.row && + this.end.row === range.end.row && + this.start.column === range.start.column && + this.end.column === range.end.column; + } + + isMultiLine(): boolean { + return (this.start.row !== this.end.row); + } + + isStart(row: number, column: number): boolean { + return this.start.row === row && this.start.column === column; + } + + moveBy(row: number, column: number): void { + this.start.row += row; + this.start.column += column; + this.end.row += row; + this.end.column += column; + } + + setEnd(row: number, column: number): void { + if (typeof row === 'object') { + this.end.column = (row as Ace.Point).column; + this.end.row = (row as Ace.Point).row; + } else { + this.end.row = row; + this.end.column = column; + } + } + + setStart(row: number, column: number): void { + if (typeof row === 'object') { + this.start.column = (row as Ace.Point).column; + this.start.row = (row as Ace.Point).row; + } else { + this.start.row = row; + this.start.column = column; + } + } + + toScreenRange(session: Ace.EditSession): Ace.Range { + const screenPosStart = session.documentToScreenPosition(this.start); + const screenPosEnd = session.documentToScreenPosition(this.end); + + return new Range( + screenPosStart.row, screenPosStart.column, + screenPosEnd.row, screenPosEnd.column + ); + } + +} diff --git a/ui-ngx/src/app/shared/models/ace/completion.models.ts b/ui-ngx/src/app/shared/models/ace/completion.models.ts index f2810781bb..30b65ab3b8 100644 --- a/ui-ngx/src/app/shared/models/ace/completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/completion.models.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import * as ace from 'ace-builds'; +import { Ace } from 'ace-builds'; export type tbMetaType = 'object' | 'function' | 'service' | 'property' | 'argument'; @@ -41,7 +41,7 @@ export interface TbEditorCompletion { children?: TbEditorCompletions; } -interface TbEditorAceCompletion extends ace.Ace.Completion { +interface TbEditorAceCompletion extends Ace.Completion { isTbEditorAceCompletion: true; snippet: string; description?: string; @@ -50,7 +50,7 @@ interface TbEditorAceCompletion extends ace.Ace.Completion { return?: FunctionArgType; } -export class TbEditorCompleter implements ace.Ace.Completer { +export class TbEditorCompleter implements Ace.Completer { identifierRegexps: RegExp[] = [ /[a-zA-Z_0-9\$\-\u00A2-\u2000\u2070-\uFFFF.]/ @@ -59,8 +59,8 @@ export class TbEditorCompleter implements ace.Ace.Completer { constructor(private editorCompletions: TbEditorCompletions) { } - getCompletions(editor: ace.Ace.Editor, session: ace.Ace.EditSession, - position: ace.Ace.Point, prefix: string, callback: ace.Ace.CompleterCallback): void { + getCompletions(editor: Ace.Editor, session: Ace.EditSession, + position: Ace.Point, prefix: string, callback: Ace.CompleterCallback): void { const result = this.prepareCompletions(prefix); if (result) { callback(null, result); @@ -91,7 +91,7 @@ export class TbEditorCompleter implements ace.Ace.Completer { return parts; } - private prepareCompletions(prefix: string): ace.Ace.Completion[] { + private prepareCompletions(prefix: string): Ace.Completion[] { const path = this.resolvePath(prefix); if (path !== null) { return this.toAceCompletionsList(this.editorCompletions, path); @@ -100,8 +100,8 @@ export class TbEditorCompleter implements ace.Ace.Completer { } } - private toAceCompletionsList(completions: TbEditorCompletions, parentPath: string[]): ace.Ace.Completion[] { - const result: ace.Ace.Completion[] = []; + private toAceCompletionsList(completions: TbEditorCompletions, parentPath: string[]): Ace.Completion[] { + const result: Ace.Completion[] = []; let targetCompletions = completions; let parentPrefix = ''; if (parentPath.length) { @@ -116,7 +116,7 @@ export class TbEditorCompleter implements ace.Ace.Completer { return result; } - private toAceCompletion(name: string, completion: TbEditorCompletion, parentPrefix: string): ace.Ace.Completion { + private toAceCompletion(name: string, completion: TbEditorCompletion, parentPrefix: string): Ace.Completion { const aceCompletion: TbEditorAceCompletion = { isTbEditorAceCompletion: true, snippet: parentPrefix + name, @@ -175,7 +175,7 @@ export class TbEditorCompleter implements ace.Ace.Completer { if (completion.args || completion.return) { let functionInfoBlock = '
'; if (completion.args) { - functionInfoBlock += '
Parameters
' + functionInfoBlock += '
Parameters
'; let argsTable = ''; const strArgs: string[] = []; for (const arg of completion.args) { @@ -185,7 +185,7 @@ export class TbEditorCompleter implements ace.Ace.Completer { } strArg += '
'; if (arg.type) { - strArg += `${arg.type}` + strArg += `${arg.type}`; } strArg += ''; if (arg.description) { diff --git a/ui-ngx/src/polyfills.ts b/ui-ngx/src/polyfills.ts index 68a59ba5c3..84b6419149 100644 --- a/ui-ngx/src/polyfills.ts +++ b/ui-ngx/src/polyfills.ts @@ -75,7 +75,6 @@ import './zone-flags'; import 'zone.js/dist/zone'; // Included with Angular CLI. import 'core-js/es/array'; -import moment from 'moment'; /*************************************************************************************************** * APPLICATION IMPORTS @@ -87,26 +86,4 @@ import moment from 'moment'; * WIDGETS IMPORTS */ -import cssjs from '@core/css/css'; -import { TbFlot } from '@home/components/widget/lib/flot-widget'; -import { TbAnalogueCompass } from '@home/components/widget/lib/analogue-compass'; -import { TbAnalogueRadialGauge } from '@home/components/widget/lib/analogue-radial-gauge'; -import { TbAnalogueLinearGauge } from '@home/components/widget/lib/analogue-linear-gauge'; -import { TbCanvasDigitalGauge } from '@home/components/widget/lib/digital-gauge'; -import { TbMapWidgetV2 } from '@home/components/widget/lib/maps/map-widget2'; -import { TbTripAnimationWidget } from '@app/modules/home/components/widget/trip-animation/trip-animation.component'; - -import * as tinycolor_ from 'tinycolor2'; - -const tinycolor = tinycolor_; - -(window as any).tinycolor = tinycolor; -(window as any).cssjs = cssjs; -(window as any).moment = moment; -(window as any).TbFlot = TbFlot; -(window as any).TbAnalogueCompass = TbAnalogueCompass; -(window as any).TbAnalogueRadialGauge = TbAnalogueRadialGauge; -(window as any).TbAnalogueLinearGauge = TbAnalogueLinearGauge; -(window as any).TbCanvasDigitalGauge = TbCanvasDigitalGauge; -(window as any).TbMapWidgetV2 = TbMapWidgetV2; -(window as any).TbTripAnimationWidget = TbTripAnimationWidget; +(window as any).GAUGES_NO_AUTO_INIT = true; diff --git a/ui-ngx/src/typings/add-marker.d.ts b/ui-ngx/src/typings/add-marker.d.ts index 565ecdddd4..d1c2f872e0 100644 --- a/ui-ngx/src/typings/add-marker.d.ts +++ b/ui-ngx/src/typings/add-marker.d.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import * as L from 'leaflet' +import * as L from 'leaflet'; declare module 'leaflet' { diff --git a/ui-ngx/tsconfig.json b/ui-ngx/tsconfig.json index b814f9ff19..baaa5cdb3a 100644 --- a/ui-ngx/tsconfig.json +++ b/ui-ngx/tsconfig.json @@ -34,6 +34,9 @@ "@home/*": ["src/app/modules/home/*"], "jszip": [ "node_modules/jszip/dist/jszip.min.js" + ], + "ace": [ + "node_modules/ace-builds/src-noconflict/ace.js" ] }, "lib": [