@mytec: iter8 ready for test

This commit is contained in:
2026-01-30 14:11:45 +02:00
parent 91618353e6
commit bb9ca9960d
8 changed files with 755 additions and 4 deletions

View File

@@ -0,0 +1,117 @@
/**
* RSRP → color mapping with smooth gradient interpolation.
*
* Gradient stops are chosen to match standard RF planning tools:
* -130 dBm = deep blue (no service)
* -90 dBm = green (fair)
* -50 dBm = red (excellent)
*
* All functions are pure and allocation-free on the hot path
* (pre-built lookup table for fast per-pixel color resolution).
*/
interface GradientStop {
value: number; // normalized 01
r: number;
g: number;
b: number;
}
const GRADIENT_STOPS: GradientStop[] = [
{ value: 0.0, r: 26, g: 35, b: 126 }, // #1a237e — deep blue
{ value: 0.15, r: 13, g: 71, b: 161 }, // #0d47a1
{ value: 0.25, r: 33, g: 150, b: 243 }, // #2196f3 — blue
{ value: 0.35, r: 0, g: 188, b: 212 }, // #00bcd4 — cyan
{ value: 0.45, r: 0, g: 137, b: 123 }, // #00897b — teal
{ value: 0.55, r: 76, g: 175, b: 80 }, // #4caf50 — green
{ value: 0.65, r: 139, g: 195, b: 74 }, // #8bc34a — light green
{ value: 0.75, r: 255, g: 235, b: 59 }, // #ffeb3b — yellow
{ value: 0.85, r: 255, g: 152, b: 0 }, // #ff9800 — orange
{ value: 1.0, r: 244, g: 67, b: 54 }, // #f44336 — red
];
/**
* Pre-built 256-entry lookup table for O(1) color resolution.
* Each entry is [r, g, b].
*/
const COLOR_LUT: Uint8Array = buildColorLUT();
function buildColorLUT(): Uint8Array {
const size = 256;
const lut = new Uint8Array(size * 3);
for (let i = 0; i < size; i++) {
const value = i / (size - 1); // 01
const [r, g, b] = interpolateGradient(value);
lut[i * 3] = r;
lut[i * 3 + 1] = g;
lut[i * 3 + 2] = b;
}
return lut;
}
function interpolateGradient(value: number): [number, number, number] {
// Find surrounding stops
let lower = GRADIENT_STOPS[0];
let upper = GRADIENT_STOPS[GRADIENT_STOPS.length - 1];
for (let i = 0; i < GRADIENT_STOPS.length - 1; i++) {
if (value >= GRADIENT_STOPS[i].value && value <= GRADIENT_STOPS[i + 1].value) {
lower = GRADIENT_STOPS[i];
upper = GRADIENT_STOPS[i + 1];
break;
}
}
const range = upper.value - lower.value;
const t = range > 0 ? (value - lower.value) / range : 0;
return [
Math.round(lower.r + (upper.r - lower.r) * t),
Math.round(lower.g + (upper.g - lower.g) * t),
Math.round(lower.b + (upper.b - lower.b) * t),
];
}
/**
* Normalize RSRP (dBm) to 01 range.
*
* -130 dBm → 0.0
* -50 dBm → 1.0
*/
export function normalizeRSRP(rsrp: number): number {
const minRSRP = -130;
const maxRSRP = -50;
const normalized = (rsrp - minRSRP) / (maxRSRP - minRSRP);
return Math.max(0, Math.min(1, normalized));
}
/**
* Fast color lookup from normalized value (01).
* Uses pre-built LUT — O(1), no allocations.
*
* Returns [r, g, b] (0255 each).
*/
export function valueToColorRGB(value: number): [number, number, number] {
const idx = Math.max(0, Math.min(255, Math.round(value * 255))) * 3;
return [COLOR_LUT[idx], COLOR_LUT[idx + 1], COLOR_LUT[idx + 2]];
}
/**
* Full gradient interpolation (slower, for non-hot-path use like legends).
*/
export function valueToColor(value: number): [number, number, number] {
return interpolateGradient(Math.max(0, Math.min(1, value)));
}
/**
* Gaussian falloff for smooth point edges.
*
* Returns 01 weight based on distance from point center.
* sigma defaults to radius/3 for a natural falloff.
*/
export function gaussianWeight(distance: number, radius: number, sigma?: number): number {
const s = sigma ?? radius / 3;
return Math.exp(-(distance * distance) / (2 * s * s));
}

View File

@@ -0,0 +1,81 @@
/**
* Geographic scale utilities for Web Mercator (EPSG:3857) projection.
*
* Provides accurate meters ↔ pixels conversions, tile bounds,
* and lat/lon → tile pixel coordinate transforms.
*/
const EARTH_CIRCUMFERENCE_M = 40075017;
/**
* Calculate pixels per meter at given latitude and zoom level.
* Uses the Web Mercator distortion factor: cos(lat).
*/
export function getPixelsPerMeter(lat: number, zoom: number): number {
const tileSize = 256;
const numTiles = Math.pow(2, zoom);
const worldWidthPixels = tileSize * numTiles;
const metersPerPixelEquator = EARTH_CIRCUMFERENCE_M / worldWidthPixels;
const latRad = (lat * Math.PI) / 180;
const metersPerPixel = metersPerPixelEquator * Math.cos(latRad);
return 1 / metersPerPixel;
}
/**
* Convert geographic distance (meters) to pixel distance at given zoom/latitude.
*/
export function metersToPixels(meters: number, lat: number, zoom: number): number {
return meters * getPixelsPerMeter(lat, zoom);
}
/**
* Get tile geographic bounds (lat/lon) for given tile coordinates.
* Returns [[latMin, lonMin], [latMax, lonMax]].
*/
export function getTileBounds(
x: number,
y: number,
zoom: number
): [[number, number], [number, number]] {
const n = Math.pow(2, zoom);
const lonMin = (x / n) * 360 - 180;
const lonMax = ((x + 1) / n) * 360 - 180;
const latMin =
(Math.atan(Math.sinh(Math.PI * (1 - (2 * (y + 1)) / n))) * 180) / Math.PI;
const latMax =
(Math.atan(Math.sinh(Math.PI * (1 - (2 * y) / n))) * 180) / Math.PI;
return [
[latMin, lonMin],
[latMax, lonMax],
];
}
/**
* Convert lat/lon to pixel position within a specific tile.
* Returns [pixelX, pixelY] relative to tile's top-left corner.
*/
export function latLonToTilePixel(
lat: number,
lon: number,
tileX: number,
tileY: number,
zoom: number
): [number, number] {
const n = Math.pow(2, zoom);
// Point's world-tile coordinates
const worldX = ((lon + 180) / 360) * n;
const latRad = (lat * Math.PI) / 180;
const worldY =
((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) *
n;
// Offset within this tile (in pixels)
const pixelX = (worldX - tileX) * 256;
const pixelY = (worldY - tileY) * 256;
return [pixelX, pixelY];
}