1499 lines
76 KiB
JavaScript
1499 lines
76 KiB
JavaScript
'use strict';
|
||
/* S-57 ENC chart layer for GPS Navigator.
|
||
Attaches to GPSMap.getOLMap() and exposes window.ChartLayer. */
|
||
|
||
const ChartLayer = (function () {
|
||
|
||
// ── Canvas symbol cache ──────────────────────────────────────────────────
|
||
const _cache = {};
|
||
function _ci(key, fn) { if (!_cache[key]) _cache[key] = fn(); return _cache[key]; }
|
||
|
||
// ── S-57 colour code → CSS colour ────────────────────────────────────────
|
||
const S57_CSS = {
|
||
1:'#ffffff', 2:'#111111', 3:'#e53935', 4:'#2e7d32',
|
||
5:'#1565c0', 6:'#f9a825', 7:'#78909c', 8:'#6d4c41',
|
||
9:'#ff8f00', 10:'#7b1fa2', 11:'#ef6c00', 12:'#e91e63',
|
||
};
|
||
function _s57css(c) { return S57_CSS[c] || '#78909c'; }
|
||
|
||
// ── Canvas helpers ───────────────────────────────────────────────────────
|
||
function _mkC(sz) {
|
||
const c = document.createElement('canvas'); c.width = sz; c.height = sz; return c;
|
||
}
|
||
function _wl(ctx, cx, y, hw) {
|
||
ctx.strokeStyle = '#1a6bb5'; ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.moveTo(cx - hw, y); ctx.lineTo(cx + hw, y); ctx.stroke();
|
||
}
|
||
function _st(ctx, cx, y1, y2) {
|
||
ctx.strokeStyle = '#222'; ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.moveTo(cx, y1); ctx.lineTo(cx, y2); ctx.stroke();
|
||
}
|
||
function _tmConeUp(ctx, cx, y0, w, h, fill) {
|
||
ctx.beginPath(); ctx.moveTo(cx, y0); ctx.lineTo(cx+w/2, y0+h); ctx.lineTo(cx-w/2, y0+h); ctx.closePath();
|
||
ctx.fillStyle = fill; ctx.fill(); ctx.strokeStyle = '#111'; ctx.lineWidth = 0.6; ctx.stroke();
|
||
}
|
||
function _tmConeDown(ctx, cx, y0, w, h, fill) {
|
||
ctx.beginPath(); ctx.moveTo(cx-w/2, y0); ctx.lineTo(cx+w/2, y0); ctx.lineTo(cx, y0+h); ctx.closePath();
|
||
ctx.fillStyle = fill; ctx.fill(); ctx.strokeStyle = '#111'; ctx.lineWidth = 0.6; ctx.stroke();
|
||
}
|
||
function _tmSphere(ctx, cx, cy, r, fill) {
|
||
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2);
|
||
ctx.fillStyle = fill; ctx.fill(); ctx.strokeStyle = '#111'; ctx.lineWidth = 0.6; ctx.stroke();
|
||
}
|
||
function _tmX(ctx, cx, cy, r, col) {
|
||
ctx.strokeStyle = col; ctx.lineWidth = 2;
|
||
ctx.beginPath(); ctx.moveTo(cx-r, cy-r); ctx.lineTo(cx+r, cy+r); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(cx+r, cy-r); ctx.lineTo(cx-r, cy+r); ctx.stroke();
|
||
}
|
||
|
||
// ── 3D helpers ───────────────────────────────────────────────────────────
|
||
function _h2r(hex) {
|
||
hex = (hex || '').replace('#','');
|
||
if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
|
||
if (hex.length < 6) return [120, 144, 156]; // fallback gris neutro — evita NaN
|
||
const r = parseInt(hex.slice(0,2),16);
|
||
const g = parseInt(hex.slice(2,4),16);
|
||
const b = parseInt(hex.slice(4,6),16);
|
||
return [isNaN(r)?120:r, isNaN(g)?144:g, isNaN(b)?156:b];
|
||
}
|
||
function _lighten(hex, f=0.45) {
|
||
const [r,g,b] = _h2r(hex);
|
||
return `rgb(${Math.min(255,Math.round(r+(255-r)*f))},${Math.min(255,Math.round(g+(255-g)*f))},${Math.min(255,Math.round(b+(255-b)*f))})`;
|
||
}
|
||
function _darken(hex, f=0.45) {
|
||
const [r,g,b] = _h2r(hex);
|
||
return `rgb(${Math.round(r*(1-f))},${Math.round(g*(1-f))},${Math.round(b*(1-f))})`;
|
||
}
|
||
function _cylGrad(ctx, x, w, yMid, col) {
|
||
const g = ctx.createLinearGradient(x, yMid, x+w, yMid);
|
||
g.addColorStop(0, _lighten(col,0.58)); g.addColorStop(0.22, _lighten(col,0.18));
|
||
g.addColorStop(0.55, col); g.addColorStop(0.82, _darken(col,0.32));
|
||
g.addColorStop(1, _darken(col,0.55)); return g;
|
||
}
|
||
function _coneGrad(ctx, x, w, yMid, col) {
|
||
const g = ctx.createLinearGradient(x, yMid, x+w, yMid);
|
||
g.addColorStop(0, _lighten(col,0.65)); g.addColorStop(0.20, _lighten(col,0.20));
|
||
g.addColorStop(0.55, col); g.addColorStop(1, _darken(col,0.60)); return g;
|
||
}
|
||
function _fillBands3D(ctx, pathFn, bx, by, bw, bh, css, gradFn) {
|
||
ctx.save(); pathFn(ctx); ctx.clip();
|
||
if (!css.length) { ctx.fillStyle = '#78909c'; ctx.fillRect(bx,by,bw,bh); }
|
||
else {
|
||
const bandH = bh / css.length;
|
||
css.forEach((col,i) => {
|
||
const y0 = by + i*bandH;
|
||
ctx.fillStyle = gradFn(ctx, bx, bw, y0+bandH/2, col);
|
||
ctx.fillRect(bx, y0, bw, bandH+1);
|
||
});
|
||
}
|
||
ctx.restore();
|
||
}
|
||
|
||
function _ialaLateralColours(catlam, region) {
|
||
const ialaB = (region||'B') === 'B';
|
||
const G=4, R=3;
|
||
const port = ialaB ? G : R, stbd = ialaB ? R : G;
|
||
if (catlam===1) return [port]; if (catlam===2) return [stbd];
|
||
if (catlam===3) return [port,stbd,port]; if (catlam===4) return [stbd,port,stbd];
|
||
return [];
|
||
}
|
||
|
||
// ── S-52 light flare ─────────────────────────────────────────────────────
|
||
function _drawLightFlare(ctx, cx, y, col) {
|
||
const MAG='#ff00aa', len=8;
|
||
ctx.save(); ctx.translate(cx, y);
|
||
ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(len*0.95,-len*0.30); ctx.lineTo(len*0.30,-len*0.95); ctx.closePath();
|
||
ctx.fillStyle = MAG; ctx.fill();
|
||
ctx.strokeStyle='#fff'; ctx.lineWidth=0.5; ctx.stroke();
|
||
ctx.beginPath(); ctx.arc(0,0,1.5,0,Math.PI*2); ctx.fillStyle=MAG; ctx.fill();
|
||
if (col && col!==MAG) {
|
||
ctx.beginPath(); ctx.arc(len*0.62,-len*0.62,1.8,0,Math.PI*2);
|
||
ctx.fillStyle=col; ctx.fill();
|
||
ctx.strokeStyle='#fff'; ctx.lineWidth=0.4; ctx.stroke();
|
||
}
|
||
ctx.restore();
|
||
}
|
||
|
||
// ── 3D buoy canvas ───────────────────────────────────────────────────────
|
||
function _encBuoyCanvas(opts) {
|
||
const sz = opts.sz || 52;
|
||
const c = _mkC(sz); const ctx = c.getContext('2d'); const cx = sz/2;
|
||
let cc = (opts.colours||[]).slice();
|
||
if (!cc.length && opts.catlam) cc = _ialaLateralColours(opts.catlam, opts.region);
|
||
const css = cc.map(_s57css);
|
||
const c1 = css[0] || '#78909c';
|
||
const wlY=sz*0.92, bBot=sz*0.88, bTop=sz*0.52, bW=sz*0.52, bH=bBot-bTop;
|
||
const eRy=bH*0.095;
|
||
const _tmRoom=sz*0.52*0.40;
|
||
const stTop = opts.hasLight ? Math.max(sz*0.20,_tmRoom+sz*0.03) : Math.max(sz*0.24,_tmRoom+sz*0.03);
|
||
const glowCol = c1 ? _lighten(c1,0.55) : 'rgba(255,255,255,0.7)';
|
||
const outln = (lw=0.9) => { ctx.shadowBlur=0; ctx.strokeStyle='rgba(0,0,0,0.75)'; ctx.lineWidth=lw; ctx.stroke(); };
|
||
|
||
const drawCylinder = (x, topY, w, h, colList) => {
|
||
const ery=h*0.095;
|
||
_fillBands3D(ctx, cc=>{ cc.beginPath(); cc.rect(x,topY,w,h); }, x, topY, w, h, colList, _cylGrad);
|
||
ctx.shadowBlur=5; ctx.shadowColor=glowCol;
|
||
ctx.strokeStyle='rgba(255,255,255,0.45)'; ctx.lineWidth=1.0; ctx.strokeRect(x,topY,w,h);
|
||
ctx.shadowBlur=0; ctx.strokeStyle='rgba(0,0,0,0.55)'; ctx.lineWidth=0.6; ctx.strokeRect(x,topY,w,h);
|
||
ctx.beginPath(); ctx.ellipse(x+w/2,topY+h,w/2,ery,0,0,Math.PI*2);
|
||
ctx.fillStyle=_darken(colList[colList.length-1]||c1,0.5); ctx.fill(); outln(0.5);
|
||
const tg=ctx.createRadialGradient(x+w*0.3,topY,1,x+w/2,topY,w/2);
|
||
tg.addColorStop(0,'rgba(255,255,255,0.80)'); tg.addColorStop(0.5,_lighten(colList[0]||c1,0.25)); tg.addColorStop(1,colList[0]||c1);
|
||
ctx.beginPath(); ctx.ellipse(x+w/2,topY,w/2,ery,0,0,Math.PI*2);
|
||
ctx.fillStyle=tg; ctx.fill(); outln(0.5);
|
||
};
|
||
const drawCone = (cx2, apexY, baseY, w, colList) => {
|
||
const ery=(baseY-apexY)*0.10, bx=cx2-w/2;
|
||
_fillBands3D(ctx, cc=>{ cc.beginPath(); cc.moveTo(cx2,apexY); cc.lineTo(cx2+w/2,baseY); cc.lineTo(cx2-w/2,baseY); cc.closePath(); },
|
||
bx,apexY,w,baseY-apexY,colList,_coneGrad);
|
||
ctx.beginPath(); ctx.moveTo(cx2,apexY); ctx.lineTo(cx2+w/2,baseY); ctx.lineTo(cx2-w/2,baseY); ctx.closePath();
|
||
ctx.shadowBlur=5; ctx.shadowColor=glowCol; ctx.strokeStyle='rgba(255,255,255,0.40)'; ctx.lineWidth=1.0; ctx.stroke(); ctx.shadowBlur=0;
|
||
ctx.beginPath(); ctx.moveTo(cx2,apexY); ctx.lineTo(cx2+w/2,baseY); ctx.lineTo(cx2-w/2,baseY); ctx.closePath(); outln(0.9);
|
||
ctx.beginPath(); ctx.ellipse(cx2,baseY,w/2,ery,0,0,Math.PI*2);
|
||
ctx.fillStyle=_darken(colList[colList.length-1]||c1,0.40); ctx.fill(); outln(0.5);
|
||
};
|
||
const drawSphere = (cx2, cy2, r, colList) => {
|
||
ctx.beginPath(); ctx.arc(cx2,cy2,r,0,Math.PI*2); ctx.fillStyle=colList[0]||'#78909c'; ctx.fill();
|
||
ctx.save(); ctx.beginPath(); ctx.arc(cx2,cy2,r,0,Math.PI*2); ctx.clip();
|
||
const rg=ctx.createRadialGradient(cx2-r*0.38,cy2-r*0.38,r*0.04,cx2,cy2,r);
|
||
rg.addColorStop(0,'rgba(255,255,255,0.72)'); rg.addColorStop(0.30,'rgba(255,255,255,0.18)');
|
||
rg.addColorStop(0.65,'rgba(0,0,0,0.00)'); rg.addColorStop(1,'rgba(0,0,0,0.42)');
|
||
ctx.fillStyle=rg; ctx.fillRect(cx2-r,cy2-r,r*2,r*2); ctx.restore();
|
||
ctx.beginPath(); ctx.arc(cx2,cy2,r,0,Math.PI*2); outln(0.8);
|
||
};
|
||
const drawTopmarkCan = (tx,ty,w,col) => { const h=w; drawCylinder(tx-w/2,ty-h,w,h,[col]); };
|
||
const drawTopmarkCone = (tx,ty,w,col) => { const h=w*0.9; drawCone(tx,ty-h,ty,w,[col]); };
|
||
const drawTopmarkConeSmall = (tx,ty,w,col) => { const h=w*0.7; drawCone(tx,ty-h,ty,w,[col]); };
|
||
|
||
const bs=opts.boyshp, cl=opts.catlam;
|
||
const isCan=bs===2||(!bs&&(cl===1||cl===3));
|
||
const isCone=bs===1||(!bs&&(cl===2||cl===4));
|
||
const isSph=bs===3, isSpar=bs===5, isBar=bs===6, isSuper=bs===7;
|
||
|
||
if (isCan) {
|
||
drawCylinder(cx-bW/2,bTop,bW,bH,css.length?css:[c1]);
|
||
_st(ctx,cx,bTop-eRy,stTop); drawTopmarkCan(cx,stTop,bW*0.42,c1);
|
||
} else if (isCone) {
|
||
drawCone(cx,bTop,bBot,bW,css.length?css:[c1]);
|
||
_st(ctx,cx,bTop,stTop); drawTopmarkCone(cx,stTop,bW*0.44,c1);
|
||
} else if (isSph) {
|
||
const r=bH*0.44, bcy=bTop+r; drawSphere(cx,bcy,r,css.length?css:[c1]);
|
||
_st(ctx,cx,bcy-r,stTop); _tmSphere(ctx,cx,stTop-sz*0.09,sz*0.09,c1);
|
||
} else if (isSpar) {
|
||
const sw=bW*0.24, sparTop=bTop-bH*0.40;
|
||
drawCylinder(cx-sw/2,sparTop,sw,bH*1.40,css.length?css:[c1]);
|
||
if (cl===1||cl===3) drawTopmarkCan(cx,sparTop-2,sw*1.4,c1);
|
||
else if (cl===2||cl===4) drawTopmarkCone(cx,sparTop-2,sw*1.4,c1);
|
||
else drawTopmarkConeSmall(cx,sparTop-2,sw*1.4,c1);
|
||
} else {
|
||
// PILLAR (default)
|
||
const pw=bW*0.48;
|
||
drawCylinder(cx-pw/2,bTop+bH*0.4,pw,bH*0.6,css.length?css:[c1]);
|
||
drawCylinder(cx-pw*0.35,bTop,pw*0.7,bH*0.4,[_darken(c1,0.08)]);
|
||
_st(ctx,cx,bTop,stTop);
|
||
if (cl===1||cl===3) drawTopmarkCan(cx,stTop,bW*0.42,c1);
|
||
else if (cl===2||cl===4) drawTopmarkCone(cx,stTop,bW*0.44,c1);
|
||
else drawTopmarkConeSmall(cx,stTop,bW*0.50,c1);
|
||
}
|
||
_wl(ctx,cx,wlY,bW*0.68);
|
||
if (opts.hasLight) _drawLightFlare(ctx,cx,stTop+sz*0.02,css[0]||'#ffffff');
|
||
return c;
|
||
}
|
||
|
||
// ── Cardinal buoy canvas ─────────────────────────────────────────────────
|
||
function _encCardinalCanvas(quadrant, sz=40) {
|
||
const c=_mkC(sz); const ctx=c.getContext('2d'); const cx=sz/2;
|
||
const BLK='#111111', YEL='#f9a825';
|
||
const wlY=sz*0.88, bBot=sz*0.84, bTop=sz*0.50, sTop=sz*0.20;
|
||
const bW=sz*0.44, bH=bBot-bTop;
|
||
const bands = {N:[BLK,YEL],S:[YEL,BLK],E:[BLK,YEL,BLK],W:[YEL,BLK,YEL]}[quadrant]||[BLK,YEL];
|
||
const bandH=bH/bands.length;
|
||
bands.forEach((col,i)=>{ ctx.fillStyle=col; ctx.fillRect(cx-bW/2,bTop+i*bandH,bW,bandH); });
|
||
ctx.strokeStyle='#444'; ctx.lineWidth=0.8; ctx.strokeRect(cx-bW/2,bTop,bW,bH);
|
||
_st(ctx,cx,bTop,sTop);
|
||
const cw=sz*0.36, ch=sz*0.14, t1=sTop-ch*0.2, t2=t1-ch-sz*0.01;
|
||
if (quadrant==='N') { _tmConeUp(ctx,cx,t2,cw,ch,BLK); _tmConeUp(ctx,cx,t1,cw,ch,BLK); }
|
||
else if (quadrant==='S') { _tmConeDown(ctx,cx,t2,cw,ch,BLK); _tmConeDown(ctx,cx,t1,cw,ch,BLK); }
|
||
else if (quadrant==='E') { _tmConeUp(ctx,cx,t2,cw,ch,BLK); _tmConeDown(ctx,cx,t1,cw,ch,BLK); }
|
||
else { _tmConeDown(ctx,cx,t2,cw,ch,BLK); _tmConeUp(ctx,cx,t1,cw,ch,BLK); }
|
||
_wl(ctx,cx,wlY,bW*0.65);
|
||
return c;
|
||
}
|
||
|
||
// ── Isolated danger ──────────────────────────────────────────────────────
|
||
function _encIsdCanvas(sz=40) {
|
||
const c=_mkC(sz); const ctx=c.getContext('2d'); const cx=sz/2;
|
||
const wlY=sz*0.88, bBot=sz*0.84, bTop=sz*0.50, sTop=sz*0.28;
|
||
const bW=sz*0.44, bH=bBot-bTop;
|
||
ctx.fillStyle='#111'; ctx.fillRect(cx-bW/2,bTop,bW,bH);
|
||
ctx.fillStyle='#e53935'; ctx.fillRect(cx-bW/2,bTop+bH*0.35,bW,bH*0.30);
|
||
ctx.strokeStyle='#444'; ctx.lineWidth=0.8; ctx.strokeRect(cx-bW/2,bTop,bW,bH);
|
||
_st(ctx,cx,bTop,sTop);
|
||
const sr=sz*0.07; _tmSphere(ctx,cx-sr*1.2,sTop-sr,sr,'#111'); _tmSphere(ctx,cx+sr*1.2,sTop-sr,sr,'#111');
|
||
_wl(ctx,cx,wlY,bW*0.65); return c;
|
||
}
|
||
|
||
// ── Safe water ───────────────────────────────────────────────────────────
|
||
function _encSawCanvas(sz=40) {
|
||
const c=_mkC(sz); const ctx=c.getContext('2d'); const cx=sz/2;
|
||
const wlY=sz*0.88, bBot=sz*0.84, bTop=sz*0.50, sTop=sz*0.28;
|
||
const bW=sz*0.44, bH=bBot-bTop;
|
||
ctx.save(); ctx.beginPath(); ctx.rect(cx-bW/2,bTop,bW,bH); ctx.clip();
|
||
for (let i=0;i<4;i++) { ctx.fillStyle=i%2===0?'#e53935':'#ffffff'; ctx.fillRect(cx-bW/2+i*bW/4,bTop,bW/4,bH); }
|
||
ctx.restore(); ctx.strokeStyle='#444'; ctx.lineWidth=0.8; ctx.strokeRect(cx-bW/2,bTop,bW,bH);
|
||
_st(ctx,cx,bTop,sTop); _tmSphere(ctx,cx,sTop-sz*0.07,sz*0.07,'#e53935');
|
||
_wl(ctx,cx,wlY,bW*0.65); return c;
|
||
}
|
||
|
||
// ── Special buoy ─────────────────────────────────────────────────────────
|
||
function _encSppCanvas(sz=40) {
|
||
const c=_mkC(sz); const ctx=c.getContext('2d'); const cx=sz/2;
|
||
const wlY=sz*0.88, bBot=sz*0.84, bTop=sz*0.50, sTop=sz*0.28;
|
||
const bW=sz*0.44, bH=bBot-bTop;
|
||
ctx.fillStyle='#f9a825'; ctx.fillRect(cx-bW/2,bTop,bW,bH);
|
||
ctx.strokeStyle='#555'; ctx.lineWidth=0.8; ctx.strokeRect(cx-bW/2,bTop,bW,bH);
|
||
_st(ctx,cx,bTop,sTop); _tmX(ctx,cx,sTop-sz*0.09,sz*0.10,'#f9a825');
|
||
_wl(ctx,cx,wlY,bW*0.65); return c;
|
||
}
|
||
|
||
// ── Beacon ───────────────────────────────────────────────────────────────
|
||
function _encBeaconCanvas(colours, catlam, region, sz=36) {
|
||
const c=_mkC(sz); const ctx=c.getContext('2d'); const cx=sz/2;
|
||
let cc=(colours||[]).slice();
|
||
if (!cc.length&&catlam) cc=_ialaLateralColours(catlam, region);
|
||
const css=cc.map(_s57css); const c1=css[0]||'#78909c';
|
||
const ialaB=(region||'B')==='B';
|
||
const groundY=sz*0.92, poleTopY=sz*0.04, poleW=sz*0.055;
|
||
const bodyW=sz*0.28, bodyTopY=sz*0.48, bodyBotY=groundY;
|
||
const tmW=sz*0.36, tmBotY=sz*0.27, tmTopY=sz*0.05;
|
||
|
||
// Ground shadow
|
||
ctx.beginPath(); ctx.ellipse(cx,groundY,sz*0.12,sz*0.020,0,0,Math.PI*2);
|
||
ctx.fillStyle='rgba(0,0,0,0.22)'; ctx.fill();
|
||
|
||
// Pole
|
||
const poleGrd=ctx.createLinearGradient(cx-poleW,0,cx+poleW,0);
|
||
poleGrd.addColorStop(0,'#999'); poleGrd.addColorStop(0.4,'#ddd'); poleGrd.addColorStop(1,'#777');
|
||
ctx.fillStyle=poleGrd; ctx.fillRect(cx-poleW/2,poleTopY,poleW,groundY-poleTopY);
|
||
|
||
// Body
|
||
const bodyGrd=ctx.createLinearGradient(cx-bodyW/2,0,cx+bodyW/2,0);
|
||
bodyGrd.addColorStop(0,_lighten(c1,0.35)); bodyGrd.addColorStop(0.45,c1); bodyGrd.addColorStop(1,_darken(c1,0.28));
|
||
ctx.fillStyle=bodyGrd; ctx.fillRect(cx-bodyW/2,bodyTopY,bodyW,bodyBotY-bodyTopY);
|
||
ctx.strokeStyle='rgba(0,0,0,0.65)'; ctx.lineWidth=0.9;
|
||
ctx.strokeRect(cx-bodyW/2,bodyTopY,bodyW,bodyBotY-bodyTopY);
|
||
|
||
// Topmark: PORT = square, STBD = triangle
|
||
const isPort=catlam===1||catlam===3||(ialaB&&cc[0]===4)||(!ialaB&&cc[0]===3);
|
||
if (isPort) {
|
||
ctx.fillStyle=c1; ctx.fillRect(cx-tmW/2,tmTopY,tmW,tmBotY-tmTopY);
|
||
ctx.strokeStyle='#111'; ctx.lineWidth=0.8; ctx.strokeRect(cx-tmW/2,tmTopY,tmW,tmBotY-tmTopY);
|
||
} else {
|
||
ctx.beginPath(); ctx.moveTo(cx,tmTopY); ctx.lineTo(cx+tmW/2,tmBotY); ctx.lineTo(cx-tmW/2,tmBotY); ctx.closePath();
|
||
ctx.fillStyle=c1; ctx.fill(); ctx.strokeStyle='#111'; ctx.lineWidth=0.8; ctx.stroke();
|
||
}
|
||
return c;
|
||
}
|
||
|
||
// ── Lighthouse / light symbol (S-52 style) ───────────────────────────────
|
||
function _ialaLight(colourCode) {
|
||
const W=26, H=46;
|
||
const c=document.createElement('canvas'); c.width=W; c.height=H;
|
||
const ctx=c.getContext('2d'); const cx=W/2;
|
||
let col='#ffffff';
|
||
if (colourCode===3) col='#dd1111';
|
||
else if (colourCode===4) col='#00aa00';
|
||
else if (colourCode===6) col='#ffcc00';
|
||
const bW=14, bH=17, bTop=H*0.50, bBot=bTop+bH;
|
||
ctx.beginPath(); ctx.rect(cx-bW/2,bTop,bW,bH);
|
||
ctx.fillStyle=col; ctx.fill(); ctx.strokeStyle='#111'; ctx.lineWidth=1.2; ctx.stroke();
|
||
// 5-point star
|
||
const starX=cx, starY=bTop+bH/2, Ro=4.5, Ri=1.85;
|
||
ctx.beginPath();
|
||
for (let i=0;i<10;i++) {
|
||
const ang=(i*Math.PI/5)-Math.PI/2, rr=(i%2===0)?Ro:Ri;
|
||
const px=starX+Math.cos(ang)*rr, py=starY+Math.sin(ang)*rr;
|
||
i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);
|
||
}
|
||
ctx.closePath(); ctx.fillStyle=(col==='#ffffff')?'#222':'#fff'; ctx.fill();
|
||
// Magenta light flash
|
||
const PURP='#8800cc', tearAng=20*Math.PI/180, tearLen=18;
|
||
ctx.save(); ctx.translate(cx+bW/2-1,bTop+1); ctx.rotate(tearAng);
|
||
ctx.beginPath(); ctx.moveTo(0,0);
|
||
ctx.bezierCurveTo(-3.8,-5,-3.8,-11,0,-tearLen);
|
||
ctx.bezierCurveTo(3.8,-11,3.8,-5,0,0);
|
||
ctx.closePath(); ctx.fillStyle=PURP; ctx.globalAlpha=0.88; ctx.fill(); ctx.globalAlpha=1.0;
|
||
ctx.strokeStyle='rgba(90,0,140,0.5)'; ctx.lineWidth=0.5; ctx.stroke(); ctx.restore();
|
||
// Anchor dot
|
||
ctx.beginPath(); ctx.arc(cx,H-2,2.2,0,Math.PI*2); ctx.fillStyle='#fff'; ctx.fill();
|
||
ctx.beginPath(); ctx.arc(cx,H-2,1.0,0,Math.PI*2); ctx.fillStyle='#111'; ctx.fill();
|
||
return c;
|
||
}
|
||
|
||
// ── Light point (infers buoy shape from colour+region) ───────────────────
|
||
function _encLightCanvas(colours, region, sz=32) {
|
||
const raw=colours[0];
|
||
const ialaB=(region||'B')==='B';
|
||
// White/magenta lights → simple lighthouse symbol
|
||
if (!raw || raw===1 || raw===12) return _ialaLight(raw||1);
|
||
// Coloured light → infer lateral buoy
|
||
const isPort = (ialaB&&raw===4)||(!ialaB&&raw===3);
|
||
const isStbd = (ialaB&&raw===3)||(!ialaB&&raw===4);
|
||
if (isPort||isStbd) {
|
||
const catlam = isPort?1:2;
|
||
return _encBuoyCanvas({ colours, boyshp: isPort?2:1, catlam, region, sz, hasLight:true });
|
||
}
|
||
if (raw===6) return _encSppCanvas(sz); // yellow = special
|
||
return _ialaLight(raw);
|
||
}
|
||
|
||
// ── SVG icon library — idénticos a AR ECDIS (anchor=bottom) ─────────────
|
||
var _svgSrc = (function () {
|
||
// Punto de posición S-52: pequeño, sutil, marca la posición exacta de la ayuda
|
||
var D = function (cx, cy) {
|
||
return '<circle cx="' + cx + '" cy="' + cy + '" r="1.8" fill="rgba(10,10,18,0.72)"/>';
|
||
};
|
||
var icons = {
|
||
// ── Lateral IALA-B ──────────────────────────────────────────────────
|
||
'boylat-port-b': '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="30" viewBox="0 0 20 30">' +
|
||
'<ellipse cx="10" cy="7" rx="6" ry="2.5" fill="#33dd33" stroke="#111" stroke-width="1"/>' +
|
||
'<rect x="4" y="7" width="12" height="14" rx="2" fill="#009900" stroke="#111" stroke-width="1"/>' +
|
||
'<ellipse cx="10" cy="21" rx="6" ry="2" fill="#007700" stroke="#111" stroke-width="1"/>' +
|
||
D(10,27) + '</svg>',
|
||
'boylat-stbd-b': '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="28" viewBox="0 0 20 28">' +
|
||
'<polygon points="10,2 18,21 2,21" fill="#cc0000" stroke="#111" stroke-width="1"/>' +
|
||
D(10,25) + '</svg>',
|
||
// ── Lateral IALA-A ──────────────────────────────────────────────────
|
||
'boylat-port': '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="30" viewBox="0 0 20 30">' +
|
||
'<ellipse cx="10" cy="7" rx="6" ry="2.5" fill="#ee2222" stroke="#111" stroke-width="1"/>' +
|
||
'<rect x="4" y="7" width="12" height="14" rx="2" fill="#cc0000" stroke="#111" stroke-width="1"/>' +
|
||
'<ellipse cx="10" cy="21" rx="6" ry="2" fill="#aa0000" stroke="#111" stroke-width="1"/>' +
|
||
D(10,27) + '</svg>',
|
||
'boylat-stbd': '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="28" viewBox="0 0 20 28">' +
|
||
'<polygon points="10,2 18,21 2,21" fill="#009900" stroke="#111" stroke-width="1"/>' +
|
||
D(10,25) + '</svg>',
|
||
// ── Balizas IALA-B ──────────────────────────────────────────────────
|
||
'bcnlat-port-b': '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="48" viewBox="0 0 22 48">' +
|
||
'<defs><linearGradient id="bpb-pole" x1="0" y1="0" x2="1" y2="0">' +
|
||
'<stop offset="0" stop-color="#bbb"/><stop offset=".45" stop-color="#eee"/><stop offset="1" stop-color="#888"/>' +
|
||
'</linearGradient><linearGradient id="bpb-body" x1="0" y1="0" x2="1" y2="0">' +
|
||
'<stop offset="0" stop-color="#66ee66"/><stop offset=".45" stop-color="#009900"/><stop offset="1" stop-color="#006600"/>' +
|
||
'</linearGradient></defs>' +
|
||
'<ellipse cx="11" cy="42" rx="5" ry="1.2" fill="rgba(0,0,0,0.28)"/>' +
|
||
'<rect x="10" y="2" width="2" height="39" fill="url(#bpb-pole)" rx="1"/>' +
|
||
'<rect x="8.5" y="24" width="5" height="16" fill="url(#bpb-body)" stroke="rgba(0,0,0,.55)" stroke-width=".9"/>' +
|
||
'<rect x="7" y="4" width="8" height="12" fill="url(#bpb-body)" stroke="rgba(0,0,0,.55)" stroke-width=".9"/>' +
|
||
D(11,45) + '</svg>',
|
||
'bcnlat-stbd-b': '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="48" viewBox="0 0 22 48">' +
|
||
'<defs><linearGradient id="bsb-pole" x1="0" y1="0" x2="1" y2="0">' +
|
||
'<stop offset="0" stop-color="#bbb"/><stop offset=".45" stop-color="#eee"/><stop offset="1" stop-color="#888"/>' +
|
||
'</linearGradient><linearGradient id="bsb-body" x1="0" y1="0" x2="1" y2="0">' +
|
||
'<stop offset="0" stop-color="#ff5555"/><stop offset=".45" stop-color="#cc0000"/><stop offset="1" stop-color="#880000"/>' +
|
||
'</linearGradient></defs>' +
|
||
'<ellipse cx="11" cy="42" rx="5" ry="1.2" fill="rgba(0,0,0,0.28)"/>' +
|
||
'<rect x="10" y="2" width="2" height="39" fill="url(#bsb-pole)" rx="1"/>' +
|
||
'<rect x="8.5" y="24" width="5" height="16" fill="url(#bsb-body)" stroke="rgba(0,0,0,.55)" stroke-width=".9"/>' +
|
||
'<polygon points="11,4 15,16 7,16" fill="url(#bsb-body)" stroke="rgba(0,0,0,.55)" stroke-width=".9"/>' +
|
||
D(11,45) + '</svg>',
|
||
// ── Balizas IALA-A ──────────────────────────────────────────────────
|
||
'bcnlat-port': '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="48" viewBox="0 0 22 48">' +
|
||
'<defs><linearGradient id="bpa-pole" x1="0" y1="0" x2="1" y2="0">' +
|
||
'<stop offset="0" stop-color="#bbb"/><stop offset=".45" stop-color="#eee"/><stop offset="1" stop-color="#888"/>' +
|
||
'</linearGradient><linearGradient id="bpa-body" x1="0" y1="0" x2="1" y2="0">' +
|
||
'<stop offset="0" stop-color="#ff5555"/><stop offset=".45" stop-color="#cc0000"/><stop offset="1" stop-color="#880000"/>' +
|
||
'</linearGradient></defs>' +
|
||
'<ellipse cx="11" cy="42" rx="5" ry="1.2" fill="rgba(0,0,0,0.28)"/>' +
|
||
'<rect x="10" y="2" width="2" height="39" fill="url(#bpa-pole)" rx="1"/>' +
|
||
'<rect x="8.5" y="24" width="5" height="16" fill="url(#bpa-body)" stroke="rgba(0,0,0,.55)" stroke-width=".9"/>' +
|
||
'<rect x="7" y="4" width="8" height="12" fill="url(#bpa-body)" stroke="rgba(0,0,0,.55)" stroke-width=".9"/>' +
|
||
D(11,45) + '</svg>',
|
||
'bcnlat-stbd': '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="48" viewBox="0 0 22 48">' +
|
||
'<defs><linearGradient id="bsa-pole" x1="0" y1="0" x2="1" y2="0">' +
|
||
'<stop offset="0" stop-color="#bbb"/><stop offset=".45" stop-color="#eee"/><stop offset="1" stop-color="#888"/>' +
|
||
'</linearGradient><linearGradient id="bsa-body" x1="0" y1="0" x2="1" y2="0">' +
|
||
'<stop offset="0" stop-color="#66ee66"/><stop offset=".45" stop-color="#009900"/><stop offset="1" stop-color="#006600"/>' +
|
||
'</linearGradient></defs>' +
|
||
'<ellipse cx="11" cy="42" rx="5" ry="1.2" fill="rgba(0,0,0,0.28)"/>' +
|
||
'<rect x="10" y="2" width="2" height="39" fill="url(#bsa-pole)" rx="1"/>' +
|
||
'<rect x="8.5" y="24" width="5" height="16" fill="url(#bsa-body)" stroke="rgba(0,0,0,.55)" stroke-width=".9"/>' +
|
||
'<polygon points="11,4 15,16 7,16" fill="url(#bsa-body)" stroke="rgba(0,0,0,.55)" stroke-width=".9"/>' +
|
||
D(11,45) + '</svg>',
|
||
// ── Cardinales ──────────────────────────────────────────────────────
|
||
'boycar-n': '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="40" viewBox="0 0 20 40">' +
|
||
'<polygon points="10,0 14,7 6,7" fill="#111" stroke="#111" stroke-width=".5"/>' +
|
||
'<polygon points="10,8 14,15 6,15" fill="#111" stroke="#111" stroke-width=".5"/>' +
|
||
'<line x1="10" y1="15" x2="10" y2="19" stroke="#555" stroke-width="1.5"/>' +
|
||
'<rect x="6" y="19" width="8" height="6" fill="#111" stroke="#111" stroke-width=".5"/>' +
|
||
'<rect x="6" y="25" width="8" height="6" fill="#ffdd00" stroke="#111" stroke-width=".8"/>' +
|
||
D(10,37) + '</svg>',
|
||
'boycar-s': '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="40" viewBox="0 0 20 40">' +
|
||
'<polygon points="6,0 14,0 10,7" fill="#111" stroke="#111" stroke-width=".5"/>' +
|
||
'<polygon points="6,8 14,8 10,15" fill="#111" stroke="#111" stroke-width=".5"/>' +
|
||
'<line x1="10" y1="15" x2="10" y2="19" stroke="#555" stroke-width="1.5"/>' +
|
||
'<rect x="6" y="19" width="8" height="6" fill="#ffdd00" stroke="#111" stroke-width=".8"/>' +
|
||
'<rect x="6" y="25" width="8" height="6" fill="#111" stroke="#111" stroke-width=".5"/>' +
|
||
D(10,37) + '</svg>',
|
||
'boycar-e': '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="42" viewBox="0 0 20 42">' +
|
||
'<polygon points="10,0 16,8 10,16 4,8" fill="#111" stroke="#111" stroke-width=".5"/>' +
|
||
'<line x1="10" y1="16" x2="10" y2="20" stroke="#555" stroke-width="1.5"/>' +
|
||
'<rect x="6" y="20" width="8" height="5" fill="#111"/>' +
|
||
'<rect x="6" y="25" width="8" height="5" fill="#ffdd00" stroke="#111" stroke-width=".8"/>' +
|
||
'<rect x="6" y="30" width="8" height="5" fill="#111"/>' +
|
||
D(10,39) + '</svg>',
|
||
'boycar-w': '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="42" viewBox="0 0 20 42">' +
|
||
'<polygon points="4,0 16,0 10,8" fill="#111" stroke="#111" stroke-width=".5"/>' +
|
||
'<polygon points="4,16 16,16 10,8" fill="#111" stroke="#111" stroke-width=".5"/>' +
|
||
'<line x1="10" y1="16" x2="10" y2="20" stroke="#555" stroke-width="1.5"/>' +
|
||
'<rect x="6" y="20" width="8" height="5" fill="#ffdd00" stroke="#111" stroke-width=".8"/>' +
|
||
'<rect x="6" y="25" width="8" height="5" fill="#111"/>' +
|
||
'<rect x="6" y="30" width="8" height="5" fill="#ffdd00" stroke="#111" stroke-width=".8"/>' +
|
||
D(10,39) + '</svg>',
|
||
// ── Especiales ──────────────────────────────────────────────────────
|
||
'boyisd': '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="44" viewBox="0 0 22 44">' +
|
||
'<circle cx="11" cy="4" r="4" fill="#111"/><circle cx="11" cy="12" r="4" fill="#111"/>' +
|
||
'<line x1="11" y1="16" x2="11" y2="20" stroke="#555" stroke-width="1.5"/>' +
|
||
'<rect x="5" y="20" width="12" height="5" fill="#111"/>' +
|
||
'<rect x="5" y="25" width="12" height="5" fill="#cc0000"/>' +
|
||
'<rect x="5" y="30" width="12" height="5" fill="#111"/>' +
|
||
D(11,41) + '</svg>',
|
||
'boysaw': '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="38" viewBox="0 0 18 38">' +
|
||
'<circle cx="9" cy="4" r="4" fill="#cc0000" stroke="#111"/>' +
|
||
'<line x1="9" y1="8" x2="9" y2="12" stroke="#555" stroke-width="1.5"/>' +
|
||
'<rect x="3" y="12" width="12" height="5" fill="#cc0000" stroke="#111" stroke-width=".5"/>' +
|
||
'<rect x="3" y="17" width="12" height="5" fill="white" stroke="#111" stroke-width=".5"/>' +
|
||
'<rect x="3" y="22" width="12" height="5" fill="#cc0000" stroke="#111" stroke-width=".5"/>' +
|
||
'<rect x="3" y="27" width="12" height="3" fill="white" stroke="#111" stroke-width=".5"/>' +
|
||
D(9,35) + '</svg>',
|
||
'boyspp': '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="32" viewBox="0 0 20 32">' +
|
||
'<line x1="6" y1="1" x2="14" y2="9" stroke="#cc9900" stroke-width="2.5" stroke-linecap="round"/>' +
|
||
'<line x1="14" y1="1" x2="6" y2="9" stroke="#cc9900" stroke-width="2.5" stroke-linecap="round"/>' +
|
||
'<line x1="10" y1="9" x2="10" y2="13" stroke="#555" stroke-width="1.5"/>' +
|
||
'<rect x="4" y="13" width="12" height="12" rx="2" fill="#ffcc00" stroke="#111" stroke-width="1"/>' +
|
||
D(10,29) + '</svg>',
|
||
// ── Peligros S-52 ────────────────────────────────────────────────────
|
||
// Wreck (WRECKS) — silueta de barco hundido, magenta S-52, contorno blanco para visibilidad
|
||
'wrecks': '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="26" viewBox="0 0 28 26">' +
|
||
'<ellipse cx="14" cy="24.5" rx="8" ry="1.2" fill="rgba(0,0,0,0.30)"/>' +
|
||
'<line x1="14" y1="1" x2="14" y2="9" stroke="white" stroke-width="3.2" stroke-linecap="round"/>' +
|
||
'<line x1="14" y1="1" x2="14" y2="9" stroke="#cc1133" stroke-width="2.2" stroke-linecap="round"/>' +
|
||
'<line x1="7" y1="5" x2="21" y2="5" stroke="white" stroke-width="3.0" stroke-linecap="round"/>' +
|
||
'<line x1="7" y1="5" x2="21" y2="5" stroke="#cc1133" stroke-width="2.0" stroke-linecap="round"/>' +
|
||
'<line x1="2" y1="9" x2="26" y2="9" stroke="white" stroke-width="3.2" stroke-linecap="round"/>' +
|
||
'<line x1="2" y1="9" x2="26" y2="9" stroke="#cc1133" stroke-width="2.2" stroke-linecap="round"/>' +
|
||
'<path d="M4,9 L4,17 Q14,22 24,17 L24,9 Z" stroke="white" stroke-width="2.8" fill="none" stroke-linejoin="round"/>' +
|
||
'<path d="M4,9 L4,17 Q14,22 24,17 L24,9 Z" stroke="#cc1133" stroke-width="1.8" fill="rgba(204,17,51,0.18)" stroke-linejoin="round"/>' +
|
||
'</svg>',
|
||
// Roca sumergida (UWTROC) — asterisco de 8 puntas, naranja vivo S-52
|
||
'uwtroc': '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="-11 -11 22 22">' +
|
||
'<line x1="0" y1="-9" x2="0" y2="9" stroke="white" stroke-width="3.4" stroke-linecap="round"/>' +
|
||
'<line x1="-9" y1="0" x2="9" y2="0" stroke="white" stroke-width="3.4" stroke-linecap="round"/>' +
|
||
'<line x1="-6.4" y1="-6.4" x2="6.4" y2="6.4" stroke="white" stroke-width="3.0" stroke-linecap="round"/>' +
|
||
'<line x1="6.4" y1="-6.4" x2="-6.4" y2="6.4" stroke="white" stroke-width="3.0" stroke-linecap="round"/>' +
|
||
'<line x1="0" y1="-9" x2="0" y2="9" stroke="#ee7700" stroke-width="2.2" stroke-linecap="round"/>' +
|
||
'<line x1="-9" y1="0" x2="9" y2="0" stroke="#ee7700" stroke-width="2.2" stroke-linecap="round"/>' +
|
||
'<line x1="-6.4" y1="-6.4" x2="6.4" y2="6.4" stroke="#ee7700" stroke-width="1.8" stroke-linecap="round"/>' +
|
||
'<line x1="6.4" y1="-6.4" x2="-6.4" y2="6.4" stroke="#ee7700" stroke-width="1.8" stroke-linecap="round"/>' +
|
||
'<circle cx="0" cy="0" r="2.8" fill="white"/>' +
|
||
'<circle cx="0" cy="0" r="1.8" fill="#ee7700"/>' +
|
||
'</svg>',
|
||
// Obstrucción (OBSTRN) — rombo con X, amarillo-naranja S-52
|
||
'obstrn': '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="-11 -11 22 22">' +
|
||
'<polygon points="0,-9 8,0 0,9 -8,0" stroke="white" stroke-width="2.8" fill="none" stroke-linejoin="round"/>' +
|
||
'<polygon points="0,-9 8,0 0,9 -8,0" stroke="#dd8800" stroke-width="1.8" fill="rgba(221,136,0,0.15)" stroke-linejoin="round"/>' +
|
||
'<line x1="-4.5" y1="-4.5" x2="4.5" y2="4.5" stroke="white" stroke-width="2.6" stroke-linecap="round"/>' +
|
||
'<line x1="4.5" y1="-4.5" x2="-4.5" y2="4.5" stroke="white" stroke-width="2.6" stroke-linecap="round"/>' +
|
||
'<line x1="-4.5" y1="-4.5" x2="4.5" y2="4.5" stroke="#dd8800" stroke-width="1.6" stroke-linecap="round"/>' +
|
||
'<line x1="4.5" y1="-4.5" x2="-4.5" y2="4.5" stroke="#dd8800" stroke-width="1.6" stroke-linecap="round"/>' +
|
||
'</svg>',
|
||
// ── Faro ────────────────────────────────────────────────────────────
|
||
'lighthouse': '<svg xmlns="http://www.w3.org/2000/svg" width="44" height="62" viewBox="0 0 44 62">' +
|
||
'<defs><radialGradient id="lhalo" cx="0.5" cy="0.5" r="0.5">' +
|
||
'<stop offset="0" stop-color="rgba(255,240,120,0.70)"/><stop offset="1" stop-color="rgba(255,240,120,0)"/>' +
|
||
'</radialGradient></defs>' +
|
||
'<ellipse cx="22" cy="60" rx="7" ry="1.4" fill="rgba(0,0,0,0.30)"/>' +
|
||
'<line x1="8" y1="59" x2="16" y2="31" stroke="white" stroke-width="2.2" stroke-linecap="round"/>' +
|
||
'<line x1="36" y1="59" x2="28" y2="31" stroke="white" stroke-width="2.2" stroke-linecap="round"/>' +
|
||
'<line x1="10" y1="47" x2="28" y2="31" stroke="white" stroke-width="1.4"/>' +
|
||
'<line x1="34" y1="47" x2="16" y2="31" stroke="white" stroke-width="1.4"/>' +
|
||
'<line x1="6" y1="59" x2="38" y2="59" stroke="white" stroke-width="2.0"/>' +
|
||
'<rect x="20" y="27" width="4" height="5" fill="white"/>' +
|
||
'<ellipse cx="22" cy="20" rx="13" ry="12" fill="url(#lhalo)"/>' +
|
||
'<rect x="12" y="25" width="20" height="2" rx="1" fill="white" stroke="rgba(0,0,0,.40)" stroke-width=".5"/>' +
|
||
'<rect x="16" y="16" width="12" height="10" rx="1" fill="white" stroke="rgba(0,0,0,.40)" stroke-width=".8"/>' +
|
||
'<path d="M16,16 Q22,7 28,16 Z" fill="white" stroke="rgba(0,0,0,.35)" stroke-width=".8"/>' +
|
||
'<line x1="22" y1="7" x2="22" y2="3" stroke="white" stroke-width="1.2"/>' +
|
||
'<circle cx="22" cy="3" r="1.2" fill="#ffee44"/>' +
|
||
'<circle cx="22" cy="21" r="2.5" fill="#ffe680" opacity=".85"/>' +
|
||
'<circle cx="22" cy="60.5" r="2" fill="white"/><circle cx="22" cy="60.5" r="1.1" fill="black"/></svg>',
|
||
};
|
||
var out = {};
|
||
Object.keys(icons).forEach(function (k) {
|
||
out[k] = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(icons[k]);
|
||
});
|
||
return out;
|
||
})();
|
||
|
||
// ── ENC feature style — SIN etiquetas estáticas (solo en hover/click) ─────
|
||
// Las etiquetas se muestran únicamente en tooltip al hacer hover.
|
||
function encStyle(feature) {
|
||
var aidType = feature.get('aid_type') || '';
|
||
var layer = (feature.get('layer') || '').toUpperCase();
|
||
var region = feature.get('cell_region') || 'B';
|
||
var isBcn = layer.startsWith('BCN');
|
||
var ialaB = region !== 'A';
|
||
var colours = feature.get('colours') || [];
|
||
|
||
var iconKey = null;
|
||
|
||
switch (aidType) {
|
||
case 'LATERAL_PORT': case 'LATERAL_PREF_STBD': case 'LATERAL_UNKNOWN':
|
||
iconKey = isBcn ? (ialaB ? 'bcnlat-port-b' : 'bcnlat-port')
|
||
: (ialaB ? 'boylat-port-b' : 'boylat-port');
|
||
break;
|
||
case 'LATERAL_STBD': case 'LATERAL_PREF_PORT':
|
||
iconKey = isBcn ? (ialaB ? 'bcnlat-stbd-b' : 'bcnlat-stbd')
|
||
: (ialaB ? 'boylat-stbd-b' : 'boylat-stbd');
|
||
break;
|
||
case 'CARDINAL_N': iconKey = 'boycar-n'; break;
|
||
case 'CARDINAL_S': iconKey = 'boycar-s'; break;
|
||
case 'CARDINAL_E': iconKey = 'boycar-e'; break;
|
||
case 'CARDINAL_W': iconKey = 'boycar-w'; break;
|
||
case 'CARDINAL_UNKNOWN': iconKey = 'boycar-n'; break;
|
||
case 'ISOLATED_DANGER': iconKey = 'boyisd'; break;
|
||
case 'SAFE_WATER': iconKey = 'boysaw'; break;
|
||
case 'SPECIAL': iconKey = 'boyspp'; break;
|
||
// Genéricos — fallback a símbolo por defecto del tipo
|
||
case 'BEACON_GENERIC':
|
||
iconKey = ialaB ? 'bcnlat-port-b' : 'bcnlat-port';
|
||
break;
|
||
case 'BUOY_GENERIC':
|
||
iconKey = ialaB ? 'boylat-port-b' : 'boylat-port';
|
||
break;
|
||
case 'LIGHT_POINT': {
|
||
// Luz S-52: sector de luz con extremo curvo (arco circular) inclinado ~40°-50° a la derecha.
|
||
// displacement=[0,28] sube 28px — aprox. al topmark de una boya/baliza estándar.
|
||
var raw = colours[0];
|
||
var lCol = raw === 3 ? '#ee2200' : raw === 4 ? '#00cc00'
|
||
: raw === 6 ? '#ffcc00' : raw === 12 ? '#cc00aa' : '#ffffff';
|
||
// Sector: fuente (2,20), ángulos ±~8° centrados en 42° de la vertical.
|
||
// Lado izq: 34°→ (2+15*sin34°, 20-15*cos34°) = (10.4, 7.6)
|
||
// Lado der: 50°→ (2+15*sin50°, 20-15*cos50°) = (13.5, 10.4)
|
||
// Extremo curvo: arco de r=15 centrado en (2,20) — clockwise (sweep=1)
|
||
var lSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="22" viewBox="0 0 20 22">' +
|
||
'<path d="M2,20 L10.4,7.6 A15,15 0 0,1 13.5,10.4 Z" fill="' + lCol + '" fill-opacity="0.45" stroke="' + lCol + '" stroke-width="0.5" stroke-opacity="0.75"/>' +
|
||
'<circle cx="2" cy="20" r="4.5" fill="' + lCol + '" fill-opacity="0.15"/>' +
|
||
'<circle cx="2" cy="20" r="2.5" fill="' + lCol + '" fill-opacity="0.92" stroke="rgba(255,255,255,0.85)" stroke-width="0.9"/>' +
|
||
'<circle cx="2" cy="20" r="1.2" fill="white"/>' +
|
||
'</svg>';
|
||
return new ol.style.Style({
|
||
image: new ol.style.Icon({
|
||
src: 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(lSvg),
|
||
// anchor en el punto-fuente (2,20) dentro del SVG de 20x22
|
||
anchor: [2/20, 20/22], anchorXUnits: 'fraction', anchorYUnits: 'fraction',
|
||
scale: 1,
|
||
// Desplazar 28px hacia arriba — topmark de boya/baliza estándar ~30px alto
|
||
displacement: [0, 28],
|
||
}),
|
||
});
|
||
}
|
||
case 'LANDMARK': case 'RACON': {
|
||
var nm = feature.get('name') || '';
|
||
// Faro/lighthouse: nombre indica estructura de luz importante
|
||
var isLH = nm.indexOf('Faro') === 0 || nm.indexOf('Light') === 0
|
||
|| nm.indexOf('Lighthouse') === 0 || nm.indexOf('Breakwater Light') >= 0;
|
||
if (isLH) {
|
||
iconKey = 'lighthouse';
|
||
} else {
|
||
// Otro hito: diamante marrón ECDIS
|
||
return new ol.style.Style({
|
||
image: new ol.style.RegularShape({
|
||
points: 4, radius: 6, angle: Math.PI / 4,
|
||
fill: new ol.style.Fill({ color: '#8b6914' }),
|
||
stroke: new ol.style.Stroke({ color: '#fffbe8', width: 1.2 }),
|
||
}),
|
||
});
|
||
}
|
||
break;
|
||
}
|
||
case 'UNKNOWN':
|
||
default: {
|
||
// Círculo con color S-57 del primer COLOUR del objeto
|
||
var fc = colours[0] ? _s57css(colours[0]) : '#607d8b';
|
||
return new ol.style.Style({
|
||
image: new ol.style.Circle({ radius: 4,
|
||
fill: new ol.style.Fill({ color: fc }),
|
||
stroke: new ol.style.Stroke({ color: '#fff', width: 1.2 }),
|
||
}),
|
||
});
|
||
}
|
||
}
|
||
|
||
// Devolver icono SVG (o círculo gris como último fallback)
|
||
var src = iconKey ? _svgSrc[iconKey] : null;
|
||
if (!src) {
|
||
return new ol.style.Style({
|
||
image: new ol.style.Circle({ radius: 5,
|
||
fill: new ol.style.Fill({ color: '#78909c' }),
|
||
stroke: new ol.style.Stroke({ color: '#fff', width: 1.5 }),
|
||
}),
|
||
});
|
||
}
|
||
return new ol.style.Style({
|
||
image: new ol.style.Icon({
|
||
src: src,
|
||
anchor: [0.5, 1.0], anchorXUnits: 'fraction', anchorYUnits: 'fraction',
|
||
scale: 1,
|
||
}),
|
||
});
|
||
}
|
||
|
||
// ── Paleta de colores por modo (idéntica a AR ECDIS presentation.js) ────────
|
||
// Ayudas a la navegación (boyas/balizas/luces) NUNCA se tocan — colores IALA.
|
||
// Solo se recoloran: DEPARE, LNDARE/LANDMASK, DEPCNT, SOUNDG, COALNE, FAIRWY.
|
||
var _chartMode = 'day';
|
||
// Paleta S-52 idéntica a AR ECDIS (session_summary_20260502.md + ar_ecdis_s52_rendering.md).
|
||
// NUNCA tocar colores de ayudas IALA (boyas/balizas/luces) — solo DEPARE/LNDARE/DEPCNT/SOUNDG.
|
||
// coalne = color del trazo de costa (borde del polígono LNDARE) — oscuro para contraste.
|
||
var _S57_COLORS = {
|
||
night: {
|
||
depare: ['#232d3d','#1a2330','#141b28','#0f1520','#0a1018'],
|
||
lndare: '#221e18', coalne: '#a09878',
|
||
soundgText: '#888880', soundgHalo: 'rgba(8,8,6,0.95)',
|
||
depcnt: '#3a3d4a', osmOpacity: 0.18, bgColor: '#000408',
|
||
},
|
||
dusk: {
|
||
depare: ['#2a4a6c','#213c5c','#1a304e','#152640','#101c30'],
|
||
lndare: '#302a1e', coalne: '#908a80',
|
||
soundgText: '#c0c0b0', soundgHalo: 'rgba(14,12,10,0.85)',
|
||
depcnt: '#506090', osmOpacity: 0.38, bgColor: '#040c18',
|
||
},
|
||
'day-std': {
|
||
depare: ['#c8e8f8','#90c8f0','#58a8e0','#3080c8','#1860b0'],
|
||
lndare: '#f0e8c0', coalne: '#2a2a2a',
|
||
soundgText: '#0a1e38', soundgHalo: 'rgba(220,236,252,0.70)',
|
||
depcnt: '#5070a0', osmOpacity: 0.82, bgColor: '#8ab8d8',
|
||
},
|
||
day: {
|
||
// DAY_BRIGHT — paleta S-52 probada en AR ECDIS (ar_ecdis_s52_rendering.md)
|
||
depare: ['#c0e8f8','#a0d0f0','#80b8e8','#4078b8','#1a5a90'],
|
||
lndare: '#dfd5b0', coalne: '#1a1a1a',
|
||
soundgText: '#000000', soundgHalo: 'rgba(255,255,255,0.75)',
|
||
depcnt: '#4060a0', osmOpacity: 0.82, bgColor: '#b8d8f0',
|
||
},
|
||
};
|
||
|
||
function _modeColors() {
|
||
return _S57_COLORS[_chartMode] || _S57_COLORS['day'];
|
||
}
|
||
|
||
// ── Depth style — recolorea según modo, mantiene colores S-57 ─────────────
|
||
function depthStyle(feature) {
|
||
var layer = feature.get('layer');
|
||
var C = _modeColors();
|
||
if (layer === 'DEPARE') {
|
||
var dmax = feature.get('depth_max');
|
||
if (dmax == null) dmax = 9999;
|
||
var fill;
|
||
if (dmax < 2) fill = C.depare[0];
|
||
else if (dmax < 5) fill = C.depare[1];
|
||
else if (dmax < 10) fill = C.depare[2];
|
||
else if (dmax < 30) fill = C.depare[3];
|
||
else fill = C.depare[4];
|
||
return new ol.style.Style({ fill: new ol.style.Fill({ color: fill }) });
|
||
}
|
||
if (layer === 'LANDMASK') {
|
||
return new ol.style.Style({ fill: new ol.style.Fill({ color: C.lndare }) });
|
||
}
|
||
if (layer === 'DEPCNT') {
|
||
var depth = feature.get('depth');
|
||
var isMajor = depth != null && depth % 10 === 0;
|
||
return new ol.style.Style({
|
||
stroke: new ol.style.Stroke({ color: C.depcnt, width: isMajor ? 1.0 : 0.5, lineDash: isMajor ? null : [4,3] }),
|
||
});
|
||
}
|
||
return null; // SOUNDG → soundgStyle() en soundgLayer dedicado
|
||
}
|
||
|
||
// ── Land layer style — diferencia por capa S-57 y modo de presentación ──────
|
||
//
|
||
// PROBLEMA RAÍZ: los polígonos LNDARE del S-57 en puertos (ej. Miami Port) incluyen
|
||
// muelles y diques que flanquean canales de navegación. Rellenarlos sólidamente TAPA
|
||
// el canal de cruceros aunque éste esté correctamente marcado como DEPARE.
|
||
//
|
||
// SOLUCIÓN: en modos día (OSM al 82-90%) NO rellenamos LNDARE — el OSM ya muestra
|
||
// la tierra. Solo ponemos el trazo de costa (COALNE). El canal queda visible porque
|
||
// DEPARE (zIndex 2, azul) no está tapado por LNDARE.
|
||
//
|
||
// En noche/dusk (OSM al 12-38%) SÍ usamos relleno S-52 — sin él la tierra sería
|
||
// indistinguible del océano oscuro.
|
||
function _landStyle(feature) {
|
||
var l = feature.get('layer');
|
||
var C = _modeColors();
|
||
var isDayMode = (_chartMode === 'day' || _chartMode === 'day-std');
|
||
var adv = (_detailLevel === 'advanced');
|
||
|
||
// COALNE — trazo de línea de costa
|
||
// CRÍTICO: en S-57 COALNE a veces viene como Polygon (área de celda con la costa como borde).
|
||
// Hacer stroke a un Polygon grande crea líneas diagonales donde el anillo exterior "cierra"
|
||
// desde el último vértice al primero cruzando el agua.
|
||
// Solo hacemos stroke si la geometría es LineString (COALNE real), no Polygon.
|
||
if (l === 'COALNE') {
|
||
if (adv && !_advLayers.coalne) return null;
|
||
var gtype = feature.getGeometry().getType();
|
||
if (gtype === 'Polygon' || gtype === 'MultiPolygon') return null;
|
||
return new ol.style.Style({
|
||
stroke: new ol.style.Stroke({ color: C.coalne, width: isDayMode ? 1.2 : 1.0 }),
|
||
});
|
||
}
|
||
|
||
// BUISGL — edificios aislados (modo oscuro)
|
||
if (l === 'BUISGL') {
|
||
if (isDayMode) return null;
|
||
return new ol.style.Style({
|
||
stroke: new ol.style.Stroke({ color: 'rgba(100,80,50,0.55)', width: 0.6 }),
|
||
});
|
||
}
|
||
|
||
// BUAARE — zona urbana: borde tenue sin fill
|
||
if (l === 'BUAARE') {
|
||
if (adv && !_advLayers.buaare) return null;
|
||
if (isDayMode) return null; // en día OSM tiene mejor detalle
|
||
return new ol.style.Style({
|
||
stroke: new ol.style.Stroke({ color: C.coalne, width: 0.7, lineDash: [4, 4] }),
|
||
});
|
||
}
|
||
|
||
// LNDARE — polígono de tierra
|
||
// NUNCA hacer stroke: el anillo exterior del polígono LNDARE puede tener un borde de "cierre"
|
||
// que va del último al primer vértice cruzando el agua — diagonal inevitable en S-57.
|
||
// El borde visual costa/agua surge naturalmente del FILL que limita con el color de agua.
|
||
if (l === 'LNDARE') {
|
||
if (isDayMode) return null; // día: LANDMASK + OSM ya muestran la tierra
|
||
// Modos oscuros: relleno sólido S-52 SIN stroke para evitar diagonales
|
||
if (adv && !_advLayers.lndare) return null;
|
||
return new ol.style.Style({
|
||
fill: new ol.style.Fill({ color: C.lndare }),
|
||
});
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// ── Routing helpers — misma depthSource, capas OL distintas ──────────────
|
||
function depAreaStyle(feature) {
|
||
var l = feature.get('layer');
|
||
// LANDMASK: pinta tierra con C.lndare para tapar DEPARE que pueda sobresalir en zonas de tierra.
|
||
// Se renderiza ANTES que DEPARE en el source (ver _parse_depths en chart_manager.py),
|
||
// por lo que DEPARE (canal) se pinta encima → el canal queda visible.
|
||
if (l === 'LANDMASK') {
|
||
if (_detailLevel === 'advanced' && !_advLayers.landmask) return null;
|
||
return depthStyle(feature);
|
||
}
|
||
if (l !== 'DEPARE') return null;
|
||
// En avanzado, respetar flag _advLayers.depare
|
||
if (_detailLevel === 'advanced' && !_advLayers.depare) return null;
|
||
return depthStyle(feature);
|
||
}
|
||
function depLineStyle(feature) {
|
||
var l = feature.get('layer');
|
||
if (l !== 'DEPCNT') return null; // SOUNDG ya va por soundgLayer dedicado
|
||
// BÁSICO: nunca mostrar veriles — solo áreas DEPARE (manejadas por depAreaStyle)
|
||
if (_detailLevel === 'basic') return null;
|
||
// AVANZADO: respetar flag individual
|
||
if (_detailLevel === 'advanced' && !_advLayers.depcnt) return null;
|
||
return depthStyle(feature);
|
||
}
|
||
|
||
// ── Hazard style — SVG S-52 (wrecks/uwtroc/obstrn idénticos a AR ECDIS) ──
|
||
function hazardStyle(feature) {
|
||
var cat = feature.get('category');
|
||
var iconKey = cat === 'wreck' ? 'wrecks' : cat === 'rock' ? 'uwtroc' : 'obstrn';
|
||
var src = _svgSrc[iconKey];
|
||
if (!src) {
|
||
var col = cat === 'wreck' ? '#e53935' : cat === 'rock' ? '#f59e0b' : '#f97316';
|
||
return new ol.style.Style({
|
||
image: new ol.style.Circle({ radius: 6,
|
||
fill: new ol.style.Fill({ color: col }),
|
||
stroke: new ol.style.Stroke({ color: '#fff', width: 1.5 }),
|
||
}),
|
||
});
|
||
}
|
||
return new ol.style.Style({
|
||
image: new ol.style.Icon({
|
||
src: src, scale: 1,
|
||
anchor: [0.5, 0.5], anchorXUnits: 'fraction', anchorYUnits: 'fraction',
|
||
}),
|
||
});
|
||
}
|
||
|
||
// ── Zone style ────────────────────────────────────────────────────────────
|
||
function zoneStyle(feature) {
|
||
const cat = feature.get('category');
|
||
// Opacidades elevadas para visibilidad en modo día sobre OSM
|
||
const fills = {
|
||
restricted: 'rgba(220,40,40,0.22)',
|
||
caution: 'rgba(255,150,0,0.22)',
|
||
anchorage: 'rgba(0,100,210,0.22)',
|
||
fairway: 'rgba(0,170,90,0.18)',
|
||
traffic_lane: 'rgba(80,80,240,0.18)',
|
||
precautionary:'rgba(200,120,0,0.18)',
|
||
dumpground: 'rgba(120,80,40,0.18)',
|
||
pipeline_area:'rgba(160,100,180,0.18)',
|
||
};
|
||
const strokes = {
|
||
restricted: 'rgba(200,30,30,0.85)',
|
||
caution: 'rgba(220,130,0,0.85)',
|
||
anchorage: 'rgba(0,80,190,0.85)',
|
||
fairway: 'rgba(0,150,70,0.80)',
|
||
traffic_lane: 'rgba(60,60,210,0.80)',
|
||
precautionary:'rgba(180,100,0,0.80)',
|
||
dumpground: 'rgba(100,60,20,0.80)',
|
||
pipeline_area:'rgba(140,80,160,0.80)',
|
||
};
|
||
const fill = fills[cat] || 'rgba(120,120,120,0.14)';
|
||
const stroke = strokes[cat] || 'rgba(100,100,100,0.65)';
|
||
return new ol.style.Style({
|
||
fill: new ol.style.Fill({ color: fill }),
|
||
stroke: new ol.style.Stroke({ color: stroke, width: 1.2, lineDash: [7,4] }),
|
||
});
|
||
}
|
||
|
||
// ── Chart layers — stack S-52 (abajo=mayor área, arriba=menor) ───────────
|
||
// zIndex 0 bgLayer ocean bg (map.js)
|
||
// zIndex 1 osmLayer OSM opcional (map.js)
|
||
// zIndex 2 depAreaLayer DEPARE áreas de profundidad (polígonos grandes, bajo tierra)
|
||
// zIndex 4 landLayer LNDARE tierra firme
|
||
// zIndex 5 depLineLayer DEPCNT veriles (líneas) — SOBRE tierra
|
||
// zIndex 6 zoneLayer zonas náuticas (semitransparente)
|
||
// zIndex 7 hazardLayer naufragios, rocas (puntos)
|
||
// zIndex 8 encLayer boyas, balizas, luces (puntos — tope ENC)
|
||
// zIndex 9 soundgLayer SOUNDG sondas — capa dedicada, sobre todo lo demás S-57
|
||
// zIndex 20+ track, rutas, barco propio (map.js)
|
||
|
||
// Fuentes
|
||
const depthSource = new ol.source.Vector(); // DEPARE + DEPCNT + LANDMASK
|
||
const soundgSource = new ol.source.Vector(); // SOUNDG — fuente dedicada (igual que ECDIS)
|
||
const landSource = new ol.source.Vector(); // LNDARE
|
||
const zoneSource = new ol.source.Vector(); // zonas
|
||
const encSource = new ol.source.Vector(); // balizamiento ENC
|
||
const hazardSource = new ol.source.Vector(); // peligros
|
||
|
||
// Estilo dedicado para sondas — idéntico a ECDIS: text-allow-overlap + text-ignore-placement
|
||
// Ancla invisible — OL 9 canvas renderer no renderiza texto en Points que solo
|
||
// tienen `text` style sin ningún `image`/`fill`/`stroke`. El Circle radio 1
|
||
// transparente es suficiente para que OL registre el feature como "dibujable"
|
||
// y luego renderice el texto. El usuario no ve el círculo (1px transparente).
|
||
var _soundgAnchor = new ol.style.Circle({
|
||
radius: 1,
|
||
fill: new ol.style.Fill({ color: 'rgba(0,0,0,0)' }),
|
||
});
|
||
|
||
function soundgStyle(feature) {
|
||
if (_detailLevel !== 'advanced' || !_advLayers.soundg) return null;
|
||
var d = feature.get('depth');
|
||
if (d == null) return null;
|
||
var C = _modeColors();
|
||
var txt = d < 10 ? d.toFixed(1) : Math.round(d).toString();
|
||
return new ol.style.Style({
|
||
image: _soundgAnchor, // ancla invisible — necesaria para que OL renderice el texto
|
||
text: new ol.style.Text({
|
||
text: txt,
|
||
font: 'bold 11px Arial, sans-serif',
|
||
textAlign: 'center',
|
||
textBaseline: 'middle',
|
||
fill: new ol.style.Fill({ color: C.soundgText }),
|
||
overflow: true, // no recortar etiquetas en el borde del tile
|
||
}),
|
||
});
|
||
}
|
||
|
||
// Capas
|
||
const depAreaLayer = new ol.layer.Vector({ // polígonos de profundidad (debajo de tierra)
|
||
source: depthSource, zIndex: 2, style: depAreaStyle,
|
||
});
|
||
const landLayer = new ol.layer.Vector({ // tierra — zIndex 4 enmascara veriles bajo tierra
|
||
source: landSource, zIndex: 4,
|
||
style: _landStyle,
|
||
});
|
||
const depLineLayer = new ol.layer.Vector({ // veriles DEPCNT — solo líneas, sin sondas
|
||
source: depthSource, zIndex: 5, style: depLineStyle,
|
||
});
|
||
const soundgLayer = new ol.layer.Vector({ // sondas — capa dedicada como en ECDIS
|
||
source: soundgSource, zIndex: 9,
|
||
style: soundgStyle,
|
||
declutter: false, // mostrar todas las sondas sin filtrar solapadas
|
||
updateWhileAnimating: true,
|
||
updateWhileInteracting: true,
|
||
});
|
||
const zoneLayer = new ol.layer.Vector({ // zonas náuticas — encima de veriles
|
||
source: zoneSource, zIndex: 6, style: zoneStyle,
|
||
});
|
||
const hazardLayer = new ol.layer.Vector({ // naufragios / rocas
|
||
source: hazardSource, zIndex: 7, style: hazardStyle, maxResolution: 600, declutter: true,
|
||
});
|
||
const encLayer = new ol.layer.Vector({ // boyas / balizas / luces
|
||
source: encSource, zIndex: 8, style: encStyle,
|
||
});
|
||
|
||
// Attach layers — idempotent: skip if already added
|
||
var _layersAttached = false;
|
||
var _moveendTimer = null;
|
||
function _attachLayers() {
|
||
if (_layersAttached) return;
|
||
var olMap = GPSMap.getOLMap();
|
||
if (!olMap) return;
|
||
// Orden de addLayer no importa — OL usa zIndex para pintar.
|
||
[depAreaLayer, landLayer, zoneLayer, depLineLayer, hazardLayer, encLayer, soundgLayer].forEach(function(l) {
|
||
olMap.addLayer(l);
|
||
});
|
||
// Cargar celdas nuevas al mover/hacer zoom (debounced 800ms)
|
||
olMap.on('moveend', function() {
|
||
clearTimeout(_moveendTimer);
|
||
_moveendTimer = setTimeout(function() {
|
||
if (window.py) loadAll();
|
||
}, 800);
|
||
});
|
||
|
||
// ── Hover: tooltip sobre ayudas ENC ─────────────────────────────────────
|
||
// Solo capas de puntos (encLayer, hazardLayer).
|
||
// Las zonas (polígonos grandes) NO participan en hover — bloquearían los puntos.
|
||
var _ptLayers = [encLayer, hazardLayer];
|
||
olMap.on('pointermove', function(evt) {
|
||
if (evt.dragging) { _hideTooltip(); return; }
|
||
var feat = olMap.forEachFeatureAtPixel(evt.pixel, function(f, l) {
|
||
if (_ptLayers.indexOf(l) >= 0) return f;
|
||
}, { hitTolerance: 14 });
|
||
|
||
if (feat) {
|
||
// evt.pixel es relativo al viewport del mapa — correcto para posicionar tooltip
|
||
var px = evt.pixel[0];
|
||
var py = evt.pixel[1];
|
||
_showTooltip(feat, px, py);
|
||
if (!window.GPSMap || GPSMap.getDrawMode() === 'none') {
|
||
olMap.getTargetElement().style.cursor = 'pointer';
|
||
}
|
||
} else {
|
||
_hideTooltip();
|
||
if (!window.GPSMap || GPSMap.getDrawMode() === 'none') {
|
||
olMap.getTargetElement().style.cursor = '';
|
||
}
|
||
}
|
||
});
|
||
|
||
// ── Click: mostrar info en panel derecho ─────────────────────────────────
|
||
olMap.on('click', function(evt) {
|
||
if (window.GPSMap && GPSMap.getDrawMode() !== 'none') return;
|
||
var feat = olMap.forEachFeatureAtPixel(evt.pixel, function(f, l) {
|
||
if (_ptLayers.indexOf(l) >= 0) return f;
|
||
}, { hitTolerance: 16 });
|
||
if (feat) _showInfoPanel(feat);
|
||
});
|
||
|
||
_layersAttached = true;
|
||
}
|
||
|
||
// ── GeoJSON loaders ───────────────────────────────────────────────────────
|
||
// _loadGeoJSON: limpia la fuente y carga desde cero (usado en reloadAll)
|
||
function _loadGeoJSON(source, geojson) {
|
||
source.clear();
|
||
return _addGeoJSON(source, geojson);
|
||
}
|
||
|
||
// _addGeoJSON: añade features SIN limpiar (carga progresiva de celdas)
|
||
function _addGeoJSON(source, geojson) {
|
||
if (!geojson || !geojson.features || !geojson.features.length) return 0;
|
||
try {
|
||
var fmt = new ol.format.GeoJSON();
|
||
var features = fmt.readFeatures(geojson, {
|
||
dataProjection: 'EPSG:4326',
|
||
featureProjection: 'EPSG:3857',
|
||
});
|
||
source.addFeatures(features);
|
||
return features.length;
|
||
} catch(e) {
|
||
console.warn('[ChartLayer] _addGeoJSON error:', e);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// _addDepthsGeoJSON: split depths — SOUNDG → soundgSource, el resto → depthSource
|
||
// Necesario porque OL no renderiza texto-solo Points mezclados con Line/Polygon en misma fuente.
|
||
function _addDepthsGeoJSON(fc) {
|
||
if (!fc || !fc.features) return [0, 0];
|
||
var soundg = [], others = [];
|
||
fc.features.forEach(function(f) {
|
||
if (f.properties && f.properties.layer === 'SOUNDG') soundg.push(f);
|
||
else others.push(f);
|
||
});
|
||
console.log('[ChartLayer] depths split → depthSource:', others.length,
|
||
'| soundgSource:', soundg.length,
|
||
'| soundg flag:', _advLayers.soundg);
|
||
var nD = _addGeoJSON(depthSource, { type: 'FeatureCollection', features: others });
|
||
var nS = _addGeoJSON(soundgSource, { type: 'FeatureCollection', features: soundg });
|
||
return [nD || 0, nS || 0];
|
||
}
|
||
|
||
// ── Icon cache — usar toDataURL para máxima compat. con WebEngine ─────────
|
||
var _iconCache = {};
|
||
function _cachedIcon(key, drawFn) {
|
||
if (_iconCache[key]) return _iconCache[key];
|
||
try {
|
||
var cv = drawFn();
|
||
_iconCache[key] = cv ? cv.toDataURL('image/png') : null;
|
||
} catch(e) {
|
||
_iconCache[key] = null;
|
||
}
|
||
return _iconCache[key];
|
||
}
|
||
|
||
// Wrapper que usa toDataURL en lugar de img: canvas
|
||
var _origCi = _ci;
|
||
function _ciUrl(key, fn) {
|
||
if (_iconCache[key] !== undefined) return _iconCache[key];
|
||
try {
|
||
var cv = fn();
|
||
_iconCache[key] = cv ? cv.toDataURL('image/png') : null;
|
||
} catch(e) {
|
||
_iconCache[key] = null;
|
||
}
|
||
return _iconCache[key];
|
||
}
|
||
|
||
// ── Lectura per-celda via bridge Python ──────────────────────────────────
|
||
// Igual que ECDIS: un slot por celda/tipo → cada respuesta cabe en QWebChannel.
|
||
// Se evita fetch() HTTP que puede fallar según el esquema de carga (file://).
|
||
async function _fetchCellFile(cellId, dataType, addRegion, region) {
|
||
try {
|
||
var raw = await _py('get_cell_data', cellId, dataType);
|
||
var fc = JSON.parse(raw);
|
||
var feats = fc.features || [];
|
||
if (addRegion) {
|
||
feats.forEach(function(f) {
|
||
if (!f.properties) f.properties = {};
|
||
f.properties.cell_region = region;
|
||
});
|
||
}
|
||
return feats;
|
||
} catch(e) {
|
||
console.warn('[ChartLayer] get_cell_data failed:', cellId, dataType, e.message || e);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// Agrega features de todas las celdas para un tipo de dato dado
|
||
async function _loadAllCells(cells, dataType, addRegion) {
|
||
// Carga secuencial para no saturar el QWebChannel con muchos slots en vuelo
|
||
var all = [];
|
||
for (var i = 0; i < cells.length; i++) {
|
||
var c = cells[i];
|
||
var feats = await _fetchCellFile(c.id, dataType, addRegion, c.region || 'B');
|
||
all = all.concat(feats);
|
||
}
|
||
return { type: 'FeatureCollection', features: all };
|
||
}
|
||
|
||
// ── Filtro de celdas por viewport ─────────────────────────────────────────
|
||
// Devuelve las celdas cuyo bbox se solapa con el extent del mapa actual.
|
||
// extent = [minLon, minLat, maxLon, maxLat]
|
||
function _cellsInView(cells, extent) {
|
||
var minLon = extent[0], minLat = extent[1], maxLon = extent[2], maxLat = extent[3];
|
||
return cells.filter(function(c) {
|
||
if (!c.bbox || c.bbox.length < 4) return false;
|
||
var w = c.bbox[0], s = c.bbox[1], e = c.bbox[2], n = c.bbox[3];
|
||
// Filtro AABB: ¿hay solapamiento?
|
||
return w <= maxLon && e >= minLon && s <= maxLat && n >= minLat;
|
||
});
|
||
}
|
||
|
||
// Convierte el extent del mapa OL (EPSG:3857) a [minLon,minLat,maxLon,maxLat]
|
||
function _mapExtentLonLat() {
|
||
try {
|
||
var olMap = GPSMap.getOLMap();
|
||
if (!olMap) return null;
|
||
var sz = olMap.getSize();
|
||
if (!sz || !sz[0] || !sz[1]) return null; // mapa sin renderizar aún
|
||
var ext3857 = olMap.getView().calculateExtent(sz);
|
||
if (!ext3857 || isNaN(ext3857[0])) return null;
|
||
var sw = ol.proj.toLonLat([ext3857[0], ext3857[1]]);
|
||
var ne = ol.proj.toLonLat([ext3857[2], ext3857[3]]);
|
||
if (isNaN(sw[0]) || isNaN(ne[0])) return null;
|
||
return [sw[0], sw[1], ne[0], ne[1]];
|
||
} catch(e) {
|
||
console.warn('[ChartLayer] _mapExtentLonLat error:', e.message || e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ── Nivel de detalle ─────────────────────────────────────────────────────
|
||
// 'basic' → aids + land + DEPARE fills (sin líneas de veril)
|
||
// 'medium' → + veriles (DEPCNT) + hazards (4 tipos)
|
||
// 'advanced' → capas seleccionables via modal
|
||
var _detailLevel = 'basic';
|
||
|
||
// Flags por capa para nivel 'advanced' (controlados por modal de capas)
|
||
var _advLayers = {
|
||
// ── Profundidades ──────────────────────────────────────────────────────
|
||
depare: true, // áreas de profundidad (fills azules)
|
||
depcnt: true, // veriles / isobatas (líneas)
|
||
soundg: false, // sondas (texto de profundidad)
|
||
hazards: true, // naufragios, rocas, obstrucciones
|
||
zones: false, // zonas restringidas, fondeo, tráfico
|
||
// ── Tierra / costa ────────────────────────────────────────────────────
|
||
coalne: true, // COALNE — trazo de línea de costa
|
||
lndare: false, // LNDARE — bordes del polígono tierra (genera líneas diagonales)
|
||
landmask: true, // LANDMASK — relleno beige de tierra S-57
|
||
buaare: false, // BUAARE — límites de zonas urbanas
|
||
// ── Mapa base ─────────────────────────────────────────────────────────
|
||
osm: true, // OSM tiles (mapa base)
|
||
};
|
||
|
||
function _dataTypes() {
|
||
if (_detailLevel === 'advanced') {
|
||
// Construir lista según flags individuales
|
||
var types = ['features', 'land'];
|
||
if (_advLayers.depare || _advLayers.depcnt || _advLayers.soundg) types.push('depths');
|
||
if (_advLayers.hazards) types.push('hazards');
|
||
if (_advLayers.zones) types.push('zones');
|
||
return types;
|
||
}
|
||
if (_detailLevel === 'medium')
|
||
return ['features', 'land', 'depths', 'hazards'];
|
||
// basic: aids + tierra + DEPARE fills (sin líneas — depLineStyle las filtra)
|
||
return ['features', 'land', 'depths'];
|
||
}
|
||
|
||
function setLayerVisibility(opts) {
|
||
Object.keys(opts).forEach(function(k) {
|
||
if (k in _advLayers) _advLayers[k] = !!opts[k];
|
||
});
|
||
// Refrescar estilos inmediatamente
|
||
depthSource.changed();
|
||
soundgSource.changed();
|
||
landSource.changed();
|
||
hazardLayer.setVisible(_advLayers.hazards);
|
||
zoneLayer.setVisible(_advLayers.zones);
|
||
// OSM — toggle visibilidad del tile layer
|
||
var olMap = GPSMap.getOLMap();
|
||
if (olMap) {
|
||
olMap.getLayers().forEach(function(lyr) {
|
||
if (lyr && lyr.getSource && lyr.getSource() instanceof ol.source.OSM) {
|
||
lyr.setVisible(_advLayers.osm);
|
||
}
|
||
});
|
||
}
|
||
// Recargar solo si se activó un tipo que no estaba cargado
|
||
_loading = false;
|
||
_loadedCellIds.clear();
|
||
loadAll();
|
||
}
|
||
|
||
function getAdvLayers() { return Object.assign({}, _advLayers); }
|
||
|
||
// ── Public API ────────────────────────────────────────────────────────────
|
||
var _loadedCellIds = new Set(); // evita recargar celdas ya cargadas
|
||
var _loading = false; // guard anti-reentrada: QWebChannel es secuencial
|
||
|
||
async function loadAll() {
|
||
_attachLayers();
|
||
if (!window.py) { console.warn('[ChartLayer] bridge not ready'); return; }
|
||
if (_loading) { console.log('[ChartLayer] carga en curso, ignorando llamada duplicada'); return; }
|
||
_loading = true;
|
||
|
||
// Obtener lista de celdas del bridge (JSON pequeño — sin problema de tamaño)
|
||
var allCells;
|
||
try {
|
||
allCells = JSON.parse(await _py('get_chart_cells'));
|
||
} catch(e) {
|
||
console.warn('[ChartLayer] get_chart_cells failed:', e.message || e);
|
||
_loading = false; return;
|
||
}
|
||
if (!allCells || !allCells.length) {
|
||
console.warn('[ChartLayer] no cells installed');
|
||
_loading = false; return;
|
||
}
|
||
|
||
var cellsWithBbox = allCells.filter(function(c){ return c.bbox; });
|
||
|
||
// Filtro de viewport — siempre usar extent si está disponible
|
||
var extent = _mapExtentLonLat();
|
||
var cells;
|
||
if (extent) {
|
||
cells = _cellsInView(cellsWithBbox, extent);
|
||
console.log('[ChartLayer] nivel:', _detailLevel,
|
||
'| extent:', extent.map(function(v){return v.toFixed(2);}).join(','),
|
||
'→', cells.length, '/', cellsWithBbox.length, 'celdas en view');
|
||
} else {
|
||
// Fallback: sin extent válido (primer frame), cargar todas
|
||
cells = cellsWithBbox;
|
||
console.log('[ChartLayer] nivel:', _detailLevel,
|
||
'| sin extent — todas las celdas con bbox:', cells.length);
|
||
}
|
||
|
||
// Filtrar celdas ya cargadas con el nivel actual
|
||
// (al cambiar nivel se hace reloadAll que limpia _loadedCellIds)
|
||
cells = cells.filter(function(c){ return !_loadedCellIds.has(c.id); });
|
||
if (!cells.length) {
|
||
console.log('[ChartLayer] sin celdas nuevas que cargar');
|
||
_loading = false; return;
|
||
}
|
||
|
||
var types = _dataTypes();
|
||
console.log('[ChartLayer] cargando', cells.length, 'celdas × [' + types.join(', ') + ']:',
|
||
cells.slice(0,6).map(function(c){ return c.id; }).join(', '),
|
||
cells.length > 6 ? '...' : '');
|
||
|
||
// Carga secuencial por celda/tipo — cada mensaje QWebChannel < ~700 KB
|
||
try {
|
||
var nFeat=0, nLand=0, nDep=0, nSoundg=0, nHaz=0, nZone=0;
|
||
|
||
if (types.indexOf('features') >= 0) {
|
||
var feat = await _loadAllCells(cells, 'features', true);
|
||
nFeat = _addGeoJSON(encSource, feat) || 0;
|
||
}
|
||
if (types.indexOf('land') >= 0) {
|
||
var land = await _loadAllCells(cells, 'land', false);
|
||
nLand = _addGeoJSON(landSource, land) || 0;
|
||
}
|
||
if (types.indexOf('hazards') >= 0) {
|
||
var haz = await _loadAllCells(cells, 'hazards', false);
|
||
nHaz = _addGeoJSON(hazardSource, haz) || 0;
|
||
}
|
||
if (types.indexOf('depths') >= 0) {
|
||
var dep = await _loadAllCells(cells, 'depths', false);
|
||
var depCounts = _addDepthsGeoJSON(dep); // split: SOUNDG → soundgSource
|
||
nDep = depCounts[0]; nSoundg = depCounts[1];
|
||
}
|
||
if (types.indexOf('zones') >= 0) {
|
||
var zone = await _loadAllCells(cells, 'zones', false);
|
||
nZone = _addGeoJSON(zoneSource, zone) || 0;
|
||
}
|
||
|
||
cells.forEach(function(c){ _loadedCellIds.add(c.id); });
|
||
console.log('[ChartLayer] LISTO — aids:', nFeat, 'land:', nLand,
|
||
'depths:', nDep, 'soundg:', nSoundg, 'hazards:', nHaz, 'zones:', nZone);
|
||
} finally {
|
||
_loading = false;
|
||
}
|
||
}
|
||
|
||
async function reloadAll() {
|
||
// Limpiar fuentes y set de celdas cargadas, luego recargar el viewport
|
||
[landSource, zoneSource, depthSource, soundgSource, encSource, hazardSource].forEach(function(s){ s.clear(); });
|
||
_loadedCellIds.clear();
|
||
_loading = false; // resetear guard para que loadAll pueda arrancar
|
||
return loadAll();
|
||
}
|
||
|
||
function setDetailLevel(lvl) {
|
||
if (lvl !== 'basic' && lvl !== 'medium' && lvl !== 'advanced') return;
|
||
_detailLevel = lvl;
|
||
['basic', 'medium', 'advanced'].forEach(function(l) {
|
||
var btn = document.getElementById('enc-lvl-' + l);
|
||
if (btn) btn.classList.toggle('active', l === lvl);
|
||
});
|
||
reloadAll();
|
||
}
|
||
|
||
// ── setChartMode — recolorea capas S-57 por modo (sin tocar ayudas IALA) ──
|
||
// Mismo principio que AR ECDIS presentation.js:
|
||
// · DEPARE/LNDARE/DEPCNT/SOUNDG → fill color cambia por modo
|
||
// · BOYLAT/BOYCAR/LIGHTS/etc. → colores IALA, NUNCA cambian
|
||
// · OSM tiles → opacidad reducida en modos oscuros
|
||
function setChartMode(mode) {
|
||
var validModes = ['night', 'dusk', 'day-std', 'day'];
|
||
if (validModes.indexOf(mode) < 0) return;
|
||
_chartMode = mode;
|
||
// Recolorar capas de área/profundidad — los SVG de ayudas no se tocan
|
||
depthSource.changed();
|
||
soundgSource.changed(); // sondas en capa dedicada también se recoloran
|
||
// landLayer usa _landStyle (función) que lee _chartMode dinámicamente — solo changed()
|
||
landSource.changed();
|
||
// Ajustar opacidad OSM — oscuro en modos noche/dusk, normal en día
|
||
var C = _modeColors();
|
||
var olMap = GPSMap.getOLMap();
|
||
if (olMap) {
|
||
olMap.getLayers().forEach(function(lyr) {
|
||
if (lyr && lyr.getSource && lyr.getSource() instanceof ol.source.OSM) {
|
||
lyr.setOpacity(C.osmOpacity);
|
||
}
|
||
});
|
||
}
|
||
console.log('[ChartLayer] modo presentación:', mode, '| OSM opacity:', C.osmOpacity);
|
||
}
|
||
|
||
// ── Hover tooltip & click info panel ─────────────────────────────────────
|
||
// Tooltip: div flotante sobre el mapa, aparece solo en hover.
|
||
// Panel: sección en panel derecho, aparece al hacer click en una ayuda.
|
||
|
||
var _ttEl = null; // tooltip DOM element
|
||
|
||
function _ensureTT() {
|
||
if (_ttEl) return _ttEl;
|
||
var wrap = document.getElementById('map-wrap');
|
||
if (!wrap) return null;
|
||
_ttEl = document.createElement('div');
|
||
_ttEl.className = 'enc-tooltip';
|
||
_ttEl.style.display = 'none';
|
||
wrap.appendChild(_ttEl);
|
||
return _ttEl;
|
||
}
|
||
|
||
// Texto HTML escapado
|
||
function _esc(s) {
|
||
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
// Formato grados-minutos "11°01.234'N"
|
||
function _fmtDeg(deg, pPos, pNeg) {
|
||
var a = Math.abs(deg), d = Math.floor(a), m = (a - d) * 60;
|
||
return d + '°' + m.toFixed(3) + '’' + (deg < 0 ? pNeg : pPos);
|
||
}
|
||
|
||
// Extraer info estructurada de un feature OL
|
||
function _featInfo(f) {
|
||
var aid = f.get('aid_type') || '';
|
||
var layer = (f.get('layer') || '').toUpperCase();
|
||
var name = f.get('name') || '';
|
||
var light = f.get('light_desc') || '';
|
||
var cat = f.get('category') || '';
|
||
var region = f.get('cell_region') || 'B';
|
||
var colours= f.get('colours') || [];
|
||
var depth = f.get('depth');
|
||
var dmax = f.get('depth_max');
|
||
|
||
var TYPE_LABELS = {
|
||
'LATERAL_PORT': 'Lateral Port', 'LATERAL_STBD': 'Lateral Starboard',
|
||
'LATERAL_PREF_PORT': 'Pref. Ch. Port', 'LATERAL_PREF_STBD': 'Pref. Ch. Stbd',
|
||
'LATERAL_UNKNOWN': 'Lateral Unknown', 'CARDINAL_N': 'Cardinal N',
|
||
'CARDINAL_S': 'Cardinal S', 'CARDINAL_E': 'Cardinal E',
|
||
'CARDINAL_W': 'Cardinal W', 'CARDINAL_UNKNOWN': 'Cardinal',
|
||
'ISOLATED_DANGER': 'Isolated Danger', 'SAFE_WATER': 'Safe Water',
|
||
'SPECIAL': 'Special Mark', 'LIGHT_POINT': 'Light',
|
||
'LANDMARK': 'Landmark', 'RACON': 'Racon',
|
||
'BEACON_GENERIC': 'Beacon', 'BUOY_GENERIC': 'Buoy',
|
||
'UNKNOWN': 'Unknown',
|
||
};
|
||
var CAT = { wreck:'Wreck', rock:'Underwater Rock', obstruction:'Obstruction' };
|
||
var CNAMES = {1:'White',2:'Black',3:'Red',4:'Green',5:'Blue',6:'Yellow',
|
||
7:'Grey',8:'Brown',9:'Amber',10:'Violet',11:'Orange',12:'Magenta'};
|
||
|
||
var typeLabel = TYPE_LABELS[aid] || CAT[cat] || layer || 'Feature';
|
||
var objClass = layer.startsWith('BCN') ? 'Beacon' : (layer.indexOf('BOY') >= 0 ? 'Buoy' : '');
|
||
if (objClass && typeLabel.indexOf(objClass) < 0) typeLabel = typeLabel + ' ' + objClass;
|
||
var regionStr = region === 'A' ? 'IALA-A' : 'IALA-B';
|
||
var colorStr = colours.map(function(c) { return CNAMES[c] || ''; }).filter(Boolean).join('/');
|
||
|
||
// Coordenadas del objeto
|
||
var coords = null;
|
||
try {
|
||
var geom = f.getGeometry();
|
||
if (geom) {
|
||
var pt = geom.getType() === 'Point'
|
||
? geom.getCoordinates()
|
||
: ol.extent.getCenter(geom.getExtent());
|
||
if (pt) coords = ol.proj.toLonLat(pt);
|
||
}
|
||
} catch(e) {}
|
||
|
||
return { aid, layer, name, light, cat, region, regionStr, colours, colorStr,
|
||
depth, dmax, coords, typeLabel };
|
||
}
|
||
|
||
// Posicionar tooltip cerca del cursor (evita salirse del mapa)
|
||
function _posTT(px, py) {
|
||
if (!_ttEl) return;
|
||
var wrap = document.getElementById('map-wrap');
|
||
var W = wrap ? wrap.offsetWidth : 800;
|
||
var H = wrap ? wrap.offsetHeight : 600;
|
||
var ew = _ttEl.offsetWidth || 180;
|
||
var eh = _ttEl.offsetHeight || 52;
|
||
var left = px + 16, top = py - Math.round(eh / 2);
|
||
if (left + ew > W - 8) left = px - ew - 12;
|
||
if (top < 4) top = 4;
|
||
if (top + eh > H - 4) top = H - eh - 4;
|
||
_ttEl.style.left = left + 'px';
|
||
_ttEl.style.top = top + 'px';
|
||
}
|
||
|
||
// Capas que NO deben mostrar tooltip (demasiado ruido)
|
||
var _noTooltipLayers = { DEPARE:1, LANDMASK:1, LNDARE:1, DEPCNT:1, SOUNDG:1 };
|
||
|
||
function _showTooltip(f, px, py) {
|
||
var el = _ensureTT(); if (!el) return;
|
||
var info = _featInfo(f);
|
||
if (_noTooltipLayers[info.layer]) { _hideTooltip(); return; }
|
||
|
||
var html = '<div class="enc-tt-type">' + _esc(info.typeLabel) + '</div>';
|
||
if (info.name) html += '<div class="enc-tt-name">' + _esc(info.name) + '</div>';
|
||
if (info.light) html += '<div class="enc-tt-light">💡 ' + _esc(info.light) + '</div>';
|
||
else if (info.colorStr) html += '<div class="enc-tt-cat">' + _esc(info.colorStr) + '</div>';
|
||
el.innerHTML = html;
|
||
el.style.display = 'block';
|
||
// Posicionar después de renderizar (para leer offsetWidth real)
|
||
setTimeout(function() { _posTT(px, py); }, 0);
|
||
}
|
||
|
||
function _hideTooltip() {
|
||
if (_ttEl) _ttEl.style.display = 'none';
|
||
}
|
||
|
||
// ── Info panel (panel derecho) ────────────────────────────────────────────
|
||
function _showInfoPanel(f) {
|
||
var el = document.getElementById('rp-feat-info');
|
||
if (!el) return;
|
||
var info = _featInfo(f);
|
||
|
||
// Color del badge según tipo de ayuda
|
||
var bc = '#607d8b';
|
||
if (info.aid.indexOf('PORT') >= 0) bc = '#009900';
|
||
if (info.aid.indexOf('STBD') >= 0) bc = '#cc1111';
|
||
if (info.aid.indexOf('CARDINAL') >= 0) bc = '#f0c000';
|
||
if (info.aid === 'ISOLATED_DANGER') bc = '#cc1111';
|
||
if (info.aid === 'SAFE_WATER') bc = '#ee2222';
|
||
if (info.aid === 'LIGHT_POINT') bc = '#cc00cc';
|
||
if (info.aid === 'LANDMARK') bc = '#8b6914';
|
||
if (info.cat === 'wreck') bc = '#e53935';
|
||
if (info.cat === 'rock') bc = '#f59e0b';
|
||
|
||
function row(lbl, val, cls) {
|
||
if (!val && val !== 0) return '';
|
||
return '<div class="fi-row' + (cls ? ' ' + cls : '') + '">' +
|
||
'<span class="fi-lbl">' + lbl + '</span>' +
|
||
'<span class="fi-val">' + _esc(String(val)) + '</span></div>';
|
||
}
|
||
|
||
var rows = '';
|
||
if (info.name) rows += row('Name', info.name);
|
||
if (info.light) rows += row('Light', info.light, 'fi-light');
|
||
if (info.colorStr) rows += row('Colors', info.colorStr);
|
||
if (info.aid && (info.aid.indexOf('LATERAL') >= 0 || info.aid.indexOf('CARDINAL') >= 0))
|
||
rows += row('Region', info.regionStr);
|
||
if (info.cat) rows += row('Cat.', info.cat);
|
||
if (info.dmax != null) rows += row('Depth max', info.dmax.toFixed(1) + ' m');
|
||
if (info.depth != null) rows += row('Depth', info.depth.toFixed(1) + ' m');
|
||
if (info.coords) {
|
||
rows += row('Lat', _fmtDeg(info.coords[1], 'N', 'S'));
|
||
rows += row('Lon', _fmtDeg(info.coords[0], 'E', 'W'));
|
||
}
|
||
|
||
el.innerHTML =
|
||
'<div class="fi-header">' +
|
||
'<span class="fi-badge" style="color:' + bc + '">■</span>' +
|
||
'<span class="fi-type">' + _esc(info.typeLabel) + '</span>' +
|
||
'<button class="fi-close" onclick="ChartLayer.hideFeatureInfo()">×</button>' +
|
||
'</div>' +
|
||
(rows ? '<div class="fi-rows">' + rows + '</div>'
|
||
: '<div class="fi-empty">No hay datos adicionales</div>');
|
||
el.style.display = 'block';
|
||
}
|
||
|
||
function hideFeatureInfo() {
|
||
var el = document.getElementById('rp-feat-info');
|
||
if (el) el.style.display = 'none';
|
||
}
|
||
|
||
return { loadAll, reloadAll, setDetailLevel, setLayerVisibility, getAdvLayers,
|
||
setChartMode, hideFeatureInfo,
|
||
encSource, depthSource, hazardSource, zoneSource, landSource };
|
||
})();
|