(data)
| 3139 | // maps a zoomed/panned window of the floor onto the plot area of the canvas, |
| 3140 | // leaving margins for the metre rulers. |
| 3141 | function _wifiHmFloorM() { |
| 3142 | const el = document.getElementById('wifi-hm-scale'); |
| 3143 | const v = parseFloat(el && el.value); |
| 3144 | return (v && v > 0) ? v : 10; |
| 3145 | } |
| 3146 | |
| 3147 | function _wifiHmViewport(W, H) { |
| 3148 | const ML = 34, MR = 10, MT = 20, MB = 24; // equal totals → the floor plot stays square |
| 3149 | const pw = W - ML - MR, ph = H - MT - MB; |
| 3150 | const v = _wifiHm.view; |
| 3151 | const span = 1 / v.zoom; // visible fraction of the floor |
| 3152 | const x0 = v.cx - span / 2, y0 = v.cy - span / 2; |
| 3153 | return { |
| 3154 | ML, MT, pw, ph, x0, y0, span, |
| 3155 | toX: wx => ML + (wx - x0) / span * pw, |
| 3156 | toY: wy => MT + (wy - y0) / span * ph, |
| 3157 | fromX: px => x0 + (px - ML) / pw * span, |
| 3158 | fromY: py => y0 + (py - MT) / ph * span, |
| 3159 | }; |
| 3160 | } |
| 3161 | |
| 3162 | function _wifiHmClampView() { |
| 3163 | const v = _wifiHm.view; |
| 3164 | v.zoom = Math.max(1, Math.min(16, v.zoom)); |
| 3165 | const half = 0.5 / v.zoom; |
| 3166 | v.cx = Math.max(half, Math.min(1 - half, v.cx)); |
| 3167 | v.cy = Math.max(half, Math.min(1 - half, v.cy)); |
| 3168 | } |
| 3169 | |
| 3170 | // Zoom by `factor`, keeping the point under (px,py) canvas px fixed (or the |
| 3171 | // view centre when called from the ± buttons). |
| 3172 | function wifiHmZoom(factor, px, py) { |
| 3173 | const cv = document.getElementById('wifi-heatmap'); if (!cv) return; |
| 3174 | const vp = _wifiHmViewport(cv.clientWidth, cv.clientHeight || cv.clientWidth); |
| 3175 | const v = _wifiHm.view; |
| 3176 | const ax = px != null ? vp.fromX(px) : v.cx; |
| 3177 | const ay = py != null ? vp.fromY(py) : v.cy; |
| 3178 | const oldZoom = v.zoom; |
| 3179 | v.zoom = Math.max(1, Math.min(16, v.zoom * factor)); |
| 3180 | const k = oldZoom / v.zoom; |
| 3181 | v.cx = ax - (ax - v.cx) * k; |
| 3182 | v.cy = ay - (ay - v.cy) * k; |
| 3183 | _wifiHmClampView(); |
| 3184 | wifiHeatmapRender(); |
| 3185 | } |
| 3186 | |
| 3187 | function wifiHmZoomReset() { |
| 3188 | _wifiHm.view = { zoom: 1, cx: 0.5, cy: 0.5 }; |
| 3189 | wifiHeatmapRender(); |
| 3190 | } |
| 3191 | |
| 3192 | function _wifiHmSyncScaleUI() { |
| 3193 | const m = _wifiHmFloorM(); |
| 3194 | const area = document.getElementById('wifi-hm-area'); |
| 3195 | if (area) area.textContent = Math.round(m * m).toLocaleString() + ' m²'; |
| 3196 | } |
| 3197 | |
| 3198 | // Floor-size input changed: keep the modelled APs' real-world size in sync |
no test coverage detected