// ==UserScript== // @name 2D车位线标注验收 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 拦截标注数据验收,根据业务规则精准校验,支持点击高亮定位、窗口拖动与参数自定义 // @author CodeGeeX // @match http://113.207.49.80:8080/* // @grant none // ==/UserScript== (function() { 'use strict'; // 默认自定义配置参数 const CONFIG = { lineMinWidth: 8, // 车位线最小宽度(像素) lineMaxWidth: 12, // 车位线最大宽度(像素) pointRectMaxDist: 100 // 角点距离车位框中心最大允许距离(像素) }; // 保存最后一次拦截到的数据,方便修改配置后重新校验 let lastMarkData = null; const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url, ...args) { this._hookData = { method, url }; return originalOpen.apply(this, [method, url, ...args]); }; XMLHttpRequest.prototype.send = function(...args) { if (this._hookData && this._hookData.url.includes('/api/task/get-mark-data')) { this.addEventListener('load', function() { if (this.status === 200) { try { const responseData = JSON.parse(this.responseText); if (responseData.code === 0 && responseData.data && responseData.data.markData) { lastMarkData = responseData.data.markData; // 缓存数据 validateAndReport(lastMarkData); } } catch (e) { console.error('[验收助手] 解析JSON失败:', e); } } }); } return originalSend.apply(this, args); } function validateAndReport(markData) { const marks = markData.marks || []; const errors = []; // 1. 数据预处理与分组 (不再依赖顺序,只要groupId相同即为一组) const groups = {}; const pointList = []; let mainIndex = 1; marks.forEach((mark) => { const gid = mark.groupId; if (gid !== undefined) { if (!groups[gid]) { groups[gid] = { mainIndex: mainIndex++, points: [], rect: null }; } if (mark.type === 'point') { const subIndex = groups[gid].points.length + 1; mark._displayLabel = `#${groups[gid].mainIndex}-${subIndex}`; mark._mainIndex = groups[gid].mainIndex; groups[gid].points.push(mark); pointList.push(mark); } else if (mark.type === 'rect' && mark.pselect === '车位') { mark._mainIndex = groups[gid].mainIndex; groups[gid].rect = mark; } } else { mark._displayLabel = `#${mainIndex}`; mark._mainIndex = mainIndex++; } }); // 为车位框补充显示标签 (此时点已经全部归组完毕) for (const gid in groups) { const group = groups[gid]; if (group.rect) { group.rect._displayLabel = `#${group.mainIndex}-${group.points.length + 1}`; } } // 2. 执行业务规则校验 // 规则1:完整库位判定与反向校验 for (const gid in groups) { const group = groups[gid]; const hasRect = !!group.rect; const pointCount = group.points.length; if (hasRect) { const category = group.rect.attrs && group.rect.attrs.category && group.rect.attrs.category[0]; // 仅当属性为完整库位(lots)时,才校验是否包含4个角点 if (category === 'lots' && pointCount !== 4) { errors.push({ type: '结构错误', label: `#${group.mainIndex}`, name: '车位组', markId: group.rect.markId, msg: `完整库位应含4个角点,实际包含 ${pointCount} 个` }); } } else { // 反向校验:如果有4个角点但没有车位框,提醒补充 if (pointCount === 4) { errors.push({ type: '结构错误', label: `#${group.mainIndex}`, name: '车位组', markId: group.points[0].markId, msg: '已有4个角点,但缺少车位整体框' }); } } } // 规则2:所有车位线属性中,车位线的宽度一定是自定义范围内的像素 marks.forEach(mark => { if (mark.type === 'rect' && mark.pselect === '车位线') { const p = mark.point || {}; const w = Math.abs((p.right || 0) - (p.left || 0)); const h = Math.abs((p.bottom || 0) - (p.top || 0)); const minSide = Math.min(w, h); if (minSide < CONFIG.lineMinWidth || minSide > CONFIG.lineMaxWidth) { errors.push({ type: '属性错误', label: mark._displayLabel, name: '车位线', markId: mark.markId, msg: `车位线宽度应为${CONFIG.lineMinWidth}-${CONFIG.lineMaxWidth}px,当前为 ${minSide.toFixed(1)}px` }); } } }); // 规则3:车位整块框和1.2点的距离不会太远 (使用自定义阈值) for (const gid in groups) { const group = groups[gid]; if (group.rect && group.points.length >= 2) { const rectP = group.rect.point || {}; const rectCx = ((rectP.left || 0) + (rectP.right || 0)) / 2; const rectCy = ((rectP.top || 0) + (rectP.bottom || 0)) / 2; group.points.slice(0, 2).forEach(p => { const pt = p.point || {}; const dist = Math.sqrt(Math.pow(rectCx - parseFloat(pt.x || 0), 2) + Math.pow(rectCy - parseFloat(pt.y || 0), 2)); if (dist > CONFIG.pointRectMaxDist) { errors.push({ type: '逻辑错误', label: p._displayLabel, name: '角点', markId: p.markId, msg: `角点距离车位框中心过远 (${dist.toFixed(1)}px),阈值${CONFIG.pointRectMaxDist}px` }); } }); } } // 规则4:车位内1和2点任意一点属性为不可见时,车位整块框的类别必须为不可辨别 for (const gid in groups) { const group = groups[gid]; if (group.rect && group.points.length >= 2) { const p1Vis = group.points[0].attrs && group.points[0].attrs.visibility && group.points[0].attrs.visibility[0]; const p2Vis = group.points[1].attrs && group.points[1].attrs.visibility && group.points[1].attrs.visibility[0]; if (p1Vis === 'invisible_p' || p2Vis === 'invisible_p') { const lotsType = group.rect.attrs && group.rect.attrs.lots_type && group.rect.attrs.lots_type[0]; const isUnknown = lotsType && (lotsType === 'vertical_unknown' || lotsType === 'oblique_unknown' || lotsType === 'horizontal_unknown'); if (!isUnknown) { errors.push({ type: '逻辑错误', label: `#${group.mainIndex}`, name: '车位框', markId: group.rect.markId, msg: `入口点(1或2)不可见时,车位类别必须为不可辨别,当前为 ${lotsType || '空'}` }); } } } } // 规则5:点的属性中,不可见和其他一定是对应的 pointList.forEach(p => { const vis = p.attrs && p.attrs.visibility && p.attrs.visibility[0]; const pType = p.attrs && p.attrs.points_type && p.attrs.points_type[0]; if (vis === 'invisible_p' && pType !== 'others_p') { errors.push({ type: '属性错误', label: p._displayLabel, name: '角点', markId: p.markId, msg: '点属性不可见时,点类别必须为其他(others_p)' }); } if (vis === 'visible_p' && pType === 'others_p') { errors.push({ type: '属性错误', label: p._displayLabel, name: '角点', markId: p.markId, msg: '点属性可见时,点类别不能为其他(others_p)' }); } }); // 规则6:地砖线一定对应颜色是其它 marks.forEach(mark => { if (mark.type === 'rect' && mark.pselect === '车位线') { const category = mark.attrs && mark.attrs.category && mark.attrs.category[0]; const color = mark.attrs && mark.attrs.color && mark.attrs.color[0]; if (category === 'floortileline' && color !== 'qita') { errors.push({ type: '属性错误', label: mark._displayLabel, name: '车位线', markId: mark.markId, msg: `地砖线(floortileline)颜色必须为其它(qita),当前为 ${color || '空'}` }); } } }); renderReport(errors); } function initPanel() { if (document.getElementById('val-panel')) return; const panel = document.createElement('div'); panel.id = 'val-panel'; panel.innerHTML = `