Files

1499 lines
76 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 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 + '">&#9632;</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 };
})();