Browse Source

Merge branch 'develop/3.0' of https://github.com/thingsboard/thingsboard into map/3.0

pull/2535/head
Artem Halushko 6 years ago
parent
commit
7b61107144
  1. 122
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  2. 86
      ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts
  3. 97
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts
  4. 243
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts
  5. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  6. 74
      ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.html
  7. 74
      ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.scss
  8. 274
      ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.ts
  9. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  10. 14
      ui-ngx/src/app/shared/components/image-input.component.html
  11. 4
      ui-ngx/src/app/shared/components/image-input.component.scss
  12. 38
      ui-ngx/src/app/shared/components/image-input.component.ts
  13. 2
      ui-ngx/src/polyfills.ts

122
application/src/main/data/json/system/widget_bundles/input_widgets.json

File diff suppressed because one or more lines are too long

86
ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts

@ -15,12 +15,12 @@
///
import * as CanvasGauges from 'canvas-gauges';
import GenericOptions = CanvasGauges.GenericOptions;
import BaseGauge = CanvasGauges.BaseGauge;
import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models';
import * as tinycolor_ from 'tinycolor2';
import { ColorFormats } from 'tinycolor2';
import { isDefined, isString, isUndefined, padValue } from '@core/utils';
import GenericOptions = CanvasGauges.GenericOptions;
import BaseGauge = CanvasGauges.BaseGauge;
const tinycolor = tinycolor_;
@ -32,12 +32,12 @@ export interface DigitalGaugeColorRange {
rgbString: string;
}
export interface colorLevelSetting {
export interface ColorLevelSetting {
value: number;
color: string;
}
export type levelColors = Array<string | colorLevelSetting>;
export type levelColors = Array<string | ColorLevelSetting>;
export interface CanvasDigitalGaugeOptions extends GenericOptions {
gaugeType?: GaugeType;
@ -81,6 +81,11 @@ export interface CanvasDigitalGaugeOptions extends GenericOptions {
fontValueHeight?: FontHeightInfo;
fontMinMaxHeight?: FontHeightInfo;
ticksValue?: number[];
ticks?: number[];
colorTicks?: string;
tickWidth?: number;
showTimestamp?: boolean;
}
@ -117,6 +122,10 @@ const defaultDigitalGaugeOptions: CanvasDigitalGaugeOptions = { ...GenericOption
neonGlowBrightness: 0,
colorTicks: 'gray',
tickWidth: 4,
ticks: [],
isMobile: false
}
};
@ -245,7 +254,12 @@ export class CanvasDigitalGauge extends BaseGauge {
for (let i = 0; i < options.levelColors.length; i++) {
const levelColor: any = options.levelColors[i];
if (levelColor !== null) {
const percentage = isColorProperty ? inc * i : CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue);
let percentage: number;
if(isColorProperty){
percentage = inc * i;
} else {
percentage = CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue);
}
let tColor = tinycolor(isColorProperty ? levelColor : levelColor.color);
options.colorsRange.push({
pct: percentage,
@ -263,6 +277,13 @@ export class CanvasDigitalGauge extends BaseGauge {
}
}
options.ticksValue = [];
for (const tick of options.ticks) {
if (tick !== null) {
options.ticksValue.push(CanvasDigitalGauge.normalizeValue(tick, options.minValue, options.maxValue))
}
}
if (options.neonGlowBrightness) {
options.neonColorTitle = tinycolor(options.colorTitle).brighten(options.neonGlowBrightness).toHexString();
options.neonColorLabel = tinycolor(options.colorLabel).brighten(options.neonGlowBrightness).toHexString();
@ -657,7 +678,7 @@ function determineFontHeight (options: CanvasDigitalGaugeOptions, target: string
fontStyle: options['font' + target + 'Style']
};
const text = $('<span>Hg</span>').css(fontStyle);
const block = $('<div style="display: inline-block; width: 1px; height: 0px;"></div>');
const block = $('<div style="display: inline-block; width: 1px; height: 0;"></div>');
const div = $('<div></div>');
div.append(text, block);
@ -866,6 +887,52 @@ function drawBarGlow(context: DigitalGaugeCanvasRenderingContext2D, startX: numb
context.stroke();
}
function drawTickArc(context: DigitalGaugeCanvasRenderingContext2D, tickValues: number[], Cx: number, Cy: number,
Ri: number, Rm: number, Ro: number, startAngle: number, endAngle: number,
color: string, tickWidth: number) {
if (!tickValues.length) {
return;
}
const strokeWidth = Ro - Ri;
context.beginPath();
context.lineWidth = tickWidth;
context.strokeStyle = color;
for (const tick of tickValues) {
const angle = startAngle + tick * endAngle;
const x1 = Cx + (Ri + strokeWidth) * Math.cos(angle);
const y1 = Cy + (Ri + strokeWidth) * Math.sin(angle);
const x2 = Cx + Ri * Math.cos(angle);
const y2 = Cy + Ri * Math.sin(angle);
context.moveTo(x1, y1);
context.lineTo(x2, y2);
}
context.stroke();
}
function drawTickBar(context: DigitalGaugeCanvasRenderingContext2D, tickValues: number[], startX: number, startY: number,
distanceBar: number, strokeWidth: number, isVertical: boolean, color: string, tickWidth: number) {
if (!tickValues.length) {
return;
}
context.beginPath();
context.lineWidth = tickWidth;
context.strokeStyle = color;
for (const tick of tickValues) {
const tickValue = tick * distanceBar;
if (isVertical) {
context.moveTo(startX - strokeWidth / 2, startY + tickValue - distanceBar);
context.lineTo(startX + strokeWidth / 2, startY + tickValue - distanceBar);
} else {
context.moveTo(startX + tickValue, startY);
context.lineTo(startX + tickValue, startY + strokeWidth);
}
}
context.stroke();
}
function drawProgress(context: DigitalGaugeCanvasRenderingContext2D,
options: CanvasDigitalGaugeOptions, progress: number) {
let neonColor;
@ -899,6 +966,8 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D,
drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, true,
options.donutStartAngle, options.donutEndAngle);
}
drawTickArc(context, options.ticksValue, Cx, Cy, Ri, Rm, Ro, options.donutStartAngle,
options.donutEndAngle - options.donutStartAngle, options.colorTicks, options.tickWidth);
} else if (options.gaugeType === 'arc') {
if (options.neonGlowBrightness) {
context.strokeStyle = neonColor;
@ -909,6 +978,7 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D,
if (options.neonGlowBrightness && !options.isMobile) {
drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, false);
}
drawTickArc(context, options.ticksValue, Cx, Cy, Ri, Rm, Ro, Math.PI, Math.PI, options.colorTicks, options.tickWidth);
} else if (options.gaugeType === 'horizontalBar') {
if (options.neonGlowBrightness) {
context.strokeStyle = neonColor;
@ -922,6 +992,8 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D,
barLeft + (barRight-barLeft)*progress, barTop + strokeWidth/2,
neonColor, strokeWidth, false);
}
drawTickBar(context, options.ticksValue, barLeft, barTop, barRight - barLeft, strokeWidth,
false, options.colorTicks, options.tickWidth);
} else if (options.gaugeType === 'verticalBar') {
if (options.neonGlowBrightness) {
context.strokeStyle = neonColor;
@ -935,6 +1007,8 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D,
baseX + width/2, barBottom - (barBottom-barTop)*progress,
neonColor, strokeWidth, true);
}
drawTickBar(context, options.ticksValue, baseX + width / 2, barTop, barTop - barBottom, strokeWidth,
true, options.colorTicks, options.tickWidth);
}
}

