|
|
@@ -1,623 +0,0 @@
|
|
|
-const parseResponse = async (r) => {
|
|
|
- const contentType = r.headers.get('content-type') || '';
|
|
|
- if (contentType.includes('application/json')) {
|
|
|
- return r.json();
|
|
|
- }
|
|
|
- const text = await r.text();
|
|
|
- return text ? { text } : {};
|
|
|
-};
|
|
|
-
|
|
|
-const API = {
|
|
|
- get: async (url) => {
|
|
|
- const r = await fetch(url);
|
|
|
- if (!r.ok) {
|
|
|
- const text = await r.text();
|
|
|
- throw new Error(`${url}: ${r.status} ${text}`);
|
|
|
- }
|
|
|
- return parseResponse(r);
|
|
|
- },
|
|
|
- post: async (url, body = {}) => {
|
|
|
- const r = await fetch(url, {
|
|
|
- method: 'POST',
|
|
|
- headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify(body)
|
|
|
- });
|
|
|
- if (!r.ok) {
|
|
|
- const text = await r.text();
|
|
|
- throw new Error(`${url}: ${r.status} ${text}`);
|
|
|
- }
|
|
|
- return parseResponse(r);
|
|
|
- },
|
|
|
- del: async (url) => {
|
|
|
- const r = await fetch(url, { method: 'DELETE' });
|
|
|
- if (!r.ok) {
|
|
|
- const text = await r.text();
|
|
|
- throw new Error(`${url}: ${r.status} ${text}`);
|
|
|
- }
|
|
|
- return parseResponse(r);
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-let currentGroup = null;
|
|
|
-let scanPollInterval = null;
|
|
|
-let controlsGloballyDisabled = false;
|
|
|
-let tempPreview = null;
|
|
|
-let selectedSampleEl = null;
|
|
|
-
|
|
|
-function log(msg) {
|
|
|
- const panel = document.getElementById('log-panel');
|
|
|
- const line = document.createElement('div');
|
|
|
- line.textContent = `${new Date().toLocaleTimeString()} ${msg}`;
|
|
|
- panel.appendChild(line);
|
|
|
- while (panel.children.length > 200) {
|
|
|
- panel.removeChild(panel.firstChild);
|
|
|
- }
|
|
|
- panel.scrollTop = panel.scrollHeight;
|
|
|
-}
|
|
|
-
|
|
|
-function setStatus(msg) {
|
|
|
- document.getElementById('status').textContent = `状态:${msg}`;
|
|
|
-}
|
|
|
-
|
|
|
-function setControlsDisabled(disabled) {
|
|
|
- controlsGloballyDisabled = disabled;
|
|
|
- ['btn-scan', 'btn-poll-start', 'btn-poll-stop', 'btn-preview', 'btn-add'].forEach(id => {
|
|
|
- const el = document.getElementById(id);
|
|
|
- if (el) el.disabled = disabled;
|
|
|
- });
|
|
|
- ['inp-pan', 'inp-tilt', 'inp-zoom', 'inp-dwell'].forEach(id => {
|
|
|
- const el = document.getElementById(id);
|
|
|
- if (el) el.disabled = disabled;
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-async function withDisabled(id, fn) {
|
|
|
- const el = document.getElementById(id);
|
|
|
- el.disabled = true;
|
|
|
- try {
|
|
|
- return await fn();
|
|
|
- } finally {
|
|
|
- if (!controlsGloballyDisabled) {
|
|
|
- el.disabled = false;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function parseStrictFloat(value, name) {
|
|
|
- const num = Number(value);
|
|
|
- if (!Number.isFinite(num)) {
|
|
|
- throw new Error(`${name} 必须是有效数字`);
|
|
|
- }
|
|
|
- return num;
|
|
|
-}
|
|
|
-
|
|
|
-function escapeHtml(text) {
|
|
|
- const div = document.createElement('div');
|
|
|
- div.textContent = text;
|
|
|
- return div.innerHTML;
|
|
|
-}
|
|
|
-
|
|
|
-function setSelectedPreview(url) {
|
|
|
- const img = document.getElementById('selected-preview');
|
|
|
- if (url) {
|
|
|
- img.src = url;
|
|
|
- img.style.display = 'block';
|
|
|
- } else {
|
|
|
- img.src = '';
|
|
|
- img.style.display = 'none';
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function selectSample(sample) {
|
|
|
- document.getElementById('inp-pan').value = sample.pan.toFixed(2);
|
|
|
- document.getElementById('inp-tilt').value = sample.tilt.toFixed(2);
|
|
|
- document.getElementById('inp-zoom').value = sample.zoom;
|
|
|
- tempPreview = { path: sample.thumbnail };
|
|
|
- setSelectedPreview(`/api/sample-image?path=${encodeURIComponent(sample.thumbnail)}`);
|
|
|
- if (sampleCanvas) sampleCanvas.setSelected(sample.pan, sample.tilt);
|
|
|
-}
|
|
|
-
|
|
|
-class SampleCanvas {
|
|
|
- constructor(containerId, wrapperId) {
|
|
|
- this.container = document.getElementById(containerId);
|
|
|
- this.wrapper = document.getElementById(wrapperId);
|
|
|
- this.samples = [];
|
|
|
- this.pans = [];
|
|
|
- this.tilts = [];
|
|
|
- this.sampleMap = new Map();
|
|
|
- this.cellW = 160;
|
|
|
- this.cellH = 120;
|
|
|
- this.captionH = 20;
|
|
|
- this.scale = 1;
|
|
|
- this.selectedPan = null;
|
|
|
- this.selectedTilt = null;
|
|
|
- this.selectedNodeId = null;
|
|
|
- this.graph = null;
|
|
|
-
|
|
|
- this.initGraph();
|
|
|
- window.addEventListener('resize', () => this.resize());
|
|
|
- }
|
|
|
-
|
|
|
- initGraph() {
|
|
|
- const G6 = window.G6;
|
|
|
- if (!G6) {
|
|
|
- log('G6 库未加载,扫描矩阵不可用');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const rect = this.wrapper.getBoundingClientRect();
|
|
|
- const width = rect.width || this.wrapper.clientWidth || 800;
|
|
|
- const height = rect.height || this.wrapper.clientHeight || 600;
|
|
|
- this.graph = new G6.Graph({
|
|
|
- container: this.container,
|
|
|
- width,
|
|
|
- height,
|
|
|
- animation: false,
|
|
|
- zoomRange: [0.1, 5],
|
|
|
- data: { nodes: [], edges: [] },
|
|
|
- node: {
|
|
|
- type: 'image',
|
|
|
- style: {
|
|
|
- size: [this.cellW, this.cellH],
|
|
|
- cursor: 'pointer',
|
|
|
- labelFill: '#cbd5e1',
|
|
|
- labelFontSize: 11,
|
|
|
- labelPlacement: 'bottom',
|
|
|
- labelOffsetY: 4,
|
|
|
- radius: 4,
|
|
|
- },
|
|
|
- state: {
|
|
|
- selected: {
|
|
|
- halo: true,
|
|
|
- haloStroke: '#4ade80',
|
|
|
- haloLineWidth: 6,
|
|
|
- haloOpacity: 1,
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- layout: {
|
|
|
- type: 'grid',
|
|
|
- cols: 1,
|
|
|
- nodeSize: [this.cellW, this.cellH + this.captionH],
|
|
|
- preventOverlap: true,
|
|
|
- },
|
|
|
- behaviors: ['drag-canvas', 'zoom-canvas'],
|
|
|
- });
|
|
|
-
|
|
|
- this.graph.on('node:click', (e) => {
|
|
|
- const target = e.item || e.target;
|
|
|
- const id = target?.id;
|
|
|
- if (!id) return;
|
|
|
- const nodeData = this.graph.getNodeData(id);
|
|
|
- if (nodeData?.data?.sample) {
|
|
|
- selectSample(nodeData.data.sample);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- this.graph.on('aftertransform', () => {
|
|
|
- if (!this.graph) return;
|
|
|
- this.scale = this.graph.getZoom();
|
|
|
- this.updateZoomLabel();
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- async resize() {
|
|
|
- if (!this.graph) return;
|
|
|
- const rect = this.wrapper.getBoundingClientRect();
|
|
|
- this.graph.setSize(rect.width, rect.height);
|
|
|
- await this.graph.fitView();
|
|
|
- this.scale = this.graph.getZoom();
|
|
|
- this.updateZoomLabel();
|
|
|
- }
|
|
|
-
|
|
|
- async setSamples(samples) {
|
|
|
- this.samples = samples || [];
|
|
|
- this.pans = Array.from(new Set(this.samples.map(s => s.pan))).sort((a, b) => a - b);
|
|
|
- this.tilts = Array.from(new Set(this.samples.map(s => s.tilt))).sort((a, b) => b - a);
|
|
|
- this.sampleMap = new Map();
|
|
|
- this.samples.forEach(s => this.sampleMap.set(`${s.pan},${s.tilt}`, s));
|
|
|
- if (!this.graph) return;
|
|
|
-
|
|
|
- const nodes = [];
|
|
|
- this.tilts.forEach(tilt => {
|
|
|
- this.pans.forEach(pan => {
|
|
|
- const s = this.sampleMap.get(`${pan},${tilt}`);
|
|
|
- if (!s) return;
|
|
|
- const id = `p${pan.toFixed(1)}_t${tilt.toFixed(1)}`;
|
|
|
- nodes.push({
|
|
|
- id,
|
|
|
- data: { sample: s },
|
|
|
- style: {
|
|
|
- src: `/api/sample-image?path=${encodeURIComponent(s.thumbnail)}`,
|
|
|
- labelText: `P:${pan.toFixed(0)} T:${tilt.toFixed(0)}`,
|
|
|
- },
|
|
|
- });
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- this.graph.setLayout({
|
|
|
- type: 'grid',
|
|
|
- cols: this.pans.length || 1,
|
|
|
- nodeSize: [this.cellW, this.cellH + this.captionH],
|
|
|
- preventOverlap: true,
|
|
|
- });
|
|
|
- this.graph.setData({ nodes, edges: [] });
|
|
|
- await this.graph.render();
|
|
|
- if (nodes.length > 0) await this.graph.fitView();
|
|
|
- this.selectedNodeId = null;
|
|
|
- this.selectedPan = null;
|
|
|
- this.selectedTilt = null;
|
|
|
- this.scale = this.graph.getZoom();
|
|
|
- this.updateZoomLabel();
|
|
|
- }
|
|
|
-
|
|
|
- setSelected(pan, tilt) {
|
|
|
- if (!this.graph) return;
|
|
|
- const prevId = this.selectedNodeId;
|
|
|
- this.selectedPan = pan;
|
|
|
- this.selectedTilt = tilt;
|
|
|
- this.selectedNodeId = pan !== null && tilt !== null
|
|
|
- ? `p${pan.toFixed(1)}_t${tilt.toFixed(1)}`
|
|
|
- : null;
|
|
|
-
|
|
|
- const states = {};
|
|
|
- if (prevId && prevId !== this.selectedNodeId) states[prevId] = [];
|
|
|
- if (this.selectedNodeId) states[this.selectedNodeId] = ['selected'];
|
|
|
- if (Object.keys(states).length > 0) {
|
|
|
- this.graph.setElementState(states, false);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async resetView() {
|
|
|
- if (!this.graph) return;
|
|
|
- await this.graph.fitView();
|
|
|
- this.scale = this.graph.getZoom();
|
|
|
- this.updateZoomLabel();
|
|
|
- }
|
|
|
-
|
|
|
- updateZoomLabel() {
|
|
|
- const label = document.getElementById('zoom-level');
|
|
|
- if (label) label.textContent = `${Math.round(this.scale * 100)}%`;
|
|
|
- }
|
|
|
-
|
|
|
- draw() {
|
|
|
- if (!this.graph) return;
|
|
|
- this.graph.zoomTo(this.scale);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-let sampleCanvas = null;
|
|
|
-
|
|
|
-async function loadSamples(groupId) {
|
|
|
- try {
|
|
|
- const data = await API.get(`/api/samples/${groupId}`);
|
|
|
- if (!sampleCanvas) {
|
|
|
- sampleCanvas = new SampleCanvas('sample-canvas', 'sample-grid-wrapper');
|
|
|
- }
|
|
|
- await sampleCanvas.setSamples(data.samples || []);
|
|
|
- } catch (e) {
|
|
|
- log(`加载扫描样本失败: ${e.message}`);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function loadGroups() {
|
|
|
- try {
|
|
|
- const status = await API.get('/api/status');
|
|
|
- const select = document.getElementById('group-select');
|
|
|
- select.innerHTML = '';
|
|
|
- const gids = Object.keys(status.groups || {});
|
|
|
- gids.forEach(gid => {
|
|
|
- const opt = document.createElement('option');
|
|
|
- opt.value = gid;
|
|
|
- opt.textContent = gid;
|
|
|
- select.appendChild(opt);
|
|
|
- });
|
|
|
- if (gids.length === 0) {
|
|
|
- currentGroup = null;
|
|
|
- setStatus('未配置摄像头组');
|
|
|
- setControlsDisabled(true);
|
|
|
- return;
|
|
|
- }
|
|
|
- setControlsDisabled(false);
|
|
|
- if (gids.includes(currentGroup)) {
|
|
|
- select.value = currentGroup;
|
|
|
- } else {
|
|
|
- currentGroup = select.options[0].value;
|
|
|
- onGroupChange();
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- log(`获取状态失败: ${e.message}`);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function resetSampleZoom() {
|
|
|
- if (sampleCanvas) sampleCanvas.resetView();
|
|
|
-}
|
|
|
-
|
|
|
-function onGroupChange() {
|
|
|
- currentGroup = document.getElementById('group-select').value;
|
|
|
- selectedSampleEl = null;
|
|
|
- tempPreview = null;
|
|
|
- setSelectedPreview(null);
|
|
|
- resetSampleZoom();
|
|
|
- loadSamples(currentGroup);
|
|
|
- renderVideos(currentGroup);
|
|
|
- loadPoints(currentGroup);
|
|
|
-}
|
|
|
-
|
|
|
-function renderVideos(groupId) {
|
|
|
- const grid = document.getElementById('video-grid');
|
|
|
- grid.innerHTML = '';
|
|
|
- ['panorama', 'ptz'].forEach(cam => {
|
|
|
- const box = document.createElement('div');
|
|
|
- box.className = 'video-box';
|
|
|
-
|
|
|
- const title = document.createElement('div');
|
|
|
- title.className = 'title';
|
|
|
- title.textContent = `${cam} - ${groupId}`;
|
|
|
-
|
|
|
- const img = document.createElement('img');
|
|
|
- img.src = `/api/live/${cam}/${groupId}?marked=1&t=${Date.now()}`;
|
|
|
- img.alt = cam;
|
|
|
-
|
|
|
- box.appendChild(title);
|
|
|
- box.appendChild(img);
|
|
|
- grid.appendChild(box);
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-async function loadPoints(groupId) {
|
|
|
- try {
|
|
|
- const data = await API.get(`/api/points/${groupId}`);
|
|
|
- const ul = document.getElementById('points');
|
|
|
- ul.innerHTML = '';
|
|
|
- data.points.forEach(p => {
|
|
|
- const li = document.createElement('li');
|
|
|
-
|
|
|
- const span = document.createElement('span');
|
|
|
- span.textContent = `P:${p.pan.toFixed(0)} T:${p.tilt.toFixed(0)}`;
|
|
|
-
|
|
|
- const previewBtn = document.createElement('button');
|
|
|
- previewBtn.textContent = '预览';
|
|
|
- previewBtn.onclick = async () => {
|
|
|
- document.getElementById('inp-pan').value = p.pan.toFixed(2);
|
|
|
- document.getElementById('inp-tilt').value = p.tilt.toFixed(2);
|
|
|
- document.getElementById('inp-zoom').value = p.zoom;
|
|
|
- await runPreview(groupId, p.pan, p.tilt, p.zoom, p.id);
|
|
|
- };
|
|
|
-
|
|
|
- const delBtn = document.createElement('button');
|
|
|
- delBtn.dataset.id = String(p.id);
|
|
|
- delBtn.textContent = '删除';
|
|
|
- delBtn.onclick = async () => {
|
|
|
- if (!currentGroup) {
|
|
|
- log('未选择摄像头组');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (!confirm('确定删除该扫描点?')) return;
|
|
|
- delBtn.disabled = true;
|
|
|
- try {
|
|
|
- await API.del(`/api/points/${groupId}/${p.id}`);
|
|
|
- loadPoints(groupId);
|
|
|
- } catch (e) {
|
|
|
- log(`删除失败: ${e.message}`);
|
|
|
- delBtn.disabled = false;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- li.appendChild(span);
|
|
|
- li.appendChild(previewBtn);
|
|
|
- li.appendChild(delBtn);
|
|
|
- ul.appendChild(li);
|
|
|
- });
|
|
|
- } catch (e) {
|
|
|
- log(`加载扫描点失败: ${e.message}`);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function updateStatus() {
|
|
|
- if (!currentGroup) return;
|
|
|
- try {
|
|
|
- const status = await API.get('/api/status');
|
|
|
- const g = status.groups[currentGroup];
|
|
|
- if (g) {
|
|
|
- setStatus(g.polling_state);
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- // ignore
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-document.getElementById('group-select').addEventListener('change', onGroupChange);
|
|
|
-
|
|
|
-document.getElementById('btn-scan').addEventListener('click', async () => {
|
|
|
- if (!currentGroup) {
|
|
|
- log('未选择摄像头组');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (scanPollInterval) return;
|
|
|
- const scannedGroup = currentGroup;
|
|
|
- const scanBtn = document.getElementById('btn-scan');
|
|
|
- scanBtn.disabled = true;
|
|
|
- setStatus('扫描中...');
|
|
|
- try {
|
|
|
- await API.post(`/api/scan/${scannedGroup}`);
|
|
|
- log(`开始扫描: ${scannedGroup}`);
|
|
|
- scanPollInterval = setInterval(async () => {
|
|
|
- try {
|
|
|
- const prog = await API.get(`/api/scan/${scannedGroup}/progress`);
|
|
|
- const progress = prog.total > 0 ? (prog.current / prog.total) * 100 : 0;
|
|
|
- if (prog.state === 'done' || prog.state === 'failed' || progress >= 100) {
|
|
|
- clearInterval(scanPollInterval);
|
|
|
- scanPollInterval = null;
|
|
|
- if (!controlsGloballyDisabled) scanBtn.disabled = false;
|
|
|
- if (prog.state === 'done') {
|
|
|
- log('扫描完成');
|
|
|
- resetSampleZoom();
|
|
|
- loadSamples(scannedGroup);
|
|
|
- loadPoints(scannedGroup);
|
|
|
- } else if (prog.state === 'failed') {
|
|
|
- log(`扫描失败: ${prog.error || 'unknown'}`);
|
|
|
- }
|
|
|
- } else {
|
|
|
- setStatus(`扫描中... ${progress.toFixed(0)}%`);
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- clearInterval(scanPollInterval);
|
|
|
- scanPollInterval = null;
|
|
|
- if (!controlsGloballyDisabled) scanBtn.disabled = false;
|
|
|
- setStatus('扫描失败');
|
|
|
- log(`扫描进度获取失败: ${e.message}`);
|
|
|
- }
|
|
|
- }, 1000);
|
|
|
- } catch (e) {
|
|
|
- log(`扫描失败: ${e.message}`);
|
|
|
- if (!controlsGloballyDisabled) scanBtn.disabled = false;
|
|
|
- setStatus('扫描失败');
|
|
|
- }
|
|
|
-});
|
|
|
-
|
|
|
-document.getElementById('btn-poll-start').addEventListener('click', async () => {
|
|
|
- if (!currentGroup) {
|
|
|
- log('未选择摄像头组');
|
|
|
- return;
|
|
|
- }
|
|
|
- await withDisabled('btn-poll-start', async () => {
|
|
|
- try {
|
|
|
- await API.post(`/api/poll/${currentGroup}/start`);
|
|
|
- log(`开始轮询: ${currentGroup}`);
|
|
|
- } catch (e) {
|
|
|
- log(`轮询启动失败: ${e.message}`);
|
|
|
- }
|
|
|
- });
|
|
|
-});
|
|
|
-
|
|
|
-document.getElementById('btn-poll-stop').addEventListener('click', async () => {
|
|
|
- if (!currentGroup) {
|
|
|
- log('未选择摄像头组');
|
|
|
- return;
|
|
|
- }
|
|
|
- await withDisabled('btn-poll-stop', async () => {
|
|
|
- try {
|
|
|
- await API.post(`/api/poll/${currentGroup}/stop`);
|
|
|
- log(`停止轮询: ${currentGroup}`);
|
|
|
- } catch (e) {
|
|
|
- log(`停止失败: ${e.message}`);
|
|
|
- }
|
|
|
- });
|
|
|
-});
|
|
|
-
|
|
|
-async function runPreview(groupId, pan, tilt, zoom, pointId = null) {
|
|
|
- const payload = { pan, tilt, zoom };
|
|
|
- const result = await API.post(`/api/preview/${groupId}`, payload);
|
|
|
- log(`预览位置: P=${pan.toFixed(1)} T=${tilt.toFixed(1)} Z=${zoom}`);
|
|
|
- if (result.snapshot_url) {
|
|
|
- log(`预览抓拍已保存: ${result.snapshot_path}`);
|
|
|
- // 预览抓拍显示在右侧,但不更新保存点图片
|
|
|
- setSelectedPreview(result.snapshot_url);
|
|
|
- tempPreview = { path: result.snapshot_path };
|
|
|
- }
|
|
|
- return result;
|
|
|
-}
|
|
|
-
|
|
|
-document.getElementById('btn-preview').addEventListener('click', async () => {
|
|
|
- if (!currentGroup) {
|
|
|
- log('未选择摄像头组');
|
|
|
- return;
|
|
|
- }
|
|
|
- let pan, tilt, zoom;
|
|
|
- try {
|
|
|
- pan = parseStrictFloat(document.getElementById('inp-pan').value, 'pan');
|
|
|
- tilt = parseStrictFloat(document.getElementById('inp-tilt').value, 'tilt');
|
|
|
- zoom = Number(document.getElementById('inp-zoom').value);
|
|
|
- } catch (e) {
|
|
|
- log(`错误:${e.message}`);
|
|
|
- return;
|
|
|
- }
|
|
|
- if (!Number.isInteger(zoom) || zoom < 1) {
|
|
|
- log('错误:zoom 必须是大于等于 1 的整数');
|
|
|
- return;
|
|
|
- }
|
|
|
- await withDisabled('btn-preview', async () => {
|
|
|
- try {
|
|
|
- await runPreview(currentGroup, pan, tilt, zoom);
|
|
|
- } catch (e) {
|
|
|
- log(`预览失败: ${e.message}`);
|
|
|
- }
|
|
|
- });
|
|
|
-});
|
|
|
-
|
|
|
-document.getElementById('btn-add').addEventListener('click', async () => {
|
|
|
- if (!currentGroup) {
|
|
|
- log('未选择摄像头组');
|
|
|
- return;
|
|
|
- }
|
|
|
- await withDisabled('btn-add', async () => {
|
|
|
- let pan, tilt, zoom, dwellTime;
|
|
|
- try {
|
|
|
- pan = parseStrictFloat(document.getElementById('inp-pan').value, 'pan');
|
|
|
- tilt = parseStrictFloat(document.getElementById('inp-tilt').value, 'tilt');
|
|
|
- zoom = Number(document.getElementById('inp-zoom').value);
|
|
|
- dwellTime = parseStrictFloat(document.getElementById('inp-dwell').value, '停留时间');
|
|
|
- } catch (e) {
|
|
|
- log(`错误:${e.message}`);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (pan < 0 || pan > 360) {
|
|
|
- log('错误:pan 必须是 0-360 之间的有限数值');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (tilt < -90 || tilt > 90) {
|
|
|
- log('错误:tilt 必须是 -90-90 之间的有限数值');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (!Number.isInteger(zoom) || zoom < 1) {
|
|
|
- log('错误:zoom 必须是大于等于 1 的整数');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (dwellTime <= 0) {
|
|
|
- log('错误:停留时间必须是大于 0 的有限数值');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const payload = { pan, tilt, zoom, dwell_time: dwellTime };
|
|
|
- if (tempPreview) {
|
|
|
- payload.preview_image = tempPreview.path;
|
|
|
- }
|
|
|
- try {
|
|
|
- await API.post(`/api/points/${currentGroup}`, payload);
|
|
|
- log('扫描点已保存');
|
|
|
- selectedSampleEl = null;
|
|
|
- tempPreview = null;
|
|
|
- setSelectedPreview(null);
|
|
|
- loadPoints(currentGroup);
|
|
|
- } catch (e) {
|
|
|
- log(`保存失败: ${e.message}`);
|
|
|
- }
|
|
|
- });
|
|
|
-});
|
|
|
-
|
|
|
-document.getElementById('btn-zoom-in').addEventListener('click', () => {
|
|
|
- if (sampleCanvas) {
|
|
|
- sampleCanvas.scale = Math.min(5.0, sampleCanvas.scale * 1.2);
|
|
|
- sampleCanvas.updateZoomLabel();
|
|
|
- sampleCanvas.draw();
|
|
|
- }
|
|
|
-});
|
|
|
-
|
|
|
-document.getElementById('btn-zoom-out').addEventListener('click', () => {
|
|
|
- if (sampleCanvas) {
|
|
|
- sampleCanvas.scale = Math.max(0.1, sampleCanvas.scale / 1.2);
|
|
|
- sampleCanvas.updateZoomLabel();
|
|
|
- sampleCanvas.draw();
|
|
|
- }
|
|
|
-});
|
|
|
-
|
|
|
-document.getElementById('btn-zoom-reset').addEventListener('click', () => {
|
|
|
- resetSampleZoom();
|
|
|
-});
|
|
|
-
|
|
|
-setControlsDisabled(true);
|
|
|
-loadGroups();
|
|
|
-setInterval(updateStatus, 2000);
|