feat(tgu-ui): тепловая карта приоритетов и инспектор объектов на вкладке «Комплексный план»
Чекбокс «Тепловая карта» — фоновый слой-заливка ячеек сетки заданий по важности (cool→hot), источник GET /api/pcp-request/v1/cells/priority-map (cellsApi + drawHeatmap). Heatmap — полноценный слой MapPolygon (layer "heatmap", importance/cellNum), участвует в spatial-индексе и chooser, рисуется под контурами плана/заявок. Клик по объекту открывает правый инспектор ComplexInfoPanel (по аналогии с редактором): карточки ячейки приоритета, включения комплексного плана и заявки; при наложении — список объектов под точкой клика с раскрытием. При включённой тепловой карте ячейка всегда идёт первой в списке и раскрыта по умолчанию. Бэклог #44 отмечен сделанным.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import type { MapProjection } from "../geometry/mapProjection";
|
||||
import type { MapPolygon } from "../model/mapTypes";
|
||||
|
||||
type Pt = { x: number; y: number };
|
||||
|
||||
function unwrapX(points: Pt[], worldSize: number): Pt[] {
|
||||
if (points.length === 0) return [];
|
||||
const out: Pt[] = [points[0]];
|
||||
let prevX = points[0].x;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
let x = points[i].x;
|
||||
while (x - prevX > worldSize / 2) x -= worldSize;
|
||||
while (prevX - x > worldSize / 2) x += worldSize;
|
||||
out.push({ x, y: points[i].y });
|
||||
prevX = x;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Фоновый слой тепловой карты приоритетов: заливка ячеек сетки заданий цветом по важности
|
||||
* (cool→hot). Важность нормируется к максимуму набора, поэтому шкала подстраивается под данные.
|
||||
* Рисуется ПОД контурами плана/заявок, без обводки и hit-теста — это фон, а не объекты.
|
||||
*/
|
||||
export function drawHeatmap(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
cells: MapPolygon[],
|
||||
projection: MapProjection,
|
||||
viewport: { width: number; height: number }
|
||||
) {
|
||||
if (cells.length === 0) return;
|
||||
const maxImportance = cells.reduce((max, cell) => Math.max(max, cell.importance ?? 0), 0);
|
||||
if (maxImportance <= 0) return;
|
||||
|
||||
ctx.save();
|
||||
for (const cell of cells) {
|
||||
if (cell.points.length < 3) continue;
|
||||
const raw = cell.points.map((p) => projection.project(p));
|
||||
const pts = unwrapX(raw, projection.worldSize);
|
||||
// sqrt-нормировка слегка поднимает слабые ячейки, чтобы они не сливались с фоном.
|
||||
const t = Math.sqrt(Math.min(1, Math.max(0, (cell.importance ?? 0) / maxImportance)));
|
||||
ctx.fillStyle = heatColor(t);
|
||||
for (const dx of [0, projection.worldSize, -projection.worldSize]) {
|
||||
const visible = pts.some((p) => {
|
||||
const x = p.x + dx;
|
||||
return x >= -64 && x <= viewport.width + 64 && p.y >= -64 && p.y <= viewport.height + 64;
|
||||
});
|
||||
if (!visible) continue;
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < pts.length; i++) {
|
||||
if (i === 0) ctx.moveTo(pts[i].x + dx, pts[i].y);
|
||||
else ctx.lineTo(pts[i].x + dx, pts[i].y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Цвет тепловой карты по нормированной важности t∈[0,1]: бирюзовый (низкая) →
|
||||
* жёлтый → красный (высокая). Альфа растёт с важностью, чтобы «холодные» ячейки были легче.
|
||||
*/
|
||||
function heatColor(t: number): string {
|
||||
// Кусочно-линейная палитра: 0 → #2BD0C4 (cool), 0.5 → #F5D020 (warm), 1 → #F03B20 (hot).
|
||||
const cool = [43, 208, 196];
|
||||
const warm = [245, 208, 32];
|
||||
const hot = [240, 59, 32];
|
||||
let rgb: number[];
|
||||
if (t < 0.5) {
|
||||
rgb = mix(cool, warm, t / 0.5);
|
||||
} else {
|
||||
rgb = mix(warm, hot, (t - 0.5) / 0.5);
|
||||
}
|
||||
const alpha = 0.35 + 0.4 * t;
|
||||
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha.toFixed(3)})`;
|
||||
}
|
||||
|
||||
function mix(a: number[], b: number[], k: number): number[] {
|
||||
return [
|
||||
Math.round(a[0] + (b[0] - a[0]) * k),
|
||||
Math.round(a[1] + (b[1] - a[1]) * k),
|
||||
Math.round(a[2] + (b[2] - a[2]) * k),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user