Commit 0c1fd9785097cec8948135cb196d65fe8870eabc

Authored by Vladyslav_Prykhodko
1 parent f73b05a8

UI: Improvement photo camera input widget

... ... @@ -391,16 +391,16 @@
391 391 },
392 392 {
393 393 "alias": "web_camera_input",
394   - "name": "Web Camera Input",
  394 + "name": "Photo camera input",
395 395 "descriptor": {
396 396 "type": "latest",
397 397 "sizeX": 7.5,
398 398 "sizeY": 3,
399 399 "resources": [],
400   - "templateHtml": "<tb-web-camera-widget \n [ctx]=\"ctx\">\n</tb-web-camera-widget>",
  400 + "templateHtml": "<tb-photo-camera-widget \n [ctx]=\"ctx\">\n</tb-photo-camera-widget>",
401 401 "templateCss": "",
402   - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.webCameraInputWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}\n",
403   - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Web Camera\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"imageFormat\": {\n \"title\": \"Image Format\",\n \"type\": \"string\",\n \"default\": \"image/png\"\n },\n \"imageQuality\":{\n \"title\":\"Image quality that use lossy compression such as jpeg and webp\",\n \"type\":\"number\",\n \"default\": 0.92,\n \"min\": 0,\n \"max\": 1\n },\n \"maxWidth\": {\n \"title\": \"The maximal image width\",\n \"type\": \"number\",\n \"default\": 640\n }, \n \"maxHeight\": {\n \"title\": \"The maximal image heigth\",\n \"type\": \"number\",\n \"default\": 480\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"imageFormat\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"image/jpeg\",\n \"label\": \"JPEG\"\n },\n {\n \"value\": \"image/png\",\n \"label\": \"PNG\"\n },\n {\n \"value\": \"image/webp\",\n \"label\": \"WEBP\"\n }\n ]\n },\n \"imageQuality\",\n \"maxWidth\",\n \"maxHeight\"\n ]\n}",
  402 + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.photoCameraInputWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}\n",
  403 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Photo Camera\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"imageFormat\": {\n \"title\": \"Image Format\",\n \"type\": \"string\",\n \"default\": \"image/png\"\n },\n \"imageQuality\":{\n \"title\":\"Image quality that use lossy compression such as jpeg and webp\",\n \"type\":\"number\",\n \"default\": 0.92,\n \"min\": 0,\n \"max\": 1\n },\n \"maxWidth\": {\n \"title\": \"The maximal image width\",\n \"type\": \"number\",\n \"default\": 640\n }, \n \"maxHeight\": {\n \"title\": \"The maximal image heigth\",\n \"type\": \"number\",\n \"default\": 480\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"imageFormat\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"image/jpeg\",\n \"label\": \"JPEG\"\n },\n {\n \"value\": \"image/png\",\n \"label\": \"PNG\"\n },\n {\n \"value\": \"image/webp\",\n \"label\": \"WEBP\"\n }\n ]\n },\n \"imageQuality\",\n \"maxWidth\",\n \"maxHeight\"\n ]\n}",
404 404 "dataKeySettingsSchema": "{}\n",
405 405 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Web Camera Input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
406 406 }
... ...
ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html renamed from ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.html
... ... @@ -16,13 +16,13 @@
16 16
17 17 -->
18 18 <div fxLayout="column" fxLayoutAlign="center center" class="tb-web-camera" tb-fullscreen [fullscreen]="isShowCamera">
19   - <div *ngIf="isEntityDetected && dataKeyDetected && isCameraSupport && isDeviceDetect" fxFlexFill>
  19 + <div *ngIf="isEntityDetected && dataKeyDetected && isCameraSupport && isProtocolHttps" class="image-container">
