@mytec: iter8 ready for test
This commit is contained in:
117
frontend/src/utils/colorGradient.ts
Normal file
117
frontend/src/utils/colorGradient.ts
Normal 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 0–1
|
||||
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); // 0–1
|
||||
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 0–1 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 (0–1).
|
||||
* Uses pre-built LUT — O(1), no allocations.
|
||||
*
|
||||
* Returns [r, g, b] (0–255 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 0–1 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));
|
||||
}
|
||||
81
frontend/src/utils/geographicScale.ts
Normal file
81
frontend/src/utils/geographicScale.ts
Normal 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];
|
||||
}
|
||||
Reference in New Issue
Block a user