|
| 1 | +// calibration.mjs — a "Calibration" settings tab that makes the chart render at TRUE |
| 2 | +// physical size on THIS screen. S-52 features are drawn at their real millimetre size |
| 3 | +// (icons, line weights, text), which only works if the app knows the screen's actual |
| 4 | +// CSS-pixel pitch. We can't read that from the browser, so the user calibrates it the |
| 5 | +// ECDIS way: a reference box that should be exactly 5 mm wide (the S-52 CHKSYM check |
| 6 | +// box), measured with a ruler. They enter what they actually measure and the whole |
| 7 | +// chart rescales. |
| 8 | +// |
| 9 | +// Registers itself as a settings contribution with a render(host) custom slot, like |
| 10 | +// the Advanced/dev-tools tab. Pure UI: the only app coupling is reading the current |
| 11 | +// pitch (app._pxPitch) and calling app.setPxPitch(mm), which persists + re-renders. |
| 12 | + |
| 13 | +import { DEFAULT_PX_PITCH_MM, clampPxPitch } from "../lib/util.mjs"; |
| 14 | + |
| 15 | +const REF_MM = 5; // the S-52 size-check box (CHKSYM01) is 5 mm × 5 mm |
| 16 | + |
| 17 | +// A feature P mm wide renders at P/pxPitch CSS px (chart-canvas _featureSizeScale), so |
| 18 | +// the reference box uses the same mapping — it's a faithful proxy for a 5 mm feature. |
| 19 | +const boxPx = (pitch) => Math.max(1, Math.round(REF_MM / pitch)); |
| 20 | + |
| 21 | +export function calibrationContribution(app) { |
| 22 | + return { |
| 23 | + id: "calibration", |
| 24 | + tab: { id: "calibration", label: "Calibration" }, |
| 25 | + order: 4, |
| 26 | + render: (host) => renderCalibration(host, app), |
| 27 | + }; |
| 28 | +} |
| 29 | + |
| 30 | +function renderCalibration(host, app) { |
| 31 | + const calibrated = typeof app._pxPitch === "number" && app._pxPitch > 0; |
| 32 | + const pitch = clampPxPitch(calibrated ? app._pxPitch : undefined); |
| 33 | + const px = boxPx(pitch); |
| 34 | + host.innerHTML = ` |
| 35 | + <style> |
| 36 | + .cal { max-width: 30rem; } |
| 37 | + .cal__intro { font-size: .9rem; line-height: 1.4; margin: 0 0 1rem; } |
| 38 | + .cal__row { display: flex; gap: 1.25rem; align-items: flex-start; flex-wrap: wrap; } |
| 39 | + .cal__box { background: #1b1b1b; outline: 1px solid #888; flex: 0 0 auto; } |
| 40 | + .cal__form { display: flex; flex-direction: column; gap: .6rem; } |
| 41 | + .cal__label { font-size: .85rem; display: flex; flex-direction: column; gap: .25rem; } |
| 42 | + .cal__in { display: inline-flex; align-items: baseline; gap: .35rem; } |
| 43 | + .cal__in input { width: 5rem; } |
| 44 | + .cal__btns { display: flex; gap: .5rem; } |
| 45 | + .cal__cur { font-size: .78rem; opacity: .7; margin: .25rem 0 0; } |
| 46 | + .cal__hint { font-size: .78rem; opacity: .7; margin: .5rem 0 0; } |
| 47 | + </style> |
| 48 | + <div class="cal"> |
| 49 | + <p class="cal__intro">Make the chart match real-world size. Hold a ruler to your screen and measure the box — it should be exactly <b>${REF_MM} mm</b> across. Enter what you actually measure and the whole chart (symbols, line weights, text) rescales to true physical size.</p> |
| 50 | + <div class="cal__row"> |
| 51 | + <div class="cal__box" style="width:${px}px;height:${px}px"></div> |
| 52 | + <div class="cal__form"> |
| 53 | + <label class="cal__label">Measured width |
| 54 | + <span class="cal__in"><input id="cal-mm" type="number" step="0.1" min="1" value="${REF_MM.toFixed(1)}"> mm</span> |
| 55 | + </label> |
| 56 | + <div class="cal__btns"> |
| 57 | + <button id="cal-apply" type="button">Apply</button> |
| 58 | + <button id="cal-reset" type="button">Reset</button> |
| 59 | + </div> |
| 60 | + <p class="cal__cur">Pixel pitch: <b>${pitch.toFixed(4)} mm</b>${calibrated ? "" : " (default — uncalibrated)"}</p> |
| 61 | + </div> |
| 62 | + </div> |
| 63 | + <p class="cal__hint">Tip: a wider measurement zooms the chart down, a narrower one up. Apply, then re-measure to confirm the box reads 5 mm.</p> |
| 64 | + </div>`; |
| 65 | + |
| 66 | + const mmInput = host.querySelector("#cal-mm"); |
| 67 | + host.querySelector("#cal-apply").addEventListener("click", () => { |
| 68 | + const measured = parseFloat(mmInput.value); |
| 69 | + if (!(measured > 0)) return; |
| 70 | + const cur = clampPxPitch(typeof app._pxPitch === "number" && app._pxPitch > 0 ? app._pxPitch : undefined); |
| 71 | + // Physical size ∝ 1/pitch, so to turn the measured size into REF_MM scale the |
| 72 | + // pitch by measured/REF (clampPxPitch in setPxPitch guards absurd values). |
| 73 | + app.setPxPitch(cur * (measured / REF_MM)); |
| 74 | + renderCalibration(host, app); // redraw at the new calibration to verify |
| 75 | + }); |
| 76 | + host.querySelector("#cal-reset").addEventListener("click", () => { |
| 77 | + app.setPxPitch(undefined); // back to the CSS-reference default |
| 78 | + renderCalibration(host, app); |
| 79 | + }); |
| 80 | +} |
| 81 | + |
| 82 | +export { DEFAULT_PX_PITCH_MM }; |
0 commit comments