Commit 0c1fd9785097cec8948135cb196d65fe8870eabc
1 parent
f73b05a8
UI: Improvement photo camera input widget
Showing
6 changed files
with
98 additions
and
54 deletions
... | ... | @@ -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", | ... | ... |