Files
mcm-26-b/scripts/assets/q1.template.html

186 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div id="q1-localization">
<style>
#q1-localization .q1-plot { width:100%; display:block; overflow:hidden; }
#q1-localization .q1-legend { display:flex; flex-wrap:wrap; gap:12px; margin-block:12px; }
#q1-localization .q1-legend span { display:inline-flex; align-items:center; gap:6px; }
#q1-localization .q1-swatch { display:inline-block; width:18px; height:3px; }
#q1-localization .q1-stats { display:flex; flex-wrap:wrap; gap:12px 24px; margin-block:12px; }
#q1-localization .q1-status { margin-block:8px; }
#q1-localization .q1-plot text { fill:var(--foreground); font-size:12px; font-weight:400; }
#q1-localization details { margin-block:12px; }
#q1-localization summary { cursor:pointer; }
</style>
<h2>带误差的交会定位</h2>
<div class="viz-controls">
<label class="form-label">观测案例
<select class="form-select" data-q1="case" aria-label="观测案例"></select>
</label>
<label class="form-label">视图
<select class="form-select" data-q1="view" aria-label="视图">
<option value="region">定位区域放大</option>
<option value="global">检测点全景</option>
</select>
</label>
<label class="form-label">已加入测点 <span data-q1="step-label" class="tabular-nums"></span>
<input class="form-range" data-q1="step" type="range" min="1" max="3" value="3" step="1" aria-label="已加入测点数量">
</label>
</div>
<div class="q1-stats tabular-nums" aria-live="polite" data-q1="stats"></div>
<svg class="q1-plot" data-q1="plot" role="img" aria-label="检测角域、定位多边形、直径线段和两个包围圆"></svg>
<div class="q1-legend text-small">
<span><i class="q1-swatch" style="background:var(--viz-series-1)"></i>定位区域</span>
<span><i class="q1-swatch" style="background:var(--viz-series-2)"></i>直径线段及直径圆</span>
<span><i class="q1-swatch" style="background:var(--viz-series-3)"></i>最小包围圆</span>
<span>□ 检测点 + 合成真值 ◎ 覆盖反例顶点</span>
</div>
<div class="q1-status text-small" data-q1="status" aria-live="polite"></div>
<details>
<summary>检测点与示向度</summary>
<div class="table-responsive">
<table class="table table-sm tabular-nums">
<thead><tr><th>测点</th><th class="text-end">x / m</th><th class="text-end">y / m</th><th class="text-end">示向度 / °</th><th class="text-end">误差半角 / °</th></tr></thead>
<tbody data-q1="rows"></tbody>
</table>
</div>
</details>
<script>
(() => {
const root=document.getElementById('q1-localization');
const cases=/*__Q1_DATA__*/;
const get=name=>root.querySelector('[data-q1="'+name+'"]');
const picker=get('case'), step=get('step'), view=get('view'), svg=get('plot');
const NS='http://www.w3.org/2000/svg';
const fmt=x=>Number.isFinite(x)?x.toLocaleString('zh-CN',{maximumFractionDigits:4}):'—';
const names={polygon:'有界多边形',segment:'线段',point:'单点',empty:'空集',unbounded:'无界区域'};
function node(tag,attrs={},parent=svg,text=null) {
const el=document.createElementNS(NS,tag);
Object.entries(attrs).forEach(([k,v])=>el.setAttribute(k,String(v)));
if(text!==null)el.textContent=text;
parent.appendChild(el);return el;
}
cases.forEach((c,i)=>picker.add(new Option(c.name,i)));
picker.value=String(Math.max(0,cases.findIndex(c=>c.source==='official_practice')));
function load() {
const c=cases[+picker.value];
step.max=String(c.measurements.length);step.value=step.max;draw();
}
function draw() {
const c=cases[+picker.value], n=+step.value, ms=c.measurements.slice(0,n), r=c.frames[n-1];
get('step-label').textContent=n+' / '+c.measurements.length;
get('stats').replaceChildren();
const values=[['区域',names[r.status]],['直径 D',r.diameter_infinite?'∞':fmt(r.diameter_m)+' m'],
['最小包围半径',r.minimum_enclosing_circle?fmt(r.minimum_enclosing_circle.radius_m)+' m':'—']];
values.forEach(([label,value])=>{const span=document.createElement('span');span.textContent=label+':'+value;get('stats').appendChild(span);});
const source=c.source==='official_practice'?'官方演练观测;真实源位置未公开':'本地合成案例';
get('status').textContent=source+' · '+(r.status==='unbounded'?'阴影只显示视窗内部分,真实区域无界':
r.status==='empty'?'这些观测在给定误差界下不相容':
'直径圆'+(r.diameter_circle.covers?'可以覆盖':'不能覆盖,最大超出 '+fmt(r.diameter_circle.max_excess_m)+' m')+
' · 清除半径余量 '+fmt(r.clearance_certificate.remaining_margin_m)+' m');
get('rows').replaceChildren();
ms.forEach((m,i)=>{
const tr=document.createElement('tr');
[String(i+1),fmt(m.x),fmt(m.y),fmt(m.bearing_deg),fmt(m.half_angle_deg)].forEach((v,j)=>{
const td=document.createElement('td');td.textContent=v;if(j)td.className='text-end';tr.appendChild(td);
});get('rows').appendChild(tr);
});
svg.replaceChildren();
const width=Math.max(280,root.getBoundingClientRect().width);
const height=Math.max(310,Math.min(440,width*.65));
svg.setAttribute('viewBox',`0 0 ${width} ${height}`);
svg.style.height=height+'px';
node('title',{},svg,c.name+':'+names[r.status]);
node('desc',{},svg,get('status').textContent);
const l=64,t=20,right=20,bottom=46,w=width-l-right,h=height-t-bottom;
const finite=['polygon','segment','point'].includes(r.status);
let pts=(view.value==='region'&&finite)?r.vertices.map(p=>[...p]):ms.map(m=>[m.x,m.y]);
if(!pts.length)pts=[[0,0]];
if(view.value==='global'&&finite)pts.push(...r.vertices);
if(c.target)pts.push(c.target);
if(finite&&view.value==='region') {
[r.diameter_circle,r.minimum_enclosing_circle].forEach(circle=>{
const [x,y]=circle.center,d=circle.radius_m;
pts.push([x-d,y-d],[x+d,y+d]);
});
}
const xs=pts.map(p=>p[0]),ys=pts.map(p=>p[1]);
const cx=(Math.min(...xs)+Math.max(...xs))/2, cy=(Math.min(...ys)+Math.max(...ys))/2;
let dx=Math.max(...xs)-Math.min(...xs),dy=Math.max(...ys)-Math.min(...ys);
const minSpan=finite&&view.value==='region'?Math.max(2,(r.diameter_m||0)*.08):200;
const scale=Math.min(w/Math.max(dx*1.3,minSpan),h/Math.max(dy*1.3,minSpan));
const xmin=cx-w/(2*scale),xmax=cx+w/(2*scale),ymin=cy-h/(2*scale),ymax=cy+h/(2*scale);
const X=x=>l+(x-xmin)*scale,Y=y=>t+h-(y-ymin)*scale;
const defs=node('defs');const clip=node('clipPath',{id:'q1-clip'},defs);
node('rect',{x:l,y:t,width:w,height:h},clip);
for(let i=0;i<4;i++) {
const x=xmin+(xmax-xmin)*i/3, y=ymin+(ymax-ymin)*i/3;
node('line',{x1:X(x),x2:X(x),y1:t,y2:t+h,stroke:'var(--border)'});
node('line',{x1:l,x2:l+w,y1:Y(y),y2:Y(y),stroke:'var(--border)'});
const tick=z=>Math.abs(z)>=1e6?z.toExponential(1):Math.abs(xmax-xmin)<20?z.toFixed(1):Math.round(z).toString();
node('text',{x:X(x),y:t+h+19,'text-anchor':i===0?'start':i===3?'end':'middle'},svg,tick(x));
node('text',{x:l-8,y:Y(y)+4,'text-anchor':'end'},svg,tick(y));
}
node('text',{x:l+w/2,y:height-7,'text-anchor':'middle',class:'axis-title','data-axis':'x'},svg,'x / m');
node('text',{x:14,y:t+h/2,'text-anchor':'middle',transform:`rotate(-90 14 ${t+h/2})`,class:'axis-title','data-axis':'y'},svg,'y / m');
const layer=node('g',{'clip-path':'url(#q1-clip)'});
let displayPoly;
if(finite)displayPoly=r.vertices;
else if(r.status==='empty')displayPoly=[];
else {
// Display clipping ONLY: Python's result remains explicitly unbounded.
displayPoly=[[xmin,ymin],[xmax,ymin],[xmax,ymax],[xmin,ymax]];
ms.forEach(m=>{
const theta=m.bearing_deg*Math.PI/180,d=m.half_angle_deg*Math.PI/180;
const normals=[[Math.sin(theta-d),-Math.cos(theta-d)],[-Math.sin(theta+d),Math.cos(theta+d)],[-Math.cos(theta),-Math.sin(theta)]];
normals.forEach(([a,b])=>{
const bound=a*m.x+b*m.y,out=[];
for(let i=0;i<displayPoly.length;i++) {
const p=displayPoly[i],q=displayPoly[(i+1)%displayPoly.length];
const f=a*p[0]+b*p[1]-bound,g=a*q[0]+b*q[1]-bound;
if(f<=1e-8)out.push(p);
if((f<0&&g>0)||(f>0&&g<0))out.push([p[0]+(q[0]-p[0])*f/(f-g),p[1]+(q[1]-p[1])*f/(f-g)]);
}displayPoly=out;
});
});
}
if(displayPoly.length>=3)node('polygon',{points:displayPoly.map(p=>X(p[0])+','+Y(p[1])).join(' '),fill:'var(--viz-series-1)','fill-opacity':.18,stroke:'var(--viz-series-1)','stroke-width':1.5},layer);
const reach=Math.hypot(xmax-xmin,ymax-ymin)+Math.max(...ms.map(m=>Math.hypot(m.x-cx,m.y-cy)))*2;
ms.forEach((m,i)=>{
[-m.half_angle_deg,0,m.half_angle_deg].forEach(offset=>{
const a=(m.bearing_deg+offset)*Math.PI/180;
node('line',{x1:X(m.x),y1:Y(m.y),x2:X(m.x+reach*Math.cos(a)),y2:Y(m.y+reach*Math.sin(a)),
stroke:'var(--foreground)','stroke-opacity':offset===0?.35:.22,'stroke-width':1,'stroke-dasharray':offset===0?'none':'4 4'},layer);
});
if(m.x>=xmin&&m.x<=xmax&&m.y>=ymin&&m.y<=ymax) {
node('rect',{x:X(m.x)-4,y:Y(m.y)-4,width:8,height:8,fill:'var(--foreground)'},layer);
node('text',{x:Math.min(l+w-22,X(m.x)+9),y:Math.max(t+14,Y(m.y)-9)},layer,'S'+(i+1));
}
});
if(finite) {
[r.diameter_circle,r.minimum_enclosing_circle].forEach((circle,i)=>{
node('circle',{cx:X(circle.center[0]),cy:Y(circle.center[1]),r:circle.radius_m*scale,
fill:'none',stroke:`var(--viz-series-${i+2})`,'stroke-width':2,'stroke-dasharray':i===0?'6 4':'none'},layer);
});
const [a,b]=r.diameter_pair;
node('line',{x1:X(a[0]),y1:Y(a[1]),x2:X(b[0]),y2:Y(b[1]),stroke:'var(--viz-series-2)','stroke-width':2.5},layer);
r.vertices.forEach((p,i)=>{
const mark=node('circle',{cx:X(p[0]),cy:Y(p[1]),r:4,fill:'var(--viz-series-1)'},layer);
node('title',{},mark,`V${i+1} (${fmt(p[0])}, ${fmt(p[1])})`);
});
const witness=r.diameter_circle.witness_vertex;
if(witness)node('circle',{cx:X(witness[0]),cy:Y(witness[1]),r:8,
fill:'none',stroke:'var(--viz-series-2)','stroke-width':2},layer);
}
if(c.target&&c.target[0]>=xmin&&c.target[0]<=xmax&&c.target[1]>=ymin&&c.target[1]<=ymax) {
const x=X(c.target[0]),y=Y(c.target[1]);
node('path',{d:`M${x-5},${y}h10 M${x},${y-5}v10`,stroke:'var(--foreground)','stroke-width':2},layer);
}
node('rect',{x:l,y:t,width:w,height:h,fill:'none',stroke:'var(--border)','data-chart-frame':''});
}
picker.addEventListener('change',load);step.addEventListener('input',draw);view.addEventListener('change',draw);
new ResizeObserver(draw).observe(root);
load();
})();
</script>
</div>