20 20 <div [fxShow]="!isShowCamera" fxLayout="column" fxLayoutAlign="space-between center" fxFlexFill>
21 21 <div class="tb-web-camera__last-photo" fxFlex>
22 22 <span [fxShow]="!lastPhoto" class="tb-web-camera__last-photo_text" translate>widgets.input-widgets.no-image</span>
23 23 <img [fxShow]="lastPhoto" class="tb-web-camera__last-photo_img" [src]="lastPhoto" alt="last photo"/>
24 24 </div>
25   - <button mat-raised-button color="primary" (click)="takePhoto()">
  25 + <button mat-raised-button color="primary" (click)="takePhoto()" *ngIf="!textMessage">
26 26 {{ "widgets.input-widgets.take-photo" | translate }}
27 27 </button>
28 28 </div>
... ... @@ -59,16 +59,7 @@
59 59 </div>
60 60 </div>
61 61 </div>
62   - <div class="message-text" *ngIf="!isEntityDetected">
63   - {{ 'widgets.input-widgets.no-entity-selected' | translate }}
64   - </div>
65   - <div class="message-text" *ngIf="isEntityDetected && !dataKeyDetected">
66   - {{ 'widgets.input-widgets.no-datakey-selected' | translate }}
67   - </div>
68   - <div class="message-text" *ngIf="isEntityDetected && dataKeyDetected && !isCameraSupport">
69   - {{ 'widgets.input-widgets.no-support-web-camera' | translate }}
70   - </div>
71   - <div class="message-text" *ngIf="isEntityDetected && dataKeyDetected && isCameraSupport && !isDeviceDetect">
72   - {{ 'widgets.input-widgets.no-support-web-camera' | translate }}
  62 + <div class="message-text" *ngIf="textMessage">
  63 + {{ textMessage | translate }}
73 64 </div>
74 65 </div>
... ...
ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.scss renamed from ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.scss
... ... @@ -18,6 +18,7 @@
18 18
19 19 &__last-photo {
20 20 width: 100%;
  21 + min-height: 0;
21 22 margin: 5px 0;
22 23 text-align: center;
23 24 border: solid 1px;
... ... @@ -71,4 +72,11 @@
71 72 color: #a0a0a0;
72 73 text-align: center;
73 74 }
  75 +
  76 + .image-container{
  77 + height: 100%;
  78 + min-height: 0;
  79 + width: 100%;
  80 + min-width: 100%;
  81 + }
