就绪 k₁正常 t = — PPI — IM²C 2026 ⌨
主题
情景模式
k₁ 正常年份:标准威胁参数,季节调整生效。

人员 & 预算
总人员 N295
地面护林员 r₁160
快速响应队 r₇55
无人机 r₄10
消防设备 r₈★40
★ 灵敏度最高 S=+0.332
社区联络员 r₆35

威胁动态 ρ / φ
偷猎 ρ₁0.85
野火 φ₂0.40
人兽冲突 ρ₃0.70
栖息地 ρ₄0.90
偷猎衰减 α₁0.30
野火衰减 α₂0.10

模型约束
Sigmoid θ C16.0
协同 δ C40.10
卫星覆盖 x₅0.60
A区优先权重1.50
鲁棒性 Γ2
湿季车辆 C60.70

区域保护目标 P*
A区 犀牛核心0.91
B区 盐沼0.84
C区 边界缓冲0.82
D区 内部区0.72

资源效能 η
护林员 vs 偷猎0.55
消防设备 vs 野火0.75
快响队 vs 偷猎0.70
无人机 vs 偷猎0.65
联络员 vs 人兽0.60

地图视图
策略快速预设
[init]就绪,请调整参数并运行。
全园保护指数 P(g,t) 月份:  |  223单元格 · 每格10×10 km
100% 滚轮缩放·拖拽·双击复位
月份浏览 — 点击查看详情
右键图表 → 保存为PNG  |  右键地图 → 导出地图
🦏
0.925
黑犀牛 SII
IUCN CR · 旗舰物种
📊
0.691
PPI 饱和上限
ρ₁=0.85 持久偷猎天花板
🔥
+0.332
消防设备灵敏度 S
全局最高 — B区野火瓶颈
👥
390
最优人员编制
+32% → PPI 0.638
⚠️
0.550
E[PPI] 期望值
四情景概率加权均值
🧬
+2.0%
GA 优化增益
遗传算法 30代 vs 贪心
情景对比分析 295人基准编制
情景概率年均 PPIA区 PPI主要影响因子状态
k₁ 正常年份 0.500.6190.916 标准威胁,季节调整 基准
k₂ 极端干旱 0.200.4850.877 野火扩散 φ₂×1.8 警戒
k₃ 执法松弛 0.200.5060.845 反偷猎效能×0.7 危险
k₄ 复合威胁 0.100.4220.796 干旱 + 执法松弛叠加 严峻
★ 最优 390人 0.6380.929 GA 优化区域权重 最优
关键发现:执法松弛(PPI=0.506)比极端干旱(PPI=0.485)危害更持久 — 制度因素重于自然灾害
核心模型方程速查 MLGDO 框架
威胁演化方程
Hs(g,t+1) = ρs·H̃s(g,t) + Σg' φs·H̃s(g',t)·e−αsd + εks
局部持久 + 空间扩散 + 随机扰动
保护度 sigmoid 映射
ps(g,t) = σ( θ · (Hsafes − H̃s) / (Hsafes + ε) )
θ=6,双阈值:Hsafe对应p=0.5,Htarget对应p≥0.88
互补概率联合削减
Δtotals(g,t) = 1 − Πr(1 − Δr,s(g,t))
各资源协同非线性叠加
人员饱和曲线(拟合)
PPI(N) = 0.6910 · (1 − e−0.00657N)
N≈450 后边际收益急剧递减
物种重要性指数
SII(m) = 0.40·End + 0.30·Eco + 0.15·Endem + 0.15·Vul
黑犀牛 SII=0.925(最高)
无人机–快响协同
ηeffr4,s1 = ηr4,s1 · (1 + δsyn·𝟙[r7共部署])
δsyn=0.10,共部署时效能+10%
基准情景 k₁ 月度 PPI 速查表 295人 · 正常年份
月份 1月2月3月4月5月6月 7月8月9月10月11月12月 年均目标P*
A区 0.880.890.900.910.920.92 0.930.930.930.930.930.93 0.9160.91
B区 0.620.620.630.630.640.64 0.650.650.650.650.640.65 0.6380.84
C区 0.480.490.510.520.530.54 0.540.540.540.540.540.54 0.5220.82
D区 0.140.140.150.150.160.16 0.160.160.160.160.160.16 0.1540.72
全园 0.5550.5590.5720.5770.5880.592 0.5960.5970.5990.6000.5990.601 0.5780.843
🌞 干季(6–10月):车辆效能降低,护林员巡逻增加10%,无人机配额+1/月。   🌧 湿季(11–2月):车辆预算削减30%,转向传感器与无人机监测。
🌍 MLGDO框架迁移对比 — 三园区 §8 迁移应用
参数 埃托沙(纳米比亚) 克鲁格(南非) 亚马逊(巴西)
面积22,935 km²19,485 km²~994,000 km²
主要威胁偷猎·野火跨境偷猎 ρ=0.88非法采矿·伐木
无人机效能 η0.650.70(开阔草原)0.40(冠层遮蔽)
卫星依赖度0.600.650.85(冠层限无人机)
旗舰物种 SII黑犀牛 0.925白/黑犀牛 0.91亚马逊河豚 ~0.82
E[PPI] 预测0.5500.57–0.620.45–0.55
建议编制390人420–480人1,200–2,000人†
新增资源类型+空中侦察艇+河流巡逻艇 r₉
地形阻抗κ(特殊)盐沼 2.5灌丛 1.8密林 4.0 · 河流 3.5
不变核心(5项):Sigmoid映射 · 双阈值框架 · 互补概率削减 · 情景树(K=4) · 三层GA优化
🔧 可调参数:网格尺寸κ · 威胁类型(ρ,φ,α) · 效能矩阵η · SII物种权重 · 区域阈值q*
† 亚马逊估算基于分散枢纽巡逻艇模型,与护林员制公园不可直接比较
📅 12月行动计划(基于埃托沙降水与威胁季节数据)
加载中…
综合保护指数 PPI
01
期望值 E[PPI]
Σ pₖ·PPIₖ
运行仿真后显示
区域保护值 vs 目标
干预后平均威胁 H̄ₛ
灵敏度指数 Sₓ(390人基准)
决策变量 xᵣ,ᵍ — 区域合计
请先运行仿真。
🎯 政策建议
运行仿真后自动生成
📋 运行历史对比
暂无历史记录
🦏 物种风险联动评估
运行仿真后更新
💰 成本效益分析
运行仿真后更新
单元格信息
区域
P(g,t)
目标 P*
差距
H̃ 偷猎/野火/人兽/栖息
生物价值
无人机/快响/护林员
`; const blob=new Blob([html],{type:'text/html'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`etosha_report_${SIM.scenario+1}_N${SIM.staff}_${new Date().toISOString().slice(0,10)}.html`; a.click(); URL.revokeObjectURL(a.href); log('✓ 年度报告已生成(HTML格式,可直接打印/转PDF)','n'); } // ── RESET PARAMS ────────────────────────────────────────────────── const PARAM_DEFAULTS={ staff:295, r1:160, r7:55, r4:10, r8:40, r6:35, rho1:0.85, phi2:0.40, rho3:0.70, rho4:0.90, alpha1:0.30, alpha2:0.10, }; function resetParams(){ Object.entries(PARAM_DEFAULTS).forEach(([key,val])=>{ const sl=document.getElementById(`sl-${key}`); const vl=document.getElementById(`v-${key}`); if(sl){ sl.value=val; if(vl) vl.textContent=val; } }); syncParamsFromUI(); log('参数已重置为默认值','n'); livePreview(); } // ── SCENARIO COMPARISON (9th chart) ─────────────────────────────── // Stores per-scenario full 12-month PPI arrays for overlay const SCEN_CACHE={}; // k→{ppiHist, label, col} const SCEN_COLS=['#2d7a52','#c47a18','#c0392b','#7040a8']; const SCEN_LBLS=['k₁正常','k₂干旱','k₃懈怠','k₄复合']; async function compareScenarios(){ if(SIM.running){ log('仿真运行中,请等待','w'); return; } const btn=document.getElementById('btn-compare'); if(btn){ btn.disabled=true; btn.textContent='运行中…'; } try { log('▶ 开始四情景横向对比…','n'); const prevScen=SIM.scenario; const prevH=[...SIM.runHistory]; for(let k=0;k<4;k++){ SIM.scenario=k; resetH(); const hist=[]; for(let t=0;t<12;t++){ const {ppi}=stepOnce(t); hist.push(ppi); await new Promise(r=>setTimeout(r,0)); } SCEN_CACHE[k]={ppiHist:hist,label:SCEN_LBLS[k],col:SCEN_COLS[k]}; log(` ${SCEN_LBLS[k]}: 年均PPI=${(hist.reduce((a,b)=>a+b,0)/12).toFixed(4)}`,'n'); } SIM.scenario=prevScen; SIM.runHistory=prevH; // Switch to compare chart tab const btn2=document.querySelector(".ctab[onclick*='compare']"); if(btn2) swChart('compare',btn2); else drawCompareChart(); log('✓ 四情景对比完成','n'); if(btn){ btn.disabled=false; btn.textContent='⚖️ 四情景对比'; } } catch(err){ log(`⚠ 对比错误: ${err.message||err}`,'a'); console.error(err); } finally { if(btn){ btn.disabled=false; btn.textContent='⚖️ 四情景对比'; } } } function drawCompareChart(){ const cc=getChartColors(); const cv=document.getElementById('ch-compare'); if(!cv) return; const W=getChartW(); cv.width=W; cv.height=148; const ctx=cv.getContext('2d'); ctx.fillStyle=cc.chartBg; ctx.fillRect(0,0,W,148); if(!Object.keys(SCEN_CACHE).length){ ctx.fillStyle=cc.chartTick; ctx.font='11px Outfit'; ctx.textAlign='center'; ctx.fillText('点击左侧「四情景对比」按钮运行',W/2,78); return; } const ML=36,MR=W-10,MT=14,MB=118,PW=MR-ML,PH=MB-MT; const xp=t=>ML+t/11*PW, yp=v=>MB-((v-0)/(1-0))*PH; const MON2=['1','2','3','4','5','6','7','8','9','10','11','12']; // Grid ctx.strokeStyle=cc.chartGrid; ctx.lineWidth=.6; [0,.2,.4,.6,.8,1].forEach(f=>{ const y=yp(f); ctx.beginPath(); ctx.moveTo(ML,y); ctx.lineTo(MR,y); ctx.stroke(); ctx.fillStyle=cc.chartTick; ctx.font='8px DM Mono'; ctx.textAlign='right'; ctx.fillText(f.toFixed(1),ML-3,y+3); }); // Month labels MON2.forEach((m,t)=>{ ctx.fillStyle=cc.chartTick; ctx.font='8px DM Mono'; ctx.textAlign='center'; ctx.fillText(m,xp(t),MB+12); }); // Target band const dynTgt=P.ztgt.reduce((a,v,z)=>a+v*ZCNT[z],0)/NC; ctx.strokeStyle='rgba(31,107,62,.35)'; ctx.lineWidth=1; ctx.setLineDash([4,3]); ctx.beginPath(); ctx.moveTo(ML,yp(dynTgt)); ctx.lineTo(MR,yp(dynTgt)); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle='rgba(31,107,62,.55)'; ctx.font='7.5px DM Mono'; ctx.textAlign='right'; ctx.fillText(`目标${dynTgt.toFixed(3)}`,MR,yp(dynTgt)-3); // Lines per scenario Object.values(SCEN_CACHE).forEach(({ppiHist,label,col},k)=>{ ctx.beginPath(); ctx.strokeStyle=col; ctx.lineWidth=2; ppiHist.forEach((v,t)=>t===0?ctx.moveTo(xp(t),yp(v)):ctx.lineTo(xp(t),yp(v))); ctx.stroke(); // End-point label const last=ppiHist[11]; ctx.fillStyle=col; ctx.font='bold 7.5px DM Mono'; ctx.textAlign='left'; ctx.fillText(`${label}=${last.toFixed(3)}`,xp(11)+4,yp(last)+3); }); // Legend row let lx=ML; const ly=MB+28; Object.values(SCEN_CACHE).forEach(({label,col})=>{ ctx.fillStyle=col; ctx.fillRect(lx,ly-5,16,3); ctx.fillStyle=cc.chartInk; ctx.font='8px DM Mono'; ctx.textAlign='left'; ctx.fillText(label,lx+19,ly); lx+=ctx.measureText(label).width+32; }); // Axes ctx.strokeStyle='#c4bba8'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(ML,MT); ctx.lineTo(ML,MB); ctx.lineTo(MR,MB); ctx.stroke(); // Title ctx.fillStyle=cc.chartInk; ctx.font='bold 8px DM Mono'; ctx.textAlign='center'; ctx.fillText(`四情景PPI对比 (N=${SIM.staff||295}人)`,W/2,11); } // ── THEME ENGINE ────────────────────────────────────────────────── const THEME_DEFS = { savanna: { chartBg:'#faf8f4', chartGrid:'#e5e0d6', chartTick:'#968e7e', chartInk:'#3a3428', cellEmpty:'#e5e0d6' }, ivory: { chartBg:'#ffffff', chartGrid:'#e8e8e0', chartTick:'#909090', chartInk:'#282828', cellEmpty:'#e8e8e0' }, slate: { chartBg:'#f8f9fb', chartGrid:'#dde2ec', chartTick:'#7a8098', chartInk:'#1c2030', cellEmpty:'#dde2ec' }, forest: { chartBg:'#f4f8f2', chartGrid:'#ccddc4', chartTick:'#6a8060', chartInk:'#142010', cellEmpty:'#ccddc4' }, dusk: { chartBg:'#242220', chartGrid:'#3a3630', chartTick:'#887e72', chartInk:'#c8c0b4', cellEmpty:'#3a3630' }, }; let _theme = 'savanna'; function getChartColors() { return THEME_DEFS[_theme] || THEME_DEFS.savanna; } function setTheme(name) { if (!THEME_DEFS[name]) return; _theme = name; document.documentElement.setAttribute('data-theme', name); // Update active dot document.querySelectorAll('.theme-dot').forEach(d => d.classList.toggle('active', d.dataset.t === name) ); // Update label const lb = document.getElementById('theme-label'); const names = {savanna:'草原沙地',ivory:'象牙白',slate:'蓝石板',forest:'深林绿',dusk:'黄昏暗'}; if (lb) lb.textContent = names[name] || name; // Persist try { localStorage.setItem('mlgdo_theme', name); } catch(e){} // Redraw all visible charts + map drawGrid(); const activeTab = document.querySelector('.ctab.on'); if (activeTab) activeTab.click(); // Redraw static charts setTimeout(() => { drawSatChart(); drawEtaChart(); drawRobustChart(); if (Object.keys(SCEN_CACHE).length) drawCompareChart(); }, 30); } // ── INIT ───────────────────────────────────────────────────────── (function init(){ // Restore saved theme before first render try{ const saved=localStorage.getItem('mlgdo_theme')||'savanna'; _theme=saved; document.documentElement.setAttribute('data-theme',saved); document.querySelectorAll('.theme-dot').forEach(d=>d.classList.toggle('active',d.dataset.t===saved)); const lb=document.getElementById('theme-label'); const TN={savanna:'草原沙地',ivory:'象牙白',slate:'蓝石板',forest:'深林绿',dusk:'黄昏暗'}; if(lb) lb.textContent=TN[saved]||saved; }catch(e){} buildTimeline(false); resetH(); const al=allocate(H_st,getBudget(0)); const dlt=computeDelta(H_st,al,0); const hT=applyDelta(H_st,dlt); const {ppi,zPPI,P:Pc}=computePPI(hT); SIM.alloc=al; SIM.hTilde=hT; SIM.ppi=ppi; SIM.zPPI=zPPI; SIM.Pcell=Pc; refreshDisplay(); document.getElementById('b-ppi').textContent=`PPI ${ppi.toFixed(3)}`; log(`公园:${NC} 单元格 | A:${ZCNT[0]} B:${ZCNT[1]} C:${ZCNT[2]} D:${ZCNT[3]} | 预览PPI=${ppi.toFixed(4)}`,'n'); log('调整参数后点击「运行」按钮开始12个月仿真','w'); })(); window.addEventListener('resize',()=>{ _chartW=0; if(SIM.Pcell){ drawGrid(); refreshDisplay(); } }); // ═══════════════════════════════════════════════════════════════════ // v12 NEW FEATURES: Heatmap · Robustness · EtaMatrix · GA · Policy · CSV // ═══════════════════════════════════════════════════════════════════ // ── roundRect polyfill (Safari / older Chrome) ────────────────── if(!CanvasRenderingContext2D.prototype.roundRect){ CanvasRenderingContext2D.prototype.roundRect=function(x,y,w,h,r){ r=Math.min(r,w/2,h/2); this.beginPath(); this.moveTo(x+r,y); this.lineTo(x+w-r,y); this.arcTo(x+w,y,x+w,y+r,r); this.lineTo(x+w,y+h-r); this.arcTo(x+w,y+h,x+w-r,y+h,r); this.lineTo(x+r,y+h); this.arcTo(x,y+h,x,y+h-r,r); this.lineTo(x,y+r); this.arcTo(x,y,x+r,y,r); this.closePath(); return this; }; } function chartSetup(id, h){ const cv=document.getElementById(id); const W=getChartW(); cv.width=W; cv.height=h; const ctx=cv.getContext('2d'); ctx.fillStyle='#faf8f4'; ctx.fillRect(0,0,W,h); return {cv,ctx,W,H:h}; } function noSimMsg(ctx,W,H,msg='请先运行仿真'){ ctx.fillStyle='#968e7e'; ctx.font='11px DM Mono'; ctx.textAlign='center'; ctx.fillText(msg,W/2,H/2+4); } function heatColor(v, tgt){ // red→yellow→green, anchored at target if(v>=tgt) return `rgb(${Math.round(lerp(200,60,Math.min(1,(v-tgt)/(1-tgt))))},${Math.round(lerp(160,190,Math.min(1,(v-tgt)/(1-tgt))))},80)`; const f=v/tgt; return `rgb(${Math.round(lerp(210,230,f))},${Math.round(lerp(55,170,f))},55)`; } // ── ZONE × MONTH HEATMAP ───────────────────────────────────────── function drawHeatChart(){ const cc=getChartColors(); const {cv,ctx,W,H}=chartSetup('ch-heat',164); if(!SIM.zHist.length){ noSimMsg(ctx,W,H); return; } const ML=46,MR=W-38,MT=24,MB=H-24; const T=SIM.zHist.length, NZ=4; const cW=(MR-ML)/T, cH=(MB-MT)/NZ; // Title ctx.fillStyle=cc.chartInk; ctx.font='bold 8.5px DM Mono'; ctx.textAlign='center'; ctx.fillText('区域 × 月份 PPI热力图(红框=低于目标,黄底=干季)',W/2,14); // Month labels MON.slice(0,T).forEach((m,t)=>{ ctx.fillStyle=cc.chartTick; ctx.font='7px DM Mono'; ctx.textAlign='center'; ctx.fillText(m.slice(0,3), ML+t*cW+cW/2, MT-5); }); // Zone labels + row mean (right side) ['A区','B区','C区','D区'].forEach((n,z)=>{ ctx.fillStyle=ZCOL[z]; ctx.font='bold 8px Sora'; ctx.textAlign='right'; ctx.fillText(n, ML-4, MT+z*cH+cH/2+3.5); // row mean const rowMean=SIM.zHist.reduce((a,r)=>a+r[z],0)/T; const ok=rowMean>=P.ztgt[z]; ctx.fillStyle=ok?'var(--grn)':'#c0392b'; ctx.font='bold 7.5px DM Mono'; ctx.textAlign='left'; ctx.fillText(rowMean.toFixed(2), MR+4, MT+z*cH+cH/2+3.5); }); // Column mean labels (bottom) ctx.fillStyle=cc.chartInk; ctx.font='6px DM Mono'; ctx.textAlign='center'; for(let t=0;ta+v,0)/NZ; ctx.fillStyle=colMean>=0.60?'#2d7a52':colMean>=0.54?'#c47a18':'#c0392b'; ctx.fillText(colMean.toFixed(2), ML+t*cW+cW/2, MB+10); } // Dry season tint [5,6,7,8,9].filter(t=>t{ ctx.fillStyle='rgba(200,160,0,.10)'; ctx.fillRect(ML+t*cW,MT,cW,MB-MT); }); // Cells for(let z=0;z18){ ctx.fillStyle='rgba(255,255,255,.90)'; ctx.font='6.5px DM Mono'; ctx.textAlign='center'; ctx.fillText(v.toFixed(2), x+w/2, y+h/2+2.5); } } } ctx.strokeStyle='#b8b0a0'; ctx.lineWidth=.8; ctx.strokeRect(ML,MT,MR-ML,MB-MT); // Row "均值" header ctx.fillStyle=cc.chartTick; ctx.font='6.5px DM Mono'; ctx.textAlign='left'; ctx.fillText('均值', MR+4, MT-5); // Gradient legend const lx=ML,ly=MB+14,lw=MR-ML-50; const g=ctx.createLinearGradient(lx,0,lx+lw,0); g.addColorStop(0,'rgb(210,55,55)'); g.addColorStop(.5,'rgb(230,170,55)'); g.addColorStop(1,'rgb(60,190,80)'); ctx.fillStyle=g; ctx.fillRect(lx,ly,lw,4); ctx.fillStyle=cc.chartTick; ctx.font='7px DM Mono'; ctx.textAlign='left'; ctx.fillText('低',lx,ly+11); ctx.textAlign='right'; ctx.fillText('高',lx+lw,ly+11); // Breach count summary let breachCount=0; for(let z=0;z=90?'#2d7a52':pctOk>=70?'#c47a18':'#c0392b'; ctx.font='bold 8px DM Mono'; ctx.textAlign='right'; ctx.fillText(`达标率 ${pctOk}% (${totalCells-breachCount}/${totalCells})`, MR+34, ly+11); } // ── ROBUSTNESS CURVE (Γ sweep) ──────────────────────────────────── function drawRobustChart(){ const cc=getChartColors(); const {cv,ctx,W,H}=chartSetup('ch-robust',128); const ML=42,MR=W-14,MT=14,MB=108; const PW=MR-ML, PH=MB-MT; const pMax=0.6910, lam=0.00657; // Model: PPI degrades under Γ simultaneous 20% effectiveness hits const robPPI=(N,gam)=>Math.max(0, pMax*(1-Math.exp(-lam*N))*(1-gam*0.07)); const staffs=[ {N:240,col:'#c0392b',lbl:'240(底线)'}, {N:295,col:'#c47a18',lbl:'295(当前)'}, {N:390,col:'#1f6b3e',lbl:'390(最优)'}, {N:520,col:'#2970a8',lbl:'520(高级)'}, ]; const gamArr=[0,.5,1,1.5,2,2.5,3]; const xp=g=>ML+g/3*PW, yp=v=>MB-((v-.3)/.42)*PH; // grid ctx.setLineDash([2,4]); ctx.strokeStyle='#ddd8cf'; ctx.lineWidth=.7; [.35,.45,.55,.65].forEach(v=>{ ctx.beginPath(); ctx.moveTo(ML,yp(v)); ctx.lineTo(MR,yp(v)); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle=cc.chartTick; ctx.font='7.5px DM Mono'; ctx.textAlign='right'; ctx.fillText(v.toFixed(2),ML-2,yp(v)+3); ctx.setLineDash([2,4]); }); gamArr.forEach(g=>{ ctx.beginPath(); ctx.moveTo(xp(g),MT); ctx.lineTo(xp(g),MB); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle=cc.chartTick; ctx.font='7.5px DM Mono'; ctx.textAlign='center'; ctx.fillText('Γ='+g,xp(g),MB+10); ctx.setLineDash([2,4]); }); ctx.setLineDash([]); // safety floor const sf=0.535; ctx.strokeStyle='#c47a18cc'; ctx.lineWidth=1.2; ctx.setLineDash([5,3]); ctx.beginPath(); ctx.moveTo(ML,yp(sf)); ctx.lineTo(MR,yp(sf)); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle='#c47a18'; ctx.font='7px DM Mono'; ctx.textAlign='left'; ctx.fillText('安全底线',ML+2,yp(sf)-3); // design Γ=2 ctx.strokeStyle='#1f6b3e44'; ctx.lineWidth=1; ctx.setLineDash([3,4]); ctx.beginPath(); ctx.moveTo(xp(2),MT); ctx.lineTo(xp(2),MB); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle='#1f6b3e'; ctx.font='7px DM Mono'; ctx.textAlign='center'; ctx.fillText('设计点',xp(2),MT+10); // curves const curN=SIM.staff||295, curG=P.gamma||2; const curPPI=SIM.ppiHist.length? SIM.ppiHist.reduce((a,b)=>a+b,0)/SIM.ppiHist.length : robPPI(curN,curG); staffs.forEach(({N,col,lbl})=>{ ctx.beginPath(); ctx.strokeStyle=col; ctx.lineWidth=N===390?2.5:1.8; gamArr.forEach((g,gi)=>{ const y=yp(robPPI(N,g)); gi===0?ctx.moveTo(xp(g),y):ctx.lineTo(xp(g),y); }); ctx.stroke(); ctx.fillStyle=col; ctx.font='7px DM Mono'; ctx.textAlign='left'; ctx.fillText(lbl, xp(3)+3, yp(robPPI(N,3))+3); }); // current run dot marker ctx.beginPath(); ctx.arc(xp(curG),yp(curPPI),6,0,2*Math.PI); ctx.fillStyle='#c47a18'; ctx.fill(); ctx.strokeStyle='#fff'; ctx.lineWidth=2; ctx.stroke(); ctx.fillStyle='#c47a18'; ctx.font='bold 7px DM Mono'; ctx.textAlign='center'; ctx.fillText(`${curN}人 Γ=${curG}`, xp(curG), yp(curPPI)-9); // axes labels ctx.fillStyle=cc.chartInk; ctx.font='bold 8px DM Mono'; ctx.textAlign='center'; ctx.fillText('PPI vs 不确定性预算 Γ(Bertsimas–Sim,各编制水平)',W/2,12); ctx.save(); ctx.rotate(-Math.PI/2); ctx.fillStyle=cc.chartTick; ctx.font='7.5px DM Mono'; ctx.textAlign='center'; ctx.fillText('均值PPI',-(MT+MB)/2,9); ctx.restore(); // Legend: N levels const robLgd=[['#c47a18','N=200'],['#a09488','N=295'],['#2d7a52','N=390'],['#2970a8','N=450']]; let lx2=ML; const ly2=MB+26; robLgd.forEach(([col,lbl])=>{ ctx.strokeStyle=col; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(lx2,ly2); ctx.lineTo(lx2+14,ly2); ctx.stroke(); ctx.fillStyle=cc.chartInk; ctx.font='7.5px DM Mono'; ctx.textAlign='left'; ctx.fillText(lbl,lx2+17,ly2+3); lx2+=ctx.measureText(lbl).width+32; }); } // ── RESOURCE EFFECTIVENESS MATRIX ──────────────────────────────── function drawEtaChart(){ const cc=getChartColors(); const {cv,ctx,W,H}=chartSetup('ch-eta',150); const ML=70,MR=W-10,MT=26,MB=H-14; const cW=(MR-ML)/4, cH=(MB-MT)/8; const rNames=['r₁ 护林员','r₂ 巡逻车','r₃ 传感器','r₄ 无人机','r₅ 卫星','r₆ 联络员','r₇ 快响队','r₈ 消防']; const sNames=['s₁ 偷猎','s₂ 野火','s₃ 人兽冲突','s₄ 栖息地']; const sCols=['#c0392b','#c47a18','#7040a8','#2d7a52']; // sensitivity values from paper (Table 13) const sens=[0.022, 0.015, 0.078, 0.016, 0.020, 0.056, 0.047, 0.332]; // title ctx.fillStyle=cc.chartInk; ctx.font='bold 8px DM Mono'; ctx.textAlign='center'; ctx.fillText('资源效能矩阵 ηᵣₛ (色深=效能值,框=列最大,右=灵敏度Sₓ)',W/2,13); // col headers sNames.forEach((s,si)=>{ ctx.fillStyle=sCols[si]; ctx.font='bold 8px Sora'; ctx.textAlign='center'; ctx.fillText(s, ML+si*cW+cW/2, MT-6); }); // per-column maxima const colMax=[0,1,2,3].map(si=>Math.max(...P.eta.map(r=>r[si]))); P.eta.forEach((row,ri)=>{ // row label ctx.fillStyle=cc.chartInk; ctx.font='8px Sora'; ctx.textAlign='right'; ctx.fillText(rNames[ri], ML-3, MT+ri*cH+cH/2+3.5); row.forEach((v,si)=>{ const x=ML+si*cW+1, y=MT+ri*cH+1, w=cW-2, h=cH-2; const [r2,g2,b2]=[parseInt(sCols[si].slice(1,3),16),parseInt(sCols[si].slice(3,5),16),parseInt(sCols[si].slice(5,7),16)]; ctx.fillStyle=`rgba(${r2},${g2},${b2},${0.07+v*0.65})`; ctx.fillRect(x,y,w,h); ctx.strokeStyle='#ddd8d0'; ctx.lineWidth=.4; ctx.strokeRect(x,y,w,h); if(v===colMax[si]&&v>0){ ctx.strokeStyle=sCols[si]; ctx.lineWidth=1.6; ctx.strokeRect(x+.5,y+.5,w-1,h-1); } ctx.fillStyle=v>0.4?'rgba(255,255,255,.88)':'#2d2820'; ctx.font=(v>0?'bold ':'')+'7.5px DM Mono'; ctx.textAlign='center'; ctx.fillText(v>0?v.toFixed(2):'—', x+w/2, y+h/2+3); }); // sensitivity bar const sx=MR+4, sy=MT+ri*cH+2, bMax=Math.min(W-MR-6,40); const bw=Math.round(sens[ri]/0.332*bMax); ctx.fillStyle=sens[ri]>0.1?'#1f6b3e':sens[ri]>0.03?'#c47a18':'#8a8070'; ctx.fillRect(sx,sy,bw,cH-4); ctx.fillStyle=cc.chartInk; ctx.font='6.5px DM Mono'; ctx.textAlign='left'; ctx.fillText('+'+sens[ri].toFixed(3), sx+bw+1, sy+cH/2+1); }); ctx.strokeStyle='#b8b0a0'; ctx.lineWidth=.8; ctx.strokeRect(ML,MT,MR-ML,MB-MT); // header for sens column ctx.fillStyle=cc.chartTick; ctx.font='7px DM Mono'; ctx.textAlign='left'; ctx.fillText('Sₓ',MR+4,MT-6); } // ── GENETIC ALGORITHM OPTIMISATION ─────────────────────────────── async function runGA(){ if(!SIM.ppiHist.length){ log('请先运行基准仿真,再执行GA优化','w'); return; } const btn=document.getElementById('btn-ga'); btn.disabled=true; btn.textContent='⏳ GA运行中…'; try { const gprog=document.getElementById('ga-prog'), gprogf=document.getElementById('ga-progf'); if(gprog){ gprog.style.display='block'; gprogf.style.width='0%'; } log('遗传算法启动(种群40,世代30)','n'); const POP=40, GENS=30; const baseZW=[...ZW]; function evalW(w){ w.forEach((v,z)=>ZW[z]=v); let H=new Float32Array(NC*4); for(let i=0;iZW[z]=v); return tot/12; } let pop=Array.from({length:POP},()=>{ let w=Array.from({length:4},()=>Math.random()+0.05); const s=w.reduce((a,b)=>a+b,0); return w.map(v=>v/s); }); pop[0]=[0.35,0.25,0.25,0.15]; pop[1]=[0.34,0.167,0.32,0.173]; let bestW=[...pop[0]], bestPPI=0; pop.forEach(w=>{ const p=evalW(w); if(p>bestPPI){bestPPI=p;bestW=[...w];} }); const genHistory=[]; for(let gen=0;gensetTimeout(r,0)); // yield to UI const scored=pop.map(w=>({w,p:evalW(w)})).sort((a,b)=>b.p-a.p); if(scored[0].p>bestPPI){bestPPI=scored[0].p; bestW=[...scored[0].w];} genHistory.push(bestPPI); const sig=0.06*(1-gen/GENS); const next=Array.from({length:POP},()=>{ const a=scored[Math.floor(Math.random()*8)].w; const b=scored[Math.floor(Math.random()*8)].w; const cut=1+Math.floor(Math.random()*2); let child=[...a.slice(0,cut),...b.slice(cut)]; child=child.map(v=>Math.max(0.05,v+(Math.random()-.5)*2*sig)); const s=child.reduce((a2,b2)=>a2+b2,0); return child.map(v=>v/s); }); pop=next; if(gprogf) gprogf.style.width=((gen+1)/GENS*100)+'%'; if(gen===9||gen===19||gen===29) log(` 第${gen+1}代 最优PPI=${bestPPI.toFixed(4)}`,'n'); } SIM.gaGenHistory=genHistory; const basePPI=SIM.ppiHist.reduce((a,b)=>a+b,0)/SIM.ppiHist.length; const gain=((bestPPI-basePPI)/basePPI*100); SIM.gaResult={w:bestW, ppi:bestPPI, gain:gain.toFixed(1)}; if(gprog) gprog.style.display='none'; log(`✓ GA完成: wA=${bestW[0].toFixed(3)} wB=${bestW[1].toFixed(3)} wC=${bestW[2].toFixed(3)} wD=${bestW[3].toFixed(3)}`,'n'); log(` 增益 +${gain.toFixed(1)}% (${basePPI.toFixed(4)} → ${bestPPI.toFixed(4)})`,'n'); drawGAConvergence(genHistory); bestW.forEach((v,z)=>ZW[z]=v); btn.disabled=false; btn.textContent='🧬 GA优化'; SIM.gaApplied=true; runSim(); } catch(err){ log(`⚠ GA错误: ${err.message||err}`,'a'); console.error(err); } finally { btn.disabled=false; btn.textContent='🧬 GA优化'; } } function updatePolicyPanel(){ if(!SIM.ppiHist||!SIM.ppiHist.length) return; const panel=document.getElementById('policy-panel'); if(!panel) return; if(!SIM.ppiHist.length){ panel.innerHTML='
运行仿真后自动生成
'; return; } const ppi=SIM.ppiHist.reduce((a,b)=>a+b,0)/SIM.ppiHist.length; const zMean=SIM.zHist.reduce((acc,r)=>r.map((v,i)=>acc[i]+v/SIM.zHist.length),[0,0,0,0]); const gaps=zMean.map((v,z)=>P.ztgt[z]-v); const staffN=SIM.staff||295; const recs=[]; // Overall PPI status if(ppi<0.535) recs.push({l:'bad', i:'🚨', t:`全园PPI=${ppi.toFixed(3)},低于安全底线0.535!立即增援`}); else if(ppi<0.580) recs.push({l:'warn',i:'⚠️', t:`PPI=${ppi.toFixed(3)},低于基准,建议扩编至390人`}); else if(ppi<0.638) recs.push({l:'warn',i:'📈', t:`PPI=${ppi.toFixed(3)},尚有优化空间(目标0.638)`}); else recs.push({l:'good',i:'✅', t:`PPI=${ppi.toFixed(3)},达到最优目标`}); // Zone A (rhino core) if(gaps[0]>0.01) recs.push({l:'bad', i:'🦏', t:`A区低于目标 Δ=${gaps[0].toFixed(3)},加强无人机+快响队协同`}); else recs.push({l:'good',i:'🦏', t:`A区PPI=${zMean[0].toFixed(3)},犀牛核心区达标`}); // Zone B wildfire if(gaps[1]>0.15) recs.push({l:'bad', i:'🔥', t:`B区野火缺口Δ=${gaps[1].toFixed(3)}(最高灵敏度S=0.332),优先增配消防`}); else if(gaps[1]>0.05) recs.push({l:'warn',i:'🔥', t:`B区野火缺口Δ=${gaps[1].toFixed(3)},干季前预置消防设备`}); // Zone C HWC if(gaps[2]>0.1) recs.push({l:'warn',i:'🏘️', t:`C区人兽冲突缺口Δ=${gaps[2].toFixed(3)},增派社区联络员(S=0.056)`}); // Scenario-specific const scMsgs=['','⚠️ 干旱情景: 野火φ₂×1.8,干季前预置消防资源','⚠️ 执法松弛: 效能-30%,比极端干旱破坏更持久','⛔ 复合威胁: 最坏情景,建议启动应急协议']; if(SIM.scenario>0) recs.push({l:SIM.scenario===3?'bad':'warn',i:['','🌵','⚖️','💥'][SIM.scenario],t:scMsgs[SIM.scenario].slice(2)}); // Synergy check if(SIM.alloc){ const hasDrone=CELLS.some((_,i)=>SIM.alloc[i*NR+3]>=1); const hasRRT =CELLS.some((_,i)=>SIM.alloc[i*NR+6]>=1); if(hasDrone&&hasRRT) recs.push({l:'good',i:'⚡',t:'无人机×快响队协同激活(+10%效能)'}); } // GA result if(SIM.gaResult) recs.push({l:'good',i:'🧬',t:`GA已优化区域权重,PPI增益+${SIM.gaResult.gain}%`}); // Staffing const ppiAt390=0.6910*(1-Math.exp(-0.00657*390)); const ppiAt295=0.6910*(1-Math.exp(-0.00657*295)); if(staffN<390) recs.push({l:'warn',i:'👥',t:`建议扩编至390人 (+${390-staffN}人),预期PPI↑${(ppiAt390-ppiAt295).toFixed(3)}`}); if(staffN>450) recs.push({l:'good',i:'📊',t:'人员超450,边际收益递减,建议转向传感器/卫星技术'}); // Critical month detection if(SIM.ppiHist.length===12){ const critMonth=SIM.ppiHist.indexOf(Math.min(...SIM.ppiHist)); const seas=seasonal(critMonth); const MON2=['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; recs.push({l:'warn',i:'📅', t:`最危险月: ${MON2[critMonth]}(PPI=${SIM.ppiHist[critMonth].toFixed(3)},火险${(seas.fireRisk*100).toFixed(0)}%,降水${seas.rain}mm)`}); const bestMonth=SIM.ppiHist.indexOf(Math.max(...SIM.ppiHist)); recs.push({l:'good',i:'✨',t:`最佳月: ${MON2[bestMonth]}(PPI=${SIM.ppiHist[bestMonth].toFixed(3)},可适当调配资源至脆弱区域)`}); } // Rainfall-based fire prep const drySeason=[5,6,7,8].filter(t=>FIRE_RISK[t]>0.8); if(drySeason.length>0) recs.push({l:'warn',i:'🌵', t:`${drySeason.length}个月高火险(6–9月),建议提前在4月底完成消防设备预置`}); panel.innerHTML=recs.map(r=>`
${r.i}${r.t}
`).join(''); } // ── CSV EXPORT ──────────────────────────────────────────────────── function exportCSV(){ if(!SIM.ppiHist.length){ log('请先运行仿真后再导出','w'); return; } const hdr=['月份','A区PPI','B区PPI','C区PPI','D区PPI','全园PPI','s1偷猎','s2野火','s3人兽','s4栖息']; const rows=[hdr]; SIM.ppiHist.forEach((p,t)=>{ rows.push([MON[t], ...(SIM.zHist[t]||[0,0,0,0]).map(v=>v.toFixed(4)), p.toFixed(4), ...(SIM.thrHist[t]||[0,0,0,0]).map(v=>v.toFixed(4)) ]); }); const mean=a=>a.reduce((s,v)=>s+v,0)/a.length; rows.push([]); rows.push(['年均','','','','',mean(SIM.ppiHist).toFixed(4),'','','','']); rows.push(['情景','k'+SIM.scenario,'','概率',[.50,.20,.20,.10][SIM.scenario],'','','','','']); rows.push(['E[PPI]','=0.5×k1+0.2×k2+0.2×k3+0.1×k4','','','0.550','','','','','']); if(SIM.gaResult) rows.push(['GA增益','+'+SIM.gaResult.gain+'%','','最优权重',SIM.gaResult.w.map(v=>v.toFixed(3)).join(' '),'','','','','']); const csv=rows.map(r=>r.join(',')).join('\n'); const blob=new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`etosha_k${SIM.scenario}_${new Date().toISOString().slice(0,10)}.csv`; a.click(); log('✓ CSV已导出','n'); } // ── SAVE BASELINE ───────────────────────────────────────────────── function saveBaseline(){ if(!SIM.ppiHist.length){ log('请先运行仿真后再保存基准','w'); return; } SIM.baseline={ ppiHist:[...SIM.ppiHist], zHist:SIM.zHist.map(r=>[...r]), thrHist:SIM.thrHist.map(r=>[...r]), scenario:SIM.scenario, staff:SIM.staff, label:`k${SIM.scenario+1} N=${SIM.staff}` }; log(`✓ 基准已保存(${SIM.baseline.label},PPI均值=${(SIM.ppiHist.reduce((a,b)=>a+b,0)/12).toFixed(3)})`,'n'); refreshDisplay(); } // ── APPLY 390-PERSON OPTIMAL PRESET ────────────────────────────── function applyPreset390(){ // Set all sliders to paper-optimal values (N=390, paper Table 5) const vals={ 'staff':390,'r1':210,'r7':72,'r4':13,'r8':52,'r6':45, 'rho1':0.85,'phi2':0.40,'rho3':0.70,'rho4':0.90, 'alpha1':0.30,'alpha2':0.10, 'theta':6.0,'syn':0.10,'sat':0.60,'azw':1.50,'gamma':2,'vmult':0.70, 'ta':0.91,'tb':0.84,'tc':0.82,'td':0.72, 'e11':0.55,'e82':0.75,'e71':0.70,'e41':0.65,'e63':0.60 }; Object.entries(vals).forEach(([id,v])=>{ const el=document.getElementById('sl-'+id); if(el){ el.value=v; onSl(id, v, el.className.match(/\b(a|r)\b/)?.[1]||''); } }); syncParamsFromUI(); log('✓ 已应用论文最优预设(N=390,全部参数还原为论文默认值)','n'); } // ── MINI E[PPI] SCENARIO BAR (draws into eppi-bd on demand) ─────── function drawEPPIMiniBar(scPPIs){ const scProb=[0.50,0.20,0.20,0.10]; const scCols=['#2d7a52','#c0392b','#c47a18','#7040a8']; const scK=['k₁','k₂','k₃','k₄']; const ev=scPPIs.reduce((a,v,i)=>a+v*scProb[i],0); return `
${scPPIs.map((v,k)=>{ const w=Math.round(v*100); const tgt=0.619; return `
${scK[k]}
${v.toFixed(3)} (×${scProb[k]})
`; }).join('')}
E[PPI] = ${ev.toFixed(4)}
`; } // ══════════════════════════════════════════════════════════════ // v15 FEATURES: Keyboard · Chart Tooltips · Species · Cost // ══════════════════════════════════════════════════════════════ // ── KEYBOARD SHORTCUTS ────────────────────────────────────────── document.addEventListener('keydown',e=>{ if(e.target.tagName==='INPUT'||e.target.tagName==='BUTTON') return; switch(e.key){ case ' ': e.preventDefault(); if(!SIM.running) runSim(); break; case '1': setScenario(0); break; case '2': setScenario(1); break; case '3': setScenario(2); break; case '4': setScenario(3); break; case 'ArrowLeft': if(SIM.hSnaps.length){ const t=Math.max(0,(SIM.curMonth<0?11:SIM.curMonth)-1); showMonth(t); } break; case 'ArrowRight': if(SIM.hSnaps.length){ const t=Math.min(SIM.hSnaps.length-1,(SIM.curMonth<0?0:SIM.curMonth)+1); showMonth(t); } break; case 'b': case 'B': saveBaseline(); break; case 'g': case 'G': if(!SIM.running) runGA(); break; case 'e': case 'E': exportCSV(); break; case 'p': case 'P': toggleAutoPlay(); break; case 'r': case 'R': if(!SIM.running) generateReport(); break; case 'c': case 'C': if(!SIM.running) compareScenarios(); break; case 't': case 'T':(()=>{ const TL=['savanna','ivory','slate','forest','dusk']; setTheme(TL[(TL.indexOf(_theme)+1)%TL.length]); })(); break; case 't': case 'T': (()=>{ const TL=['savanna','ivory','slate','forest','dusk']; setTheme(TL[(TL.indexOf(_theme)+1)%TL.length]); })(); break; case '0': resetParams(); break; } }); // ── CHART HOVER TOOLTIPS ─────────────────────────────────────── (function(){ const div=document.createElement('div'); div.id='ctt'; div.style.cssText='position:fixed;background:rgba(26,23,16,.92);color:#f0ece2;'+ 'font:10px DM Mono,monospace;padding:6px 9px;border-radius:5px;pointer-events:none;'+ 'display:none;z-index:300;border:1px solid rgba(255,255,255,.13);line-height:1.6;'+ 'max-width:200px;box-shadow:0 4px 16px rgba(0,0,0,.35)'; document.body.appendChild(div); function show(e,html){ div.innerHTML=html; div.style.display='block'; div.style.left=(e.clientX+14)+'px'; div.style.top=(e.clientY-8)+'px'; } function hide(){ div.style.display='none'; } function attach(id,fn){ const cv=document.getElementById(id); if(!cv) return; cv.addEventListener('mousemove',e=>{ const rc=cv.getBoundingClientRect(); const rx=(e.clientX-rc.left)/rc.width, ry=(e.clientY-rc.top)/rc.height; const h=fn(rx,ry,cv.width,cv.height); h ? show(e,h) : hide(); }); cv.addEventListener('mouseleave',hide); } // PPI History attach('ch-ppi',(rx,ry,W)=>{ if(!SIM.ppiHist.length) return null; const ML=32,MR=W-10; const t=Math.round((rx*W-ML)/(MR-ML)*(SIM.ppiHist.length-1)); if(t<0||t>=SIM.ppiHist.length) return null; const pv=SIM.ppiHist[t], zv=SIM.zHist[t]||[0,0,0,0]; const tgt=P.ztgt.reduce((a,v,z)=>a+v*ZW[z],0); const ok=pv>=tgt; return `${MON[t]} — 全园PPI ${pv.toFixed(4)}
`+ `A区 ${zv[0].toFixed(3)} · B区 ${zv[1].toFixed(3)}
`+ `C区 ${zv[2].toFixed(3)} · D区 ${zv[3].toFixed(3)}
`+ `目标 ${tgt.toFixed(3)} · ${ok?'✓达标':'✗低于目标'}`; }); // Threat chart attach('ch-thr',(rx,ry,W)=>{ if(!SIM.thrHist.length) return null; const ML=32,MR=W-10; const t=Math.round((rx*W-ML)/(MR-ML)*(SIM.thrHist.length-1)); if(t<0||t>=SIM.thrHist.length) return null; const r=SIM.thrHist[t]; const dry=[5,6,7,8,9].includes(t)?'🌵 干季':'🌧 湿季'; return `${MON[t]} ${dry}
`+ `🏹偷猎 ${r[0].toFixed(4)}
🔥野火 ${r[1].toFixed(4)}
`+ `⚡人兽 ${r[2].toFixed(4)}
🌱栖息 ${r[3].toFixed(4)}`; }); // Heatmap attach('ch-heat',(rx,ry,W,H)=>{ if(!SIM.zHist.length) return null; const ML=46,MR=W-10,MT=24,MB=H-14; const T=SIM.zHist.length; const t=Math.floor((rx*W-ML)/(MR-ML)*T); const z=Math.floor((ry*H-MT)/(MB-MT)*4); if(t<0||t>=T||z<0||z>3) return null; const v=SIM.zHist[t][z], tgt=P.ztgt[z], gap=v-tgt; return `${['A','B','C','D'][z]}区 — ${MON[t]}
`+ `PPI ${v.toFixed(4)}
目标 ${tgt.toFixed(3)}
`+ `差距 ${gap>=0?'+':''}${gap.toFixed(4)}`; }); // Saturation curve attach('ch-sat',(rx,ry,W)=>{ const ML=32,MR=W-12; const nMin=80,nMax=600; const N=Math.round(nMin+(rx*W-ML)/(MR-ML)*(nMax-nMin)); if(NnMax) return null; const pM=0.6910,lam=0.00657; const ppi=pM*(1-Math.exp(-lam*N)); const mg=pM*lam*Math.exp(-lam*N); return `编制 N = ${N} 人
PPI = ${ppi.toFixed(4)}
`+ `边际增益 ${(mg*10).toFixed(4)} /10人
`+ `年成本估算 $${(N*18/1000).toFixed(1)}M`; }); // Robustness attach('ch-robust',(rx,ry,W)=>{ const ML=42,MR=W-14; const gam=Math.round((rx*W-ML)/(MR-ML)*3*2)/2; // 0–3 step 0.5 if(gam<0||gam>3) return null; const pMax=0.6910,lam=0.00657; const staffs=[240,295,390,520]; return `不确定性预算 Γ = ${gam}
`+ staffs.map(N=>`N=${N}: PPI=${Math.max(0,pMax*(1-Math.exp(-lam*N))*(1-gam*0.07)).toFixed(4)}`).join('
'); }); // Eta matrix attach('ch-eta',(rx,ry,W,H)=>{ const ML=70,MR=W-10,MT=26,MB=H-14; const si=Math.floor((rx*W-ML)/(MR-ML)*4); const ri=Math.floor((ry*H-MT)/(MB-MT)*8); if(si<0||si>3||ri<0||ri>7) return null; const rN=['地面护林员','巡逻车','固定传感器','无人机','卫星','社区联络员','快速响应队','消防设备']; const sN=['偷猎 s₁','野火 s₂','人兽冲突 s₃','栖息地退化 s₄']; const wt=[0.40,0.30,0.15,0.15]; const contrib=P.eta[ri][si]*P.mu[ri][si]*wt[si]; return `${rN[ri]} vs ${sN[si]}
效能 η=${P.eta[ri][si].toFixed(3)} μ=${P.mu[ri][si].toFixed(2)}
加权贡献 ${contrib.toFixed(4)}`; }); // Alloc chart (stacked bar — 4 zones) attach('ch-alloc',(rx,ry,W,H)=>{ if(!SIM.alloc) return null; const ML=38,MR=W-10,MB=112; const nZ=4,PW=MR-ML; const bGap=16, bW=(PW-bGap*(nZ-1))/nZ; const z=Math.floor((rx*W-ML)/(bW+bGap)); if(z<0||z>=nZ) return null; const rNames=['护林员','车辆','传感器','无人机','','联络员','快响队','消防']; const zR=Array.from({length:4},()=>new Array(NR).fill(0)); CELLS.forEach((_,i)=>{ for(let r=0;rr===4?a:a+b,0); return `${zNames[z]}
总人力 ${tot}人
`+ [0,1,2,3,5,6,7].filter(r=>zR[z][r]>0) .map(r=>`${rNames[r]} ${zR[z][r]}`) .join(' · '); }); // Compare chart attach('ch-compare',(rx,ry,W,H)=>{ if(!Object.keys(SCEN_CACHE).length) return null; const ML=36,MR=W-10,MT=14,MB=118,PW=MR-ML; const t=Math.round((rx*W-ML)/PW*11); if(t<0||t>11) return null; const MON=['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; return `${MON[t]}
`+ Object.values(SCEN_CACHE).map(({label,col,ppiHist})=> `${label}: ${ppiHist[t].toFixed(4)}` ).join('
'); }); // Radar chart attach('ch-radar',(rx,ry,W,H)=>{ const cx=W/2, cy=84, R=Math.min(cx-52,66); const N=8; const angle=i=>-Math.PI/2+i/N*2*Math.PI; // Find which spoke we're near let nearest=-1, nearDist=99; for(let i=0;iR*0.5) return null; const rN=['地面护林员','巡逻车','固定传感器','无人机','卫星','社区联络员','快速响应队','消防设备']; const wt=[0.40,0.30,0.15,0.15]; const eff=P.eta[nearest].reduce((a,e,s)=>a+e*P.mu[nearest][s]*wt[s],0); let allocTot=0; if(SIM.alloc) for(let ci=0;ci${rN[nearest]}
综合效能 η=${eff.toFixed(4)}
`+ (SIM.alloc?`当前分配 ${allocTot}人`:'运行仿真后显示分配量'); }); })(); // ── SPECIES PANEL ────────────────────────────────────────────── function buildSpeciesPanel(){ const el=document.getElementById('species-panel'); if(!el) return; // IUCN 2023 Red List + Etosha census data const SP=[ {n:'黑犀牛', s:'Diceros bicornis', st:'CR', sii:0.925, pop:'全球≈2,500 / 纳米比亚≈1,900', tr:'↑缓慢恢复', c:'#c0392b'}, {n:'非洲象', s:'Loxodonta africana', st:'VU', sii:0.782, pop:'埃托沙≈2,500–3,000头', tr:'→稳定', c:'#7040a8'}, {n:'狮子', s:'Panthera leo', st:'VU', sii:0.735, pop:'埃托沙≈600–700头', tr:'→稳定', c:'#c47a18'}, {n:'猎豹', s:'Acinonyx jubatus', st:'VU', sii:0.692, pop:'纳米比亚≈3,500头', tr:'↓下降', c:'#e07840'}, {n:'长颈鹿', s:'Giraffa giraffa', st:'VU', sii:0.543, pop:'埃托沙≈5,000头', tr:'↑增长', c:'#c8a030'}, {n:'斑马', s:'Equus quagga', st:'LC', sii:0.312, pop:'埃托沙≈15,000头', tr:'→稳定', c:'#5a5040'}, ]; const SC={'CR':'#c0392b','EN':'#e07840','VU':'#c47a18','NT':'#8a8070','LC':'#2d7a52'}; const ICO={'CR':'🦏','VU':'🐘','LC':'🦓'}; const icos=['🦏','🐘','🦁','🐆','🦒','🦓']; el.innerHTML=SP.map((sp,i)=>`
${icos[i]}
${sp.n} ${sp.s}
${sp.pop} · ${sp.tr}
${sp.st}
SII ${sp.sii.toFixed(3)}
`).join(''); } // ── COST-BENEFIT ANALYSIS ────────────────────────────────────── function updateCostBenefit(){ if(!SIM.alloc) return; const el=document.getElementById('cost-panel'); if(!el) return; // Cost data: Etosha NP Annual Reports + NACSO reports + comparable African parks const cPR=18000, cDR=45000, cSN=8000, cFE=12000, cVH=25000; // USD/unit/year const N=SIM.staff||295; const bud=getBudgetN(N,6); // peak dry-season allocation const tot=bud[0]*cPR + bud[1]*cVH + bud[2]*cSN + bud[3]*cDR + bud[7]*cFE; const pM=0.6910,lam=0.00657; const ppi295=pM*(1-Math.exp(-lam*295)), ppi390=pM*(1-Math.exp(-lam*390)); const curPPI=SIM.ppiHist.length? SIM.ppiHist.reduce((a,b)=>a+b,0)/SIM.ppiHist.length : ppi295; const dN=95, dPPI=ppi390-ppi295, costPerPPI=(dN*cPR)/dPPI; const roiLabel=((dPPI/((dN*cPR)/1e6))*100).toFixed(1); el.innerHTML=`
${[ ['$'+( tot/1e6).toFixed(2)+'M', '年度估算成本',N+'人编制'], ['$'+(tot/N/1000).toFixed(0)+'k','人均年成本','含设备运营'], [roiLabel+'%', 'PPI增益/百万USD','295→390段'], ['$'+(costPerPPI/1000).toFixed(0)+'k','每单位PPI成本','295→390段'], ].map(([v,l,s])=>`
${v}
${l}
${s}
`).join('')}
资源成本分项(干季峰值,N=${N})
${[['护林员 r₁','#2d7a52',bud[0],cPR],['巡逻车 r₂','#6aaee0',bud[1],cVH], ['传感器 r₃','#7040a8',bud[2],cSN],['无人机 r₄','#c47a18',bud[3],cDR], ['消防 r₈','#e8a030',bud[7],cFE]].map(([n,col,qty,uc])=>{ const cost=qty*uc, pct=Math.round(cost/tot*100)||1; return `
${n}
${pct}%
$${(cost/1000).toFixed(0)}k
`; }).join('')}
※ 成本基于NACSO/MET年报估算,含薪资、设备、培训。不含基础设施建设。
`; } // ── MAP CONTEXT MENU ────────────────────────────────────────── (function(){ const menu=document.createElement('div'); menu.id='ctx-menu'; menu.style.cssText='position:fixed;background:var(--surf);border:1px solid var(--bd);'+ 'border-radius:6px;box-shadow:0 4px 20px rgba(0,0,0,.2);z-index:400;'+ 'display:none;font-size:9px;font-family:var(--mono);min-width:160px;overflow:hidden'; document.body.appendChild(menu); let ctxCell=-1; document.addEventListener('click',()=>{ menu.style.display='none'; }); const CV=document.getElementById('grid-canvas'); if(CV) CV.addEventListener('contextmenu',e=>{ e.preventDefault(); const rc=CV.getBoundingClientRect(); const cw=CV.width/38, ch=cw*0.75; const gc=Math.floor((e.clientX-rc.left)*(CV.width/rc.width)/cw); const gr=Math.floor((e.clientY-rc.top)*(CV.height/rc.height)/ch); const idx=CIDX[gr*100+gc]; if(idx===undefined||!SIM.hTilde) return; ctxCell=idx; const z=ZONE[idx], pv=SIM.Pcell?.[idx]||0, tgt=P.ztgt[z]; const al=SIM.alloc||new Float32Array(1); const zNames=['A区犀牛核心','B区盐沼过渡','C区边界缓冲','D区内部区']; menu.innerHTML=`
${zNames[z]} 格(${gc},${gr})
=tgt?'var(--grn)':'var(--red)'}"> PPI: ${pv.toFixed(4)} / 目标 ${tgt.toFixed(3)}
H̃偷猎 ${(SIM.hTilde[idx*4]||0).toFixed(3)} 野火 ${(SIM.hTilde[idx*4+1]||0).toFixed(3)}
人兽 ${(SIM.hTilde[idx*4+2]||0).toFixed(3)} 栖息 ${(SIM.hTilde[idx*4+3]||0).toFixed(3)}
🔍 聚焦此格
📋 输出格日志
`; menu.style.left=e.clientX+'px'; menu.style.top=e.clientY+'px'; menu.style.display='block'; // Adjust if off-screen const mr=menu.getBoundingClientRect(); if(mr.right>window.innerWidth) menu.style.left=(e.clientX-mr.width)+'px'; if(mr.bottom>window.innerHeight) menu.style.top=(e.clientY-mr.height)+'px'; }); })(); function setViewAndFocus(gc,gr){ const cv=document.getElementById('grid-canvas'); if(!cv) return; document.getElementById('ctx-menu').style.display='none'; // highlight cell temporarily const cw=cv.width/38, ch=cw*0.75; const origDraw=drawGrid; let flashes=0; const flash=setInterval(()=>{ drawGrid(); if(flashes%2===0){ const ctx=cv.getContext('2d'); ctx.strokeStyle='rgba(255,220,50,.9)'; ctx.lineWidth=2; ctx.strokeRect(gc*cw+1,gr*ch+1,cw-2,ch-2); } if(++flashes>=6) clearInterval(flash); },150); } function logCellDetail(idx){ document.getElementById('ctx-menu').style.display='none'; if(idx<0||!SIM.hTilde) return; const z=ZONE[idx], pv=SIM.Pcell?.[idx]||0; const h=[0,1,2,3].map(s=>SIM.hTilde[idx*4+s].toFixed(4)); log(`格${idx} 区${['A','B','C','D'][z]}: PPI=${pv.toFixed(4)} H̃=[${h.join(',')}]`,'n'); } // ── PRINT STYLESHEET ────────────────────────────────────────── (function(){ const s=document.createElement('style'); s.textContent=` @media print { body{background:#fff!important;-webkit-print-color-adjust:exact;print-color-adjust:exact} header{-webkit-print-color-adjust:exact;print-color-adjust:exact} #lp{display:none!important} #page{display:block!important} #rp{width:100%;margin-top:8px} .ctabs,.btn-aux,.btnrun,.prog,.logbox,#btn-ga,#timeline{display:none!important} #charts-col canvas{display:block!important;page-break-inside:avoid} .info-card,.info-strip{page-break-inside:avoid} @page{margin:10mm;size:A4 landscape} } .cmenu-item{padding:4px 10px;cursor:pointer;color:var(--ink)} .cmenu-item:hover{background:var(--surf2)}`; document.head.appendChild(s); })(); // ── LANDMARKS ON MAP ───────────────────────────────────────── // (patched into drawGrid via monkey-patch to avoid editing core) (function(){ const _orig=drawGrid; window.drawGrid=function(){ _orig(); const cv=document.getElementById('grid-canvas'); if(!cv) return; const ctx=cv.getContext('2d'); const cw=cv.width/38, ch=cw*0.75; if(cw<8) return; // Etosha NP key landmarks (col, row, label) const LM=[ [5, 5.8, 'Okaukuejo', 'rgba(41,112,168,.9)'], [19, 4.8, 'Halali', 'rgba(41,112,168,.9)'], [33, 3.8, 'Namutoni', 'rgba(41,112,168,.9)'], [13, 1.6, 'Etosha盐沼','rgba(100,160,220,.7)'], ]; LM.forEach(([gc,gr,lbl,dotCol])=>{ const px=gc*cw+cw/2, py=gr*ch; ctx.beginPath(); ctx.arc(px,py,Math.max(2.5,cw*.20),0,2*Math.PI); ctx.fillStyle=dotCol; ctx.fill(); ctx.strokeStyle='rgba(255,255,255,.85)'; ctx.lineWidth=1; ctx.stroke(); ctx.shadowColor='rgba(0,0,0,.5)'; ctx.shadowBlur=3; ctx.font=`bold ${Math.max(7,Math.round(cw*.38))}px Sora,sans-serif`; ctx.fillStyle='rgba(255,255,255,.93)'; ctx.textAlign='left'; ctx.fillText(lbl, px+Math.max(3,cw*.28), py+Math.max(4,cw*.20)); ctx.shadowBlur=0; }); }; })(); // ── RUN HISTORY TABLE ───────────────────────────────────────────── function updateRunHistory(){ const el=document.getElementById('run-history'); if(!el) return; if(!SIM.runHistory.length){ el.innerHTML='
暂无历史记录
'; return; } const best=Math.max(...SIM.runHistory.map(r=>r.ppiMean)); const SCOL=['#2d7a52','#c47a18','#c0392b','#7040a8']; el.innerHTML=SIM.runHistory.slice().reverse().map((r,ri)=>{ const isBest=Math.abs(r.ppiMean-best)<0.0001; // Sparkline SVG let spark=''; if(r.ppiHist&&r.ppiHist.length===12){ const mn=Math.min(...r.ppiHist), mx=Math.max(...r.ppiHist), rng=mx-mn||0.01; const pts=r.ppiHist.map((v,t)=>`${t*5},${14-((v-mn)/rng)*13}`).join(' '); const col=SCOL[r.scenario]||'#2d7a52'; spark=` `; } return `
${r.ts} ${r.label}${r.gaApplied?' 🧬':''}${isBest?' ⭐':''}
=0.60?'var(--grn)':r.ppiMean>=0.54?'var(--amb)':'var(--red)'}"> 均${r.ppiMean.toFixed(3)} ↓${r.ppiMin.toFixed(3)} N=${r.staff}
${spark}
`; }).join(''); } function drawGAConvergence(genData){ const cc=getChartColors(); const el=document.getElementById('ga-chart-wrap'); if(!el||!genData||genData.length<2) return; el.style.display='block'; // Keep header div, create/replace canvas const existing=el.querySelector('canvas'); if(existing) existing.remove(); const cv=document.createElement('canvas'); const W=Math.max(el.offsetWidth-12, 120); cv.width=W; cv.height=54; el.appendChild(cv); const ctx=cv.getContext('2d'), H=cv.height; ctx.fillStyle=cc.chartBg; ctx.fillRect(0,0,W,H); const ML=8,MR=W-8,MT=6,MB=H-12; const mn=Math.min(...genData), mx=Math.max(...genData); const pad=(mx-mn)*0.05||0.001; const xp=i=>ML+i/(genData.length-1)*(MR-ML); const yp=v=>MB-((v-(mn-pad))/((mx+pad)-(mn-pad)))*(MB-MT); // Grid ctx.strokeStyle=cc.chartGrid; ctx.lineWidth=.5; [mn,mn+(mx-mn)/2,mx].forEach(v=>{ const y=yp(v); ctx.beginPath(); ctx.moveTo(ML,y); ctx.lineTo(MR,y); ctx.stroke(); }); // Area fill ctx.beginPath(); genData.forEach((v,i)=>{ i===0?ctx.moveTo(xp(i),yp(v)):ctx.lineTo(xp(i),yp(v)); }); ctx.lineTo(MR,MB); ctx.lineTo(ML,MB); ctx.closePath(); ctx.fillStyle='rgba(45,122,82,.12)'; ctx.fill(); // Line ctx.beginPath(); ctx.strokeStyle='#2d7a52'; ctx.lineWidth=1.5; genData.forEach((v,i)=>{ i===0?ctx.moveTo(xp(i),yp(v)):ctx.lineTo(xp(i),yp(v)); }); ctx.stroke(); // Final point const last=genData[genData.length-1]; ctx.beginPath(); ctx.arc(xp(genData.length-1),yp(last),3,0,2*Math.PI); ctx.fillStyle='#2d7a52'; ctx.fill(); // Labels ctx.fillStyle=cc.chartInk; ctx.font='6.5px DM Mono'; ctx.textAlign='left'; ctx.fillText(`起: ${genData[0].toFixed(4)}`,ML,MB+10); ctx.textAlign='center'; ctx.fillText(`${genData.length}代`,W/2,MB+10); ctx.textAlign='right'; ctx.fillStyle='#1f6b3e'; ctx.font='bold 7px DM Mono'; ctx.fillText(`收敛: ${last.toFixed(4)}`,MR,MB+10); // Improvement annotation const imp=((last-genData[0])/genData[0]*100).toFixed(1); ctx.fillStyle='rgba(45,122,82,.7)'; ctx.font='7px DM Mono'; ctx.textAlign='center'; ctx.fillText(`+${imp}% 提升`,W/2,MT+10); } // ── SPECIES–PPI LINKAGE ──────────────────────────────────────────── // Store previous run survivals for trend comparison let _prevSpeciesSurv=null; function updateSpeciesRisk(){ if(!SIM.ppiHist||!SIM.ppiHist.length) return; const el=document.getElementById('species-panel'); if(!el||!SIM.ppiHist.length) return; const ppiMean=SIM.ppiHist.reduce((a,b)=>a+b,0)/SIM.ppiHist.length; // Risk model: survival probability linked to PPI via logistic curve // Higher PPI → more patrol resources → lower poaching/habitat loss → better survival const riskFn=(sii,ppi)=>{ // returns 0–1 annual survival increment const base=0.92+(ppi-0.5)*0.3*sii; return Math.min(0.99,Math.max(0.65,base)); }; const SP=[ {n:'黑犀牛', s:'Diceros bicornis', st:'CR', sii:0.925, pop:'~2,500全球', c:'#c0392b'}, {n:'非洲象', s:'Loxodonta africana', st:'VU', sii:0.782, pop:'埃托沙≈2,500', c:'#7040a8'}, {n:'狮子', s:'Panthera leo', st:'VU', sii:0.735, pop:'埃托沙≈650', c:'#c47a18'}, {n:'猎豹', s:'Acinonyx jubatus', st:'VU', sii:0.692, pop:'纳米比亚≈3,500',c:'#e07840'}, {n:'长颈鹿', s:'Giraffa giraffa', st:'VU', sii:0.543, pop:'埃托沙≈5,000',c:'#c8a030'}, {n:'斑马', s:'Equus quagga', st:'LC', sii:0.312, pop:'埃托沙≈15,000',c:'#5a5040'}, ]; const SC={'CR':'#c0392b','EN':'#e07840','VU':'#c47a18','NT':'#8a8070','LC':'#2d7a52'}; const icos=['🦏','🐘','🦁','🐆','🦒','🦓']; el.innerHTML=`
基于当前PPI均值 ${ppiMean.toFixed(3)} 的物种风险评估
`+ SP.map((sp,i)=>{ const surv=riskFn(sp.sii,ppiMean); const survBase=riskFn(sp.sii,0.619); // reference at paper baseline const delta=surv-survBase; return `
${icos[i]}
${sp.n} (SII=${sp.sii})
${sp.st}
${(surv*100).toFixed(1)}% ${delta>=0?'↑':'↓'}${Math.abs(delta*100).toFixed(1)}pp ${_prevSpeciesSurv ? ``+ (_prevSpeciesSurv[sp2.n]!==undefined ? (surv>_prevSpeciesSurv[sp2.n]?'':'')+ `vs上次` : '') + `` : ''}
`; }).join(''); // Save current survivals for next comparison const survMap={}; SP.forEach(sp2=>{ survMap[sp2.n]=riskFn(sp2.sii,ppiMean); }); _prevSpeciesSurv=survMap; } // ── INIT ───────────────────────────────────────────────────── window.addEventListener('load',()=>{ setTimeout(()=>{ drawSatChart(); drawEtaChart(); drawRobustChart(); buildSpeciesPanel(); updateCostBenefit(); updateRunHistory(); buildMonthlyPlan(); },400); });