diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index ac3486223a..097c31dac4 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -391,16 +391,16 @@ }, { "alias": "web_camera_input", - "name": "Web Camera Input", + "name": "Photo camera input", "descriptor": { "type": "latest", "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "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", - "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}", + "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", + "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}", "dataKeySettingsSchema": "{}\n", "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\":{}}" } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html similarity index 81% rename from ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html index 88e5475a40..b3b457849b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html @@ -16,13 +16,13 @@ -->
-
+
widgets.input-widgets.no-image last photo
-
@@ -59,16 +59,7 @@
-
- {{ 'widgets.input-widgets.no-entity-selected' | translate }} -
-
- {{ 'widgets.input-widgets.no-datakey-selected' | translate }} -
-
- {{ 'widgets.input-widgets.no-support-web-camera' | translate }} -
-
- {{ 'widgets.input-widgets.no-support-web-camera' | translate }} +
+ {{ textMessage | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.scss similarity index 92% rename from ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.scss index 94a51e6870..ea364e17a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.scss @@ -18,6 +18,7 @@ &__last-photo { width: 100%; + min-height: 0; margin: 5px 0; text-align: center; border: solid 1px; @@ -71,4 +72,11 @@ color: #a0a0a0; text-align: center; } + + .image-container{ + height: 100%; + min-height: 0; + width: 100%; + min-width: 100%; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts similarity index 72% rename from ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts index ee92ee8fe0..aebc500faf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts @@ -40,7 +40,7 @@ import { Observable } from 'rxjs'; import { isString } from '@core/utils'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -interface WebCameraInputWidgetSettings { +interface PhotoCameraInputWidgetSettings { widgetTitle: string; imageQuality: number; imageFormat: string; @@ -50,12 +50,12 @@ interface WebCameraInputWidgetSettings { // @dynamic @Component({ - selector: 'tb-web-camera-widget', - templateUrl: './web-camera-input.component.html', - styleUrls: ['./web-camera-input.component.scss'], + selector: 'tb-photo-camera-widget', + templateUrl: './photo-camera-input.component.html', + styleUrls: ['./photo-camera-input.component.scss'], encapsulation: ViewEncapsulation.None }) -export class WebCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy { +export class PhotoCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy { constructor(@Inject(WINDOW) private window: Window, protected store: Store, @@ -93,11 +93,11 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn @Input() ctx: WidgetContext; - @ViewChild('videoStream', {static: true}) videoStreamRef: ElementRef; - @ViewChild('canvas', {static: true}) canvasRef: ElementRef; + @ViewChild('videoStream', {static: false}) videoStreamRef: ElementRef; + @ViewChild('canvas', {static: false}) canvasRef: ElementRef; private videoInputsIndex = 0; - private settings: WebCameraInputWidgetSettings; + private settings: PhotoCameraInputWidgetSettings; private datasource: Datasource; private width = 640; private height = 480; @@ -106,10 +106,13 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn isEntityDetected = false; dataKeyDetected = false; + isProtocolHttps = false; isCameraSupport = false; isDeviceDetect = false; isShowCamera = false; isPreviewPhoto = false; + isHavePermissionCamera = true; + isLoading = false; singleDevice = true; updatePhoto = false; previewPhoto: SafeUrl; @@ -132,7 +135,8 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn } ngOnInit(): void { - this.ctx.$scope.webCameraInputWidget = this; + this.ctx.$scope.photoCameraInputWidget = this; + this.isLoading = true; this.settings = this.ctx.settings; this.datasource = this.ctx.datasources[0]; @@ -168,25 +172,33 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn } public onDataUpdated() { - this.ngZone.run(() => { - this.updateWidgetData(this.ctx.defaultSubscription.data); - this.ctx.detectChanges(); - }); + 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 = []; - } - ) + if (this.window.location.protocol === 'https:' || this.window.location.hostname === 'localhost') { + this.isProtocolHttps = true; + + if (PhotoCameraInputWidgetComponent.hasGetUserMedia()) { + this.isCameraSupport = true; + PhotoCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => { + this.isLoading = false; + this.isDeviceDetect = !!devices.length; + this.singleDevice = devices.length < 2; + this.availableVideoInputs = devices; + this.ctx.detectChanges(); + }, () => { + this.isLoading = false; + this.availableVideoInputs = []; + } + ); + } else { + this.isLoading = false; + } + } else { + this.isLoading = false; } } @@ -206,8 +218,7 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn } takePhoto() { - this.isShowCamera = true; - this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId); + this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId, true); } closeCamera() { @@ -243,13 +254,13 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn this.closeCamera(); }, () => { this.updatePhoto = false; - }) + }); } switchWebCamera() { this.videoInputsIndex = (this.videoInputsIndex + 1) % this.availableVideoInputs.length; this.stopMediaTracks(); - this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId) + this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId); } createPhoto() { @@ -257,22 +268,53 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn 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.sanitizer.bypassSecurityTrustUrl(this.canvasElement.toDataURL(mimeType, quality)); + const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE; + const quality: number = this.settings.imageQuality ? this.settings.imageQuality : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY; + this.previewPhoto = this.canvasElement.toDataURL(mimeType, quality); this.isPreviewPhoto = true; } - private initWebCamera(deviceId?: string) { + private inititedVideoStream(deviceId?: string, init = false) { if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) { const videoTrackConstraints = { video: {deviceId: deviceId !== '' ? {exact: deviceId} : undefined} }; window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => { + if (init) { + this.isShowCamera = true; + } this.mediaStream = stream; this.videoElement.srcObject = stream; - }) + this.ctx.detectChanges(); + }, () => { + this.isHavePermissionCamera = false; + }); + } + } + + get textMessage() { + if (this.isLoading) { + return ''; + } + if (!this.isProtocolHttps) { + return 'widgets.input-widgets.enable-https-use-widget'; + } + if (!this.isCameraSupport) { + return 'widgets.input-widgets.no-support-web-camera'; + } + if (!this.isEntityDetected) { + return 'widgets.input-widgets.no-entity-selected'; + } + if (!this.dataKeyDetected) { + return 'widgets.input-widgets.no-datakey-selected'; + } + if (!this.isDeviceDetect) { + return 'widgets.input-widgets.no-found-your-camera'; + } + if (!this.isHavePermissionCamera) { + return 'widgets.input-widgets.no-permission-camera'; } + return null; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index a97096a953..c2a158fcdf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -32,7 +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'; +import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component'; import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; import { ImportExportService } from '@home/components/import-export/import-export.service'; @@ -49,7 +49,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor DateRangeNavigatorPanelComponent, MultipleInputWidgetComponent, TripAnimationComponent, - WebCameraInputWidgetComponent, + PhotoCameraInputWidgetComponent, GatewayFormComponent ], imports: [ @@ -67,7 +67,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor DateRangeNavigatorWidgetComponent, MultipleInputWidgetComponent, TripAnimationComponent, - WebCameraInputWidgetComponent, + PhotoCameraInputWidgetComponent, GatewayFormComponent ], providers: [ diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 453d5d2abd..20a01cc99a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2283,7 +2283,10 @@ "no-entity-selected": "No entity selected", "no-image": "No image", "no-support-geolocation": "Your browser doesn't support geolocation", - "no-support-web-camera": "No supported web camera", + "no-support-web-camera": "Your browser does not support cameras", + "enable-https-use-widget": "Please enable HTTPS to use this widget", + "no-found-your-camera": "Can't find your camera", + "no-permission-camera": "Permission was denied by the user / This site doesn't have permission to use the camera", "no-timeseries-selected": "No timeseries selected", "secret-key": "Secret key", "secret-key-required": "Secret key is required",