74 82 }
... ...
ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts renamed from ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.ts
... ... @@ -40,7 +40,7 @@ import { Observable } from 'rxjs';
40 40 import { isString } from '@core/utils';
41 41 import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
42 42
43   -interface WebCameraInputWidgetSettings {
  43 +interface PhotoCameraInputWidgetSettings {
44 44 widgetTitle: string;
45 45 imageQuality: number;
46 46 imageFormat: string;
... ... @@ -50,12 +50,12 @@ interface WebCameraInputWidgetSettings {
50 50
51 51 // @dynamic
52 52 @Component({
53   - selector: 'tb-web-camera-widget',
54   - templateUrl: './web-camera-input.component.html',
55   - styleUrls: ['./web-camera-input.component.scss'],
  53 + selector: 'tb-photo-camera-widget',
  54 + templateUrl: './photo-camera-input.component.html',
  55 + styleUrls: ['./photo-camera-input.component.scss'],
56 56 encapsulation: ViewEncapsulation.None
57 57 })
58   -export class WebCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy {
  58 +export class PhotoCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy {
59 59
60 60 constructor(@Inject(WINDOW) private window: Window,
61 61 protected store: Store<AppState>,
... ... @@ -93,11 +93,11 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
93 93 @Input()
94 94 ctx: WidgetContext;
95 95
96   - @ViewChild('videoStream', {static: true}) videoStreamRef: ElementRef<HTMLVideoElement>;
97   - @ViewChild('canvas', {static: true}) canvasRef: ElementRef<HTMLCanvasElement>;
  96 + @ViewChild('videoStream', {static: false}) videoStreamRef: ElementRef<HTMLVideoElement>;
  97 + @ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;
98 98
99 99 private videoInputsIndex = 0;
100   - private settings: WebCameraInputWidgetSettings;
  100 + private settings: PhotoCameraInputWidgetSettings;
101 101 private datasource: Datasource;
102 102 private width = 640;
103 103 private height = 480;
... ... @@ -106,10 +106,13 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
106 106
107 107 isEntityDetected = false;
108 108 dataKeyDetected = false;
  109 + isProtocolHttps = false;
109 110 isCameraSupport = false;
110 111 isDeviceDetect = false;
111 112 isShowCamera = false;
112 113 isPreviewPhoto = false;
  114 + isHavePermissionCamera = true;
  115 + isLoading = false;
113 116 singleDevice = true;
114 117 updatePhoto = false;
115 118 previewPhoto: SafeUrl;
... ... @@ -132,7 +135,8 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
132 135 }
133 136
134 137 ngOnInit(): void {
135   - this.ctx.$scope.webCameraInputWidget = this;
  138 + this.ctx.$scope.photoCameraInputWidget = this;
  139 + this.isLoading = true;
136 140 this.settings = this.ctx.settings;
137 141 this.datasource = this.ctx.datasources[0];
138 142
... ... @@ -168,25 +172,33 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
168 172 }
169 173
170 174 public onDataUpdated() {
171   - this.ngZone.run(() => {
172   - this.updateWidgetData(this.ctx.defaultSubscription.data);
173   - this.ctx.detectChanges();
174   - });
  175 + this.updateWidgetData(this.ctx.defaultSubscription.data);
  176 + this.ctx.detectChanges();
175 177 }
176 178
177 179
178 180 private detectAvailableDevices(): void {
179   - if (WebCameraInputWidgetComponent.hasGetUserMedia()) {
180   - this.isCameraSupport = true;
181   - WebCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
182   - this.isDeviceDetect = !!devices.length;
183   - this.singleDevice = devices.length < 2;
184   - this.availableVideoInputs = devices;
185   - this.ctx.detectChanges();
186   - }, () => {
187   - this.availableVideoInputs = [];
188   - }
189   - )
  181 + if (this.window.location.protocol === 'https:' || this.window.location.hostname === 'localhost') {
  182 + this.isProtocolHttps = true;
  183 +
  184 + if (PhotoCameraInputWidgetComponent.hasGetUserMedia()) {
  185 + this.isCameraSupport = true;
  186 + PhotoCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
  187 + this.isLoading = false;
  188 + this.isDeviceDetect = !!devices.length;
  189 + this.singleDevice = devices.length < 2;
  190 + this.availableVideoInputs = devices;
  191 + this.ctx.detectChanges();
  192 + }, () => {
  193 + this.isLoading = false;
  194 + this.availableVideoInputs = [];
  195 + }
  196 + );
  197 + } else {
  198 + this.isLoading = false;
  199 + }
  200 + } else {
  201 + this.isLoading = false;
190 202 }
191 203 }
192 204
... ... @@ -206,8 +218,7 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
206 218 }
207 219
208 220 takePhoto() {
209   - this.isShowCamera = true;
210   - this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId);
  221 + this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId, true);
211 222 }
212 223
213 224 closeCamera() {
... ... @@ -243,13 +254,13 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
243 254 this.closeCamera();
244 255 }, () => {
245 256 this.updatePhoto = false;
246   - })
  257 + });
247 258 }
248 259
249 260 switchWebCamera() {
250 261 this.videoInputsIndex = (this.videoInputsIndex + 1) % this.availableVideoInputs.length;
251 262 this.stopMediaTracks();
252   - this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId)
  263 + this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId);
253 264 }
254 265
255 266 createPhoto() {
... ... @@ -257,22 +268,53 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
257 268 this.canvasElement.height = this.videoHeight;
258 269 this.canvasElement.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight);
259 270
260   - const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : WebCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
261   - const quality: number = this.settings.imageQuality ? this.settings.imageQuality : WebCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
262   - this.previewPhoto = this.sanitizer.bypassSecurityTrustUrl(this.canvasElement.toDataURL(mimeType, quality));
  271 + const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
  272 + const quality: number = this.settings.imageQuality ? this.settings.imageQuality : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
  273 + this.previewPhoto = this.canvasElement.toDataURL(mimeType, quality);
