第一问建模完成
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<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>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,25 @@
|
||||
:root {
|
||||
color-scheme:light dark;
|
||||
--background:light-dark(#ffffff,#191c21);
|
||||
--foreground:light-dark(#24282e,#eceff3);
|
||||
--border:light-dark(#d9dfe5,#424a55);
|
||||
--muted-foreground:light-dark(#56616c,#b4bdc8);
|
||||
--viz-series-1:light-dark(#146e9a,#6cc8ed);
|
||||
--viz-series-2:light-dark(#c6503a,#ff947d);
|
||||
--viz-series-3:light-dark(#198064,#64d6ac);
|
||||
--font-size-base:14px;
|
||||
}
|
||||
body { max-width:1000px; margin:0 auto; padding:24px; background:var(--background);color:var(--foreground);font:var(--font-size-base)/1.6 system-ui,"Microsoft YaHei",sans-serif; }
|
||||
h2 {font-size:1.4em;font-weight:500;margin-block:0 18px;}
|
||||
.viz-controls {display:flex;gap:16px;flex-wrap:wrap;align-items:end;}
|
||||
.form-label {display:flex;flex-direction:column;gap:6px;flex:1 1 180px;min-width:0;}
|
||||
.form-select {font:inherit;color:var(--foreground);background:var(--background);border:1px solid var(--border);padding:8px;max-width:100%;border-radius:5px;}
|
||||
.form-range {width:100%;min-height:30px;accent-color:var(--viz-series-1);}
|
||||
.text-small {font-size:12px;}
|
||||
.tabular-nums {font-variant-numeric:tabular-nums;}
|
||||
.table-responsive {overflow-x:auto;}
|
||||
.table {width:100%;border-collapse:collapse;}
|
||||
.table th,.table td {padding:6px 8px;border-bottom:1px solid var(--border);}
|
||||
.table th {font-weight:500;text-align:left;}
|
||||
.table .text-end {text-align:right;}
|
||||
@media(max-width:480px) {body{padding:12px;}.form-label{flex-basis:100%;}.form-select{font-size:16px;}}
|
||||
Reference in New Issue
Block a user