Line data Source code
1 : // @ts-check
2 :
3 : import Vue from "vue";
4 : import CameraCard from "./CameraCard.vue";
5 : import Card from "../Card.vue";
6 : import { debounce, forEach, throttle } from "lodash";
7 : import { mapGetters } from "vuex";
8 : import { sources } from "../store";
9 : import { DateTime } from "luxon";
10 : import d from "debug";
11 :
12 1 : const debug = d("cam");
13 :
14 : /**
15 : * @typedef {import('@cern/base-vue').BaseToggle} BaseToggle
16 : * @typedef {{
17 : * cam: V.Instance<typeof CameraCard>,
18 : * card: V.Instance<typeof Card>,
19 : * focus: HTMLInputElement,
20 : * iris: HTMLInputElement,
21 : * autofocus: V.Instance<BaseToggle>,
22 : * autoiris: V.Instance<BaseToggle>
23 : * }} Refs
24 : * @typedef {V.Instance<typeof component, V.ExtVue<any, Refs>>} Instance
25 : */
26 1 : const component = /** @type {V.Constructor<any, Refs>} */(Vue).extend({
27 : name: "CameraControl",
28 : components: { CameraCard, Card },
29 : props: {
30 : group: { type: String, default: "" },
31 : camera: { type: String, default: "" },
32 : description: { type: String, default: "" },
33 : ptz: { type: Boolean, default: false }
34 : },
35 : /**
36 : * @return {{
37 : * selected: null|CamExpress.CameraInfo, refreshing: boolean, advanced: boolean,
38 : * position: any , limits: any, paused: boolean, screenshot: string,
39 : * screenshotName: string, showControls: boolean, isFocused: boolean
40 : * }}
41 : */
42 : data() {
43 26 : return { selected: null, refreshing: false, advanced: false, paused: false,
44 : position: null, limits: null, screenshot: "", screenshotName: "",
45 : showControls: false, isFocused: false
46 : };
47 : },
48 : computed: {
49 : .../** @type {{isOperator(): boolean, isExpert(): boolean}} */(mapGetters([
50 : "isOperator", "isExpert"
51 : ])),
52 : /**
53 : * @this {Instance}
54 : * @return {boolean}
55 : */
56 0 : canControl() { return this.isOperator || this.isExpert; }
57 : },
58 : /** @this {Instance} */
59 : mounted() {
60 26 : this.wheelMove = debounce(this.move, 300);
61 26 : this.move = throttle(this.move, 250);
62 26 : this.refreshAdvanced();
63 :
64 26 : this.showControls = this.$refs?.cam?.connected ?? false;
65 : },
66 : methods: {
67 : /** @param {any} params */
68 : async move(params, absolute = true) {
69 0 : if (!this.canControl) { return; }
70 0 : if (this.paused) { return; }
71 0 : debug("ptz move request", params);
72 0 : await sources.cam.movePTZ(this.group, this.camera, params, absolute);
73 : },
74 : /** @param {MouseEvent} event */
75 : async moveToPointer(event) {
76 0 : if (!this.canControl) { return; }
77 0 : if (this.paused ||
78 : !this.$refs.cam?.connected ||
79 : !this.isFocused || !this.$refs?.card?.isFocused ||
80 : !this.ptz) {
81 0 : return;
82 : }
83 :
84 0 : event?.preventDefault();
85 0 : event?.stopImmediatePropagation();
86 0 : const rect = /** @type {Element} */ (event.target)?.getBoundingClientRect?.();
87 0 : await sources.cam.movePTZ(this.group, this.camera, {
88 : imagewidth: Math.trunc(rect.width),
89 : imageheight: Math.trunc(rect.height),
90 : center: `${Math.trunc(event.clientX - rect.x)},${Math.trunc(event.clientY - rect.y)}`
91 : })
92 : .then(() => {
93 0 : if (this.advanced) {
94 0 : this.refreshAdvanced();
95 : }
96 : });
97 : },
98 : onMouseDown() {
99 : // 'click' event occurs always after 'focusin' one: let's check if the
100 : // card is already focused before a complete mouse click (down + up)
101 0 : this.isFocused = this.$refs?.card?.isFocused ?? false;
102 : },
103 : /**
104 : * @this {Instance}
105 : * @param {WheelEvent} event
106 : */
107 : onWheel(event) { /* eslint-disable-line complexity */
108 0 : if (!this.canControl) { return; }
109 0 : if (this.paused || !this.$refs.cam?.connected ||
110 0 : !this.$refs?.card?.isFocused || !this.ptz) { return; }
111 :
112 0 : event?.preventDefault();
113 0 : event?.stopImmediatePropagation();
114 :
115 0 : /** @type {number} */let zoom = (this.position?.zoom ?? 0);
116 0 : /** @type {number} */const zoomMin = (this.limits?.MinZoom ?? 0);
117 0 : /** @type {number} */const zoomMax = (this.limits?.MaxZoom ?? 9999);
118 0 : /** @type {number} */let delta = Math.abs(zoomMin - zoomMax);
119 0 : delta /= (event.ctrlKey ? 250 : 40);
120 :
121 0 : zoom += (event.deltaY > 0) ? -delta : delta;
122 0 : zoom = Math.max(zoomMin, Math.min(zoomMax, zoom));
123 :
124 0 : Object.assign(this.position ?? {}, { zoom });
125 0 : this.wheelMove?.({ zoom }, true);
126 : },
127 : /**
128 : * @this {Instance}
129 : * @param {any} position
130 : */
131 : updateAdvanced(position) {
132 0 : let input = this.$refs.autofocus;
133 0 : if (input) { input.editValue = position.autofocus; }
134 :
135 0 : input = this.$refs.autoiris;
136 0 : if (input) { input.editValue = position.autoiris; }
137 :
138 0 : forEach([ "pan", "tilt", "zoom", "brightness", "focus", "iris" ], (p) => {
139 0 : const input = this.$refs[p];
140 0 : if (input) { input.value = position[p]; }
141 : });
142 0 : this.$refs.focus?.setAttribute?.("disabled", position["autofocus"] ? "disabled" : "");
143 0 : this.$refs.iris?.setAttribute?.("disabled", position["autoiris"] ? "disabled" : "");
144 : },
145 : async refreshAdvanced() {
146 26 : if (this.refreshing || !this.ptz) { return; }
147 0 : this.refreshing = true;
148 0 : try {
149 0 : this.position = await sources.cam.getPTZ(this.group, this.camera);
150 0 : this.limits = await sources.cam.getPTZLimits(this.group, this.camera);
151 0 : this.updateAdvanced(this.position);
152 : }
153 : finally {
154 0 : this.refreshing = false;
155 : }
156 : },
157 : /** @this {Instance} */
158 : async toggleAdvanced() {
159 0 : this.advanced = !this.advanced;
160 0 : if (this.advanced) {
161 0 : this.refreshAdvanced();
162 : }
163 : },
164 : /**
165 : * @this {Instance}
166 : * @param {string} name
167 : */
168 : async toggleCtrl(name) {
169 0 : if (this.paused) { return; }
170 0 : await this.move({ [name]: !this.$refs[name]?.editValue });
171 0 : this.refreshAdvanced();
172 : },
173 : /** @this {Instance} */
174 : async togglePause() {
175 0 : const tmp = this.screenshot;
176 0 : this.paused = !this.paused;
177 :
178 0 : if (this.paused) {
179 0 : await sources.cam.getScreenshot(this.group, this.camera)
180 : .then(
181 : (ret) => {
182 0 : this.screenshot = URL.createObjectURL(ret);
183 : },
184 : () => {
185 0 : if (this.$refs.cam?.imgURL) {
186 0 : this.screenshot = this.$refs.cam.imgURL;
187 0 : this.$refs.cam.imgURL = "";
188 : }
189 : })
190 : .then(() => {
191 0 : if (!this.screenshot) { throw new Error("no image"); }
192 :
193 0 : const timestamp = DateTime.now()
194 : .set({ millisecond: 0 })
195 : .toISO({
196 : format: "extended",
197 : suppressMilliseconds: true,
198 : includeOffset: false
199 : });
200 0 : this.screenshotName = `${this.group}_${this.camera}_${timestamp}.jpeg`;
201 : })
202 0 : .catch((err) => console.warn('saving screenshot failed: ', err));
203 : }
204 : else {
205 0 : this.screenshot = "";
206 0 : this.screenshotName = "";
207 : }
208 0 : URL.revokeObjectURL(tmp);
209 : }
210 : }
211 : });
212 :
213 : export default component;
|