263 274 this.isPreviewPhoto = true;
264 275 }
265 276
266   - private initWebCamera(deviceId?: string) {
  277 + private inititedVideoStream(deviceId?: string, init = false) {
267 278 if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
268 279 const videoTrackConstraints = {
269 280 video: {deviceId: deviceId !== '' ? {exact: deviceId} : undefined}
270 281 };
271 282
272 283 window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => {
  284 + if (init) {
  285 + this.isShowCamera = true;
  286 + }
273 287 this.mediaStream = stream;
274 288 this.videoElement.srcObject = stream;
275   - })
  289 + this.ctx.detectChanges();
  290 + }, () => {
  291 + this.isHavePermissionCamera = false;
  292 + });
  293 + }
  294 + }
  295 +
  296 + get textMessage() {
  297 + if (this.isLoading) {
  298 + return '';
  299 + }
  300 + if (!this.isProtocolHttps) {
  301 + return 'widgets.input-widgets.enable-https-use-widget';
  302 + }
  303 + if (!this.isCameraSupport) {
  304 + return 'widgets.input-widgets.no-support-web-camera';
  305 + }
  306 + if (!this.isEntityDetected) {
  307 + return 'widgets.input-widgets.no-entity-selected';
  308 + }
  309 + if (!this.dataKeyDetected) {
  310 + return 'widgets.input-widgets.no-datakey-selected';
  311 + }
  312 + if (!this.isDeviceDetect) {
  313 + return 'widgets.input-widgets.no-found-your-camera';
  314 + }
  315 + if (!this.isHavePermissionCamera) {
  316 + return 'widgets.input-widgets.no-permission-camera';
276 317 }
  318 + return null;
277 319 }
278 320 }
... ...
... ... @@ -32,7 +32,7 @@ import {
32 32 } from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component';
33 33 import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component';
34 34 import { TripAnimationComponent } from './trip-animation/trip-animation.component';
35   -import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component';
  35 +import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component';
36 36 import { GatewayFormComponent } from './lib/gateway/gateway-form.component';
37 37 import { ImportExportService } from '@home/components/import-export/import-export.service';
38 38
... ... @@ -49,7 +49,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
49 49 DateRangeNavigatorPanelComponent,
50 50 MultipleInputWidgetComponent,
51 51 TripAnimationComponent,
52   - WebCameraInputWidgetComponent,
  52 + PhotoCameraInputWidgetComponent,
53 53 GatewayFormComponent
54 54 ],
55 55 imports: [
... ... @@ -67,7 +67,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
67 67 DateRangeNavigatorWidgetComponent,
68 68 MultipleInputWidgetComponent,
69 69 TripAnimationComponent,
70   - WebCameraInputWidgetComponent,
  70 + PhotoCameraInputWidgetComponent,
71 71 GatewayFormComponent
72 72 ],
73 73 providers: [
... ...
... ... @@ -2283,7 +2283,10 @@
2283 2283 "no-entity-selected": "No entity selected",
2284 2284 "no-image": "No image",
2285 2285 "no-support-geolocation": "Your browser doesn't support geolocation",
2286   - "no-support-web-camera": "No supported web camera",
  2286 + "no-support-web-camera": "Your browser does not support cameras",
  2287 + "enable-https-use-widget": "Please enable HTTPS to use this widget",
  2288 + "no-found-your-camera": "Can't find your camera",
  2289 + "no-permission-camera": "Permission was denied by the user / This site doesn't have permission to use the camera",
2287 2290 "no-timeseries-selected": "No timeseries selected",
2288 2291 "secret-key": "Secret key",
2289 2292 "secret-key-required": "Secret key is required",
... ...