97
ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts

@ -19,25 +19,27 @@ import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge';
import { AnimationRule } from '@home/components/widget/lib/analogue-gauge.models';
import { FontSettings } from '@home/components/widget/lib/settings.models';
export interface colorLevelProperty {
export interface AttributeSourceProperty {
valueSource: string;
entityAlias?: string;
attribute?: string;
value?: number;
}
export interface fixedLevelColors {
from?: colorLevelProperty;
to?: colorLevelProperty;
export interface FixedLevelColors {
from?: AttributeSourceProperty;
to?: AttributeSourceProperty;
color: string;
}
export interface colorLevelSetting {
export interface ColorLevelSetting {
value: number;
color: string;
}
export type colorLevel = Array<string | colorLevelSetting>;
export type colorLevel = Array<string | ColorLevelSetting>;
export type attributesGaugeType = 'levelColors' | 'ticks';
export interface DigitalGaugeSettings {
minValue?: number;
@ -60,7 +62,7 @@ export interface DigitalGaugeSettings {
gaugeColor?: string;
useFixedLevelColor?: boolean;
levelColors?: colorLevel;
fixedLevelColors?: fixedLevelColors[];
fixedLevelColors?: FixedLevelColors[];
animation?: boolean;
animationDuration?: number;
animationRule?: AnimationRule;
@ -72,6 +74,11 @@ export interface DigitalGaugeSettings {
units?: string;
hideValue?: boolean;
hideMinMax?: boolean;
showTicks?: boolean;
ticksValue?: AttributeSourceProperty[];
ticks?: number[];
colorTicks?: string;
tickWidth?: number;
}
export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
@ -242,6 +249,48 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
}
}
},
showTicks: {
title: 'Show ticks',
type: 'boolean',
default: false
},
tickWidth: {
title: 'Width ticks',
type: 'number',
default: 4
},
colorTicks: {
title: 'Color ticks',
type: 'string',
default: '#666'
},
ticksValue: {
title: 'The ticks predefined value',
type: 'array',
items: {
title: 'tickValue',
type: 'object',
properties: {
valueSource: {
title: 'Value source',
type: 'string',
default: 'predefinedValue'
},
entityAlias: {
title: 'Source entity alias',
type: 'string'
},
attribute: {
title: 'Source entity attribute',
type: 'string'
},
value: {
title: 'Value (if predefined value is selected)',
type: 'number'
}
}
}
},
animation: {
title: 'Enable animation',
type: 'boolean',
@ -487,6 +536,40 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = {
}
]
},
'showTicks',
{
key: 'tickWidth',
condition: 'model.showTicks === true'
},
{
key: 'colorTicks',
condition: 'model.showTicks === true',
type: 'color'
},
{
key: 'ticksValue',
condition: 'model.showTicks === true',
items: [
{
key: 'ticksValue[].valueSource',
type: 'rc-select',
multiple: false,
items: [
{
value: 'predefinedValue',
label: 'Predefined value (Default)'
},
{
value: 'entityAttribute',
label: 'Value taken from entity attribute'
}
]
},
'ticksValue[].value',
'ticksValue[].entityAlias',
'ticksValue[].attribute'
]
},
'animation',
'animationDuration',
{

243
ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts

@ -17,19 +17,30 @@
import * as CanvasGauges from 'canvas-gauges';
import { WidgetContext } from '@home/models/widget-component.models';
import {
colorLevelSetting,
attributesGaugeType,
AttributeSourceProperty,
ColorLevelSetting,
DigitalGaugeSettings,
digitalGaugeSettingsSchema
digitalGaugeSettingsSchema,
FixedLevelColors
} from '@home/components/widget/lib/digital-gauge.models';
import * as tinycolor_ from 'tinycolor2';
import { isDefined } from '@core/utils';
import { prepareFontSettings } from '@home/components/widget/lib/settings.models';
import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge';
import { DatePipe } from '@angular/common';
import {DataKey, Datasource, DatasourceType, JsonSettingsSchema, widgetType} from '@shared/models/widget.models';
import {
DataKey,
Datasource,
DatasourceData,
DatasourceType,
JsonSettingsSchema,
widgetType
} from '@shared/models/widget.models';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { EMPTY, Observable } from 'rxjs';
import GenericOptions = CanvasGauges.GenericOptions;
import {IWidgetSubscription, WidgetSubscriptionOptions} from '@core/api/widget-api.models';
import {DataKeyType} from '@shared/models/telemetry/telemetry.models';
const tinycolor = tinycolor_;
@ -37,11 +48,6 @@ const digitalGaugeSettingsSchemaValue = digitalGaugeSettingsSchema;
export class TbCanvasDigitalGauge {
private localSettings: DigitalGaugeSettings;
private levelColorsSourcesSubscription: IWidgetSubscription;
private gauge: CanvasDigitalGauge;
static get settingsSchema(): JsonSettingsSchema {
return digitalGaugeSettingsSchemaValue;
}
@ -84,6 +90,12 @@ export class TbCanvasDigitalGauge {
this.localSettings.fixedLevelColors = settings.fixedLevelColors || [];
}
this.localSettings.showTicks = settings.showTicks || false;
this.localSettings.ticks = [];
this.localSettings.ticksValue = settings.ticksValue || [];
this.localSettings.tickWidth = settings.tickWidth || 4;
this.localSettings.colorTicks = settings.colorTicks || '#666';
this.localSettings.decimals = isDefined(dataKey.decimals) ? dataKey.decimals :
((isDefined(settings.decimals) && settings.decimals !== null)
? settings.decimals : ctx.decimals);
@ -137,6 +149,10 @@ export class TbCanvasDigitalGauge {
gaugeColor: this.localSettings.gaugeColor,
levelColors: this.localSettings.levelColors,
colorTicks: this.localSettings.colorTicks,
tickWidth: this.localSettings.tickWidth,
ticks: this.localSettings.ticks,
title: this.localSettings.title,
fontTitleSize: this.localSettings.titleFont.size,
@ -192,76 +208,92 @@ export class TbCanvasDigitalGauge {
this.init();
}
private localSettings: DigitalGaugeSettings;
private levelColorsSourcesSubscription: IWidgetSubscription;
private ticksSourcesSubscription: IWidgetSubscription;
private gauge: CanvasDigitalGauge;
static generateDatasource(ctx: WidgetContext, datasources: Datasource[], entityAlias: string,
attribute: string, settings: any): Datasource[]{
const entityAliasId = ctx.aliasController.getEntityAliasId(entityAlias);
if (!entityAliasId) {
throw new Error('Not valid entity aliase name ' + entityAlias);
}
const datasource = datasources.find((datasourceIteration) => {
return datasourceIteration.entityAliasId === entityAliasId;
});
const dataKey: DataKey = {
type: DataKeyType.attribute,
name: attribute,
label: attribute,
settings: [settings],
_hash: Math.random()
};
if (datasource) {
const findDataKey = datasource.dataKeys.find((dataKeyIteration) => {
return dataKeyIteration.name === attribute;
});
if (findDataKey) {
findDataKey.settings.push(settings);
} else {
datasource.dataKeys.push(dataKey)
}
} else {
const datasourceAttribute: Datasource = {
type: DatasourceType.entity,
name: entityAlias,
aliasName: entityAlias,
entityAliasId,
dataKeys: [dataKey]
};
datasources.push(datasourceAttribute);
}
return datasources;
}
init() {
if (this.localSettings.useFixedLevelColor) {
if (this.localSettings.fixedLevelColors && this.localSettings.fixedLevelColors.length > 0) {
this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors);
this.updateLevelColors(this.localSettings.levelColors);
}
if (this.localSettings.showTicks) {
if (this.localSettings.ticksValue && this.localSettings.ticksValue.length) {
this.localSettings.ticks = this.settingTicksSubscribe(this.localSettings.ticksValue);
}
}
this.updateSetting();
}
}
settingLevelColorsSubscribe(options) {
const levelColorsDatasource: Datasource[] = [];
const predefineLevelColors: colorLevelSetting[] = [];
settingLevelColorsSubscribe(options: FixedLevelColors[]): ColorLevelSetting[] {
let levelColorsDatasource: Datasource[] = [];
const predefineLevelColors: ColorLevelSetting[] = [];
function setLevelColor(levelSetting, color) {
function setLevelColor(levelSetting: AttributeSourceProperty, color: string) {
if (levelSetting.valueSource === 'predefinedValue' && isFinite(levelSetting.value)) {
predefineLevelColors.push({
value: levelSetting.value,
color
})
} else if (levelSetting.entityAlias && levelSetting.attribute) {
const entityAliasId = this.ctx.aliasController.getEntityAliasId(levelSetting.entityAlias);
if (!entityAliasId) {
try {
levelColorsDatasource = TbCanvasDigitalGauge.generateDatasource(this.ctx, levelColorsDatasource,
levelSetting.entityAlias, levelSetting.attribute, {color, index: predefineLevelColors.length});
} catch (e) {
return;
}
const datasource = levelColorsDatasource.find((datasource) => {
return datasource.entityAliasId === entityAliasId;
});
const dataKey: DataKey = {
type: DataKeyType.attribute,
name: levelSetting.attribute,
label: levelSetting.attribute,
settings: [{
color,
index: predefineLevelColors.length
}],
_hash: Math.random()
};
if (datasource) {
const findDataKey = datasource.dataKeys.find((dataKey) => {
return dataKey.name === levelSetting.attribute;
});
if (findDataKey) {
findDataKey.settings.push({
color,
index: predefineLevelColors.length
});
} else {
datasource.dataKeys.push(dataKey)
}
} else {
const datasource: Datasource = {
type: DatasourceType.entity,
name: levelSetting.entityAlias,
aliasName: levelSetting.entityAlias,
entityAliasId,
dataKeys: [dataKey]
};
levelColorsDatasource.push(datasource);
}
predefineLevelColors.push(null);
}
}
for (let i = 0; i < options.length; i++) {
const levelColor = options[i];
for(const levelColor of options){
if (levelColor.from) {
setLevelColor.call(this, levelColor.from, levelColor.color);
}
@ -270,49 +302,86 @@ export class TbCanvasDigitalGauge {
}
}
this.subscribeLevelColorsAttributes(levelColorsDatasource);
this.subscribeAttributes(levelColorsDatasource, 'levelColors').subscribe((subscription) => {
this.levelColorsSourcesSubscription = subscription;
});
return predefineLevelColors;
}
updateLevelColors(levelColors) {
(this.gauge.options as CanvasDigitalGaugeOptions).levelColors = levelColors;
this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options);
this.gauge.update({} as CanvasDigitalGaugeOptions);
settingTicksSubscribe(options: AttributeSourceProperty[]): number[] {
let ticksDatasource: Datasource[] = [];
const predefineTicks: number[] = [];
for(const tick of options){
if (tick.valueSource === 'predefinedValue' && isFinite(tick.value)) {
predefineTicks.push(tick.value)
} else if (tick.entityAlias && tick.attribute) {
try {
ticksDatasource = TbCanvasDigitalGauge
.generateDatasource(this.ctx, ticksDatasource, tick.entityAlias, tick.attribute, predefineTicks.length);
} catch (e) {
continue;
}
predefineTicks.push(null);
}
}
this.subscribeAttributes(ticksDatasource, 'ticks').subscribe((subscription) => {
this.ticksSourcesSubscription = subscription;
});
return predefineTicks;
}
subscribeLevelColorsAttributes(datasources: Datasource[]) {
const TbCanvasDigitalGauge = this;
subscribeAttributes(datasource: Datasource[], typeAttributes: attributesGaugeType): Observable<IWidgetSubscription> {
if (!datasource.length) {
return EMPTY;
}
const levelColorsSourcesSubscriptionOptions: WidgetSubscriptionOptions = {
datasources,
datasources: datasource,
useDashboardTimewindow: false,
type: widgetType.latest,
callbacks: {
onDataUpdated: (subscription) => {
for (let i = 0; i < subscription.data.length; i++) {
const keyData = subscription.data[i];
if (keyData && keyData.data && keyData.data[0]) {
const attrValue = keyData.data[0][1];
if (isFinite(attrValue)) {
for (let i = 0; i < keyData.dataKey.settings.length; i++) {
const setting = keyData.dataKey.settings[i];
this.localSettings.levelColors[setting.index] = {
value: attrValue,
color: setting.color
};
}
}
}
}
this.updateLevelColors(this.localSettings.levelColors);
this.updateAttribute(subscription.data, typeAttributes);
}
}
};
this.ctx.subscriptionApi.createSubscription(levelColorsSourcesSubscriptionOptions, true).subscribe(
(subscription) => {
TbCanvasDigitalGauge.levelColorsSourcesSubscription = subscription;
return this.ctx.subscriptionApi.createSubscription(levelColorsSourcesSubscriptionOptions, true);
}
updateAttribute(data: Array<DatasourceData>, typeAttributes: attributesGaugeType) {
for (const keyData of data) {
if (keyData && keyData.data && keyData.data[0]) {
const attrValue = keyData.data[0][1];
if (isFinite(attrValue)) {
for (const setting of keyData.dataKey.settings) {
switch (typeAttributes) {
case 'levelColors':
this.localSettings.levelColors[setting.index] = {
value: attrValue,
color: setting.color
};
break;
case 'ticks':
this.localSettings.ticks[setting] = attrValue;
break;
}
}
}
}
);
}
this.updateSetting();
}
updateSetting() {
(this.gauge.options as CanvasDigitalGaugeOptions).ticks = this.localSettings.ticks;
(this.gauge.options as CanvasDigitalGaugeOptions).levelColors = this.localSettings.levelColors;
this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options);
this.gauge.update({} as CanvasDigitalGaugeOptions);
}
update() {

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -43,7 +43,7 @@ let defaultSettings;
export class MapWidgetController implements MapWidgetInterface {
constructor(public mapProvider: MapProviders, private drawRoutes, public ctx: WidgetContext, $element: HTMLElement) {
constructor(public mapProvider: MapProviders, private drawRoutes: boolean, public ctx: WidgetContext, $element: HTMLElement) {
if (this.map) {
this.map.map.remove();
delete this.map;

74
ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.html

@ -0,0 +1,74 @@
<!--
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.
-->
<div fxLayout="column" fxLayoutAlign="center center" class="tb-web-camera" tb-fullscreen [fullscreen]="isShowCamera">
<div [fxShow]="isEntityDetected && dataKeyDetected && isCameraSupport && isDeviceDetect" fxFlexFill>
<div [fxShow]="!isShowCamera" fxLayout="column" fxLayoutAlign="space-between center" fxFlexFill>
<div class="tb-web-camera__last-photo" fxFlex>
<span [fxShow]="!lastPhoto" class="tb-web-camera__last-photo_text" translate>widgets.input-widgets.no-image</span>
<img [fxShow]="lastPhoto" class="tb-web-camera__last-photo_img" [src]="lastPhoto" alt="last photo"/>
</div>
<button mat-raised-button color="primary" (click)="takePhoto()">
{{ "widgets.input-widgets.take-photo" | translate }}
</button>
</div>
<div [fxShow]="isShowCamera" fxLayout="column" fxLayoutAlign="center center" class="camera-container">
<div class="camera" [fxShow]="!isPreviewPhoto">
<video autoplay muted playsinline class="camera-stream" #videoStream></video>
<div class="camera-controls" fxLayout="row wrap" fxLayoutAlign="space-between end">
<div fxFlex></div>
<button mat-mini-fab color="primary" (click)="switchWebCamera()" [disabled]="singleDevice">
<mat-icon>switch_camera</mat-icon>
</button>
<button mat-fab color="accent" (click)="createPhoto()">
<mat-icon>photo_camera</mat-icon>
</button>
<button mat-mini-fab color="primary" (click)="closeCamera()">
<mat-icon>close</mat-icon>
</button>
<div fxFlex></div>
</div>
</div>
<div class="camera" [fxShow]="isPreviewPhoto">
<img alt="preview photo" class="camera-stream" [src]="previewPhoto">
<canvas #canvas style="display:none;"></canvas>
<div class="camera-controls" fxLayout="row" fxLayoutAlign="space-between end">
<div fxFlex></div>
<button mat-fab color="primary" [disabled]="updatePhoto" (click)="cancelPhoto()">
<mat-icon>close</mat-icon>
</button>
<button mat-fab color="accent" [disabled]="updatePhoto" (click)="savePhoto()">
<mat-icon>check</mat-icon>
</button>
<div fxFlex></div>
</div>
</div>
</div>
</div>
<div class="message-text" [fxHide]="isEntityDetected">
{{ 'widgets.input-widgets.no-entity-selected' | translate }}
</div>
<div class="message-text" [fxShow]="isEntityDetected && !dataKeyDetected">
{{ 'widgets.input-widgets.no-datakey-selected' | translate }}
</div>
<div class="message-text" [fxShow]="isEntityDetected && dataKeyDetected && !isCameraSupport">
{{ 'widgets.input-widgets.no-support-web-camera' | translate }}
</div>
<div class="message-text" [fxShow]="isEntityDetected && dataKeyDetected && isCameraSupport && !isDeviceDetect">
{{ 'widgets.input-widgets.no-support-web-camera' | translate }}
</div>
</div>

74
ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.scss

@ -0,0 +1,74 @@
/**
* 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.
*/
.tb-web-camera {
height: 100%;
&__last-photo {
width: 100%;
margin: 5px 0;
text-align: center;
border: solid 1px;
&_text {
position: absolute;
top: 50%;
left: 50%;
margin-top: -.625em;
transform: translate(-50%, -50%);
}
&_img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.camera-container{
height: 100%;
}
.camera {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
.camera-stream {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.camera-controls {
position: absolute;
bottom: 0;
width: 100%;
padding: 0 5px 5px;
.mat-button-base{
margin: 6px 8px;
}
}
}
.message-text {
font-size: 18px;
color: #a0a0a0;
text-align: center;
}
}

274
ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.ts

@ -0,0 +1,274 @@
///
/// 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 {
Component,
ElementRef,
Inject,
Input,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models';
import { WINDOW } from '@core/services/window.service';
import { AttributeService } from '@core/http/attribute.service';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable } from 'rxjs';
interface WebCameraInputWidgetSettings {
widgetTitle: string;
imageQuality: number;
imageFormat: string;
maxWidth: number;
maxHeight: number;
}
@Component({
selector: 'tb-web-camera-widget',
templateUrl: './web-camera-input.component.html',
styleUrls: ['./web-camera-input.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class WebCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy {
constructor(@Inject(WINDOW) private window: Window,
protected store: Store<AppState>,
private elementRef: ElementRef,
private ngZone: NgZone,
private overlay: Overlay,
private utils: UtilsService,
private attributeService: AttributeService,
) {
super(store);
}
public get videoElement() {
return this.videoStreamRef.nativeElement;
}
public get canvasElement() {
return this.canvasRef.nativeElement;
}
public get videoWidth() {
const videoRatio = this.getVideoAspectRatio();
return Math.min(this.width, this.height * videoRatio);
}
public get videoHeight() {
const videoRatio = this.getVideoAspectRatio();
return Math.min(this.height, this.width / videoRatio);
}
private static DEFAULT_IMAGE_TYPE = 'image/jpeg';
private static DEFAULT_IMAGE_QUALITY = 0.92;
@Input()
ctx: WidgetContext;
@ViewChild('videoStream', {static: true}) videoStreamRef: ElementRef<HTMLVideoElement>;
@ViewChild('canvas', {static: true}) canvasRef: ElementRef<HTMLCanvasElement>;
private videoInputsIndex = 0;
private settings: WebCameraInputWidgetSettings;
private datasource: Datasource;
private width = 640;
private height = 480;
private availableVideoInputs: MediaDeviceInfo[];
private mediaStream: MediaStream;
isEntityDetected = false;
dataKeyDetected = false;
isCameraSupport = false;
isDeviceDetect = false;
isShowCamera = false;
isPreviewPhoto = false;
singleDevice = true;
updatePhoto = false;
previewPhoto: any;
lastPhoto: any;
private static hasGetUserMedia(): boolean {
return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia);
}
private static getAvailableVideoInputs(): Promise<MediaDeviceInfo[]> {
return new Promise((resolve, reject) => {
navigator.mediaDevices.enumerateDevices()
.then((devices: MediaDeviceInfo[]) => {
resolve(devices.filter((device: MediaDeviceInfo) => device.kind === 'videoinput'));
})
.catch(err => {
reject(err.message || err);
});
});
}
ngOnInit(): void {
this.ctx.$scope.webCameraInputWidget = this;
this.settings = this.ctx.settings;
this.datasource = this.ctx.datasources[0];
if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
this.ctx.widgetTitle = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle);
} else {
this.ctx.widgetTitle = this.ctx.widgetConfig.title;
}
this.width = this.settings.maxWidth ? this.settings.maxWidth : 640;
this.height = this.settings.maxHeight ? this.settings.maxWidth : 480;
if (this.datasource.type === DatasourceType.entity) {
if (this.datasource.entityType && this.datasource.entityId) {
this.isEntityDetected = true;
}
}
if (this.datasource.dataKeys.length) {
this.dataKeyDetected = true;
}
this.detectAvailableDevices();
}
ngOnDestroy(): void {
this.stopMediaTracks();
}
private updateWidgetData(data: Array<DatasourceData>) {
const keyData = data[0].data;
if (keyData && keyData.length) {
this.lastPhoto = keyData[0][1];
}
}
public onDataUpdated() {
this.ngZone.run(() => {
this.updateWidgetData(this.ctx.defaultSubscription.data);
this.ctx.detectChanges();
});
}
private detectAvailableDevices(): void {
if (WebCameraInputWidgetComponent.hasGetUserMedia()) {
this.isCameraSupport = true;
WebCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
this.isDeviceDetect = !!devices.length;
this.singleDevice = devices.length < 2;
this.availableVideoInputs = devices;
this.ctx.detectChanges();
}, () => {
this.availableVideoInputs = [];
}
)
}
}
private getVideoAspectRatio(): number {
if (this.videoElement.videoWidth && this.videoElement.videoWidth > 0 &&
this.videoElement.videoHeight && this.videoElement.videoHeight > 0) {
return this.videoElement.videoWidth / this.videoElement.videoHeight;
}
return this.width / this.height;
}
private stopMediaTracks() {
if (this.mediaStream && this.mediaStream.getTracks) {
this.mediaStream.getTracks()
.forEach((track: MediaStreamTrack) => track.stop());
}
}
takePhoto() {
this.isShowCamera = true;
this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId);
}
closeCamera() {
this.stopMediaTracks();
this.videoElement.srcObject = null;
this.isShowCamera = false;
}
cancelPhoto() {
this.isPreviewPhoto = false;
this.previewPhoto = '';
}
savePhoto() {
this.updatePhoto = true;
let task: Observable<any>;
const entityId: EntityId = {
entityType: this.datasource.entityType,
id: this.datasource.entityId
};
const saveData = [{
key: this.datasource.dataKeys[0].name,
value: this.previewPhoto
}];
if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
task = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData);
} else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
task = this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData);
}
task.subscribe(() => {
this.isPreviewPhoto = false;
this.updatePhoto = false;
this.closeCamera();
}, () => {
this.updatePhoto = false;
})
}
switchWebCamera() {
this.videoInputsIndex = (this.videoInputsIndex + 1) % this.availableVideoInputs.length;
this.stopMediaTracks();
this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId)
}
createPhoto() {
this.canvasElement.width = this.videoWidth;
this.canvasElement.height = this.videoHeight;
this.canvasElement.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight);
const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : WebCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
const quality: number = this.settings.imageQuality ? this.settings.imageQuality : WebCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
this.previewPhoto = this.canvasElement.toDataURL(mimeType, quality);
this.isPreviewPhoto = true;
}
private initWebCamera(deviceId?: string) {
if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
const videoTrackConstraints = {
video: {deviceId: deviceId !== '' ? {exact: deviceId} : undefined}
};
window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => {
this.mediaStream = stream;
this.videoElement.srcObject = stream;
})
}
}
}

7
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -32,6 +32,7 @@ import {
} from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component';
import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component';
import { TripAnimationComponent } from './trip-animation/trip-animation.component';
import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component';
@NgModule({
declarations:
@ -45,7 +46,8 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen
DateRangeNavigatorWidgetComponent,
DateRangeNavigatorPanelComponent,
MultipleInputWidgetComponent,
TripAnimationComponent
TripAnimationComponent,
WebCameraInputWidgetComponent
],
imports: [
CommonModule,
@ -61,7 +63,8 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen
RpcWidgetsModule,
DateRangeNavigatorWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent
TripAnimationComponent,
WebCameraInputWidgetComponent
],
providers: [
CustomDialogService

14
ui-ngx/src/app/shared/components/image-input.component.html

@ -16,15 +16,15 @@
-->
<div class="tb-container">
<label class="tb-title">{{label}}</label>
<label class="tb-title" *ngIf="label">{{label}}</label>
<ng-container #flow="flow"
[flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
<div class="tb-image-select-container">
<div class="tb-image-preview-container">
<div *ngIf="!safeImageUrl" translate>dashboard.no-image</div>
<img *ngIf="safeImageUrl" class="tb-image-preview" [src]="safeImageUrl" />
<div *ngIf="showPreview" class="tb-image-preview-container">
<div *ngIf="!safeImageUrl;else elseBlock" translate>dashboard.no-image</div>
<ng-template #elseBlock><img class="tb-image-preview" [src]="safeImageUrl" /></ng-template>
</div>
<div class="tb-image-clear-container">
<div *ngIf="showClearButton" class="tb-image-clear-container">
<button mat-button mat-icon-button color="primary"
type="button"
(click)="clearImage()"
@ -37,8 +37,8 @@
<div class="drop-area tb-flow-drop"
flowDrop
[flow]="flow.flowJs">
<label for="select" translate>dashboard.drop-image</label>
<input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: 'image/*'}" id="select">
<label for="{{inputId}}" translate>dashboard.drop-image</label>
<input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: 'image/*'}" id="{{inputId}}">
</div>
</div>
</ng-container>

4
ui-ngx/src/app/shared/components/image-input.component.scss

@ -35,9 +35,9 @@ $previewSize: 100px !default;
.tb-image-preview {
width: auto;
max-width: $previewSize;
max-width: $previewSize - 2;
height: auto;
max-height: $previewSize;
max-height: $previewSize - 2;
}
.tb-image-preview-container {

38
ui-ngx/src/app/shared/components/image-input.component.ts

@ -14,34 +14,16 @@
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { AfterViewInit, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DataKey, DatasourceType } from '@shared/models/widget.models';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service';
import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable, of, Subscription } from 'rxjs';
import { map, mergeMap, tap } from 'rxjs/operators';
import { alarmFields } from '@shared/models/alarm.models';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, } from '@angular/forms';
import { Subscription } from 'rxjs';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DialogService } from '@core/services/dialog.service';
import { FlowDirective } from '@flowjs/ngx-flow';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { UtilsService } from '@core/services/utils.service';
@Component({
selector: 'tb-image-input',
@ -61,9 +43,11 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
label: string;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
const newVal = coerceBooleanProperty(value);
@ -75,6 +59,15 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
@Input()
disabled: boolean;
@Input()
showClearButton = true;
@Input()
showPreview = true;
@Input()
inputId = this.utils.guid();
imageUrl: string;
safeImageUrl: SafeUrl;
@ -86,6 +79,7 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
private propagateChange = null;
constructor(protected store: Store<AppState>,
private utils: UtilsService,
private sanitizer: DomSanitizer) {
super(store);
}

2
ui-ngx/src/polyfills.ts

@ -75,6 +75,7 @@
import './zone-flags';
import 'zone.js/dist/zone'; // Included with Angular CLI.
import 'core-js/es/array';
import moment from 'moment';
/***************************************************************************************************
* APPLICATION IMPORTS
@ -101,6 +102,7 @@ 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;

Loading…
Cancel
Save