mirror of
https://github.com/Hypfer/Valetudo.git
synced 2025-10-26 11:27:27 +00:00
feat(ui): Simplify and merge paths to improve performance
This commit is contained in:
parent
0bdf3914e0
commit
ac5e09618a
@ -86,7 +86,7 @@ class EditMap extends Map<EditMapProps, EditMapState> {
|
||||
|
||||
if (this.props.mode === "virtual_restrictions") {
|
||||
const pathsImage = await PathDrawer.drawPaths( {
|
||||
paths: this.props.rawMap.entities.filter(e => {
|
||||
pathMapEntities: this.props.rawMap.entities.filter(e => {
|
||||
return e.type === RawMapEntityType.Path;
|
||||
}),
|
||||
mapWidth: this.props.rawMap.size.x,
|
||||
|
||||
@ -243,7 +243,7 @@ abstract class Map<P, S> extends React.Component<P & MapProps, S & MapState > {
|
||||
this.drawableComponents.push(this.mapLayerManager.getCanvas());
|
||||
|
||||
const pathsImage = await PathDrawer.drawPaths( {
|
||||
paths: this.props.rawMap.entities.filter(e => {
|
||||
pathMapEntities: this.props.rawMap.entities.filter(e => {
|
||||
return e.type === RawMapEntityType.Path || e.type === RawMapEntityType.PredictedPath;
|
||||
}),
|
||||
mapWidth: this.props.rawMap.size.x,
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import {RawMapEntity, RawMapEntityType} from "../api";
|
||||
import {PaletteMode} from "@mui/material";
|
||||
import {simplify} from "./utils/simplify_js";
|
||||
|
||||
type PathDrawerOptions = {
|
||||
paths: Array<RawMapEntity>,
|
||||
pathMapEntities: Array<RawMapEntity>,
|
||||
mapWidth: number,
|
||||
mapHeight: number,
|
||||
pixelSize: number,
|
||||
@ -12,11 +13,11 @@ type PathDrawerOptions = {
|
||||
};
|
||||
|
||||
export class PathDrawer {
|
||||
static drawPaths(options: PathDrawerOptions) : Promise<HTMLImageElement> {
|
||||
static drawPaths(options: PathDrawerOptions): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
if (options.paths.length > 0) {
|
||||
if (options.pathMapEntities.length > 0) {
|
||||
img.src = PathDrawer.createSVGDataUrlFromPaths(options);
|
||||
|
||||
img.decode().then(() => {
|
||||
@ -35,14 +36,14 @@ export class PathDrawer {
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
paletteMode,
|
||||
paths,
|
||||
pathMapEntities,
|
||||
pixelSize,
|
||||
width,
|
||||
opacity
|
||||
} = options;
|
||||
|
||||
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${mapWidth}" height="${mapHeight}" viewBox="0 0 ${mapWidth} ${mapHeight}">`;
|
||||
let pathColor : string;
|
||||
let pathColor: string;
|
||||
|
||||
switch (paletteMode) {
|
||||
case "light":
|
||||
@ -53,24 +54,37 @@ export class PathDrawer {
|
||||
break;
|
||||
}
|
||||
|
||||
paths.forEach(path => {
|
||||
svg += PathDrawer.createSVGPathFromPoints(
|
||||
path.points,
|
||||
path.type,
|
||||
const paths = pathMapEntities.filter(e => e.type === RawMapEntityType.Path).map(e => e.points);
|
||||
if (paths.length > 0) {
|
||||
svg += PathDrawer.createSVGPathFromPaths(
|
||||
paths,
|
||||
RawMapEntityType.Path,
|
||||
pixelSize,
|
||||
pathColor,
|
||||
width,
|
||||
opacity
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const predictedPaths = pathMapEntities.filter(e => e.type === RawMapEntityType.PredictedPath).map(e => e.points);
|
||||
if (predictedPaths.length > 0) {
|
||||
svg += PathDrawer.createSVGPathFromPaths(
|
||||
predictedPaths,
|
||||
RawMapEntityType.PredictedPath,
|
||||
pixelSize,
|
||||
pathColor,
|
||||
width,
|
||||
opacity
|
||||
);
|
||||
}
|
||||
|
||||
svg += "</svg>";
|
||||
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
private static createSVGPathFromPoints(
|
||||
points: Array<number>,
|
||||
private static createSVGPathFromPaths(
|
||||
paths: Array<Array<number>>,
|
||||
type: RawMapEntityType,
|
||||
pixelSize: number,
|
||||
color: string,
|
||||
@ -79,19 +93,23 @@ export class PathDrawer {
|
||||
) {
|
||||
const pathWidth = width ?? 0.5;
|
||||
const pathOpacity = opacity ?? 1;
|
||||
let svgPath = "<path d=\"";
|
||||
let commands = "";
|
||||
|
||||
for (let i = 0; i < points.length; i = i + 2) {
|
||||
let type = "L";
|
||||
paths.forEach(points => {
|
||||
const simplifiedPoints = simplify(points, 0.8);
|
||||
|
||||
if (i === 0) {
|
||||
type = "M";
|
||||
for (let i = 0; i < simplifiedPoints.length; i = i + 2) {
|
||||
let type = "L";
|
||||
|
||||
if (i === 0) {
|
||||
type = "M";
|
||||
}
|
||||
|
||||
commands += `${type} ${simplifiedPoints[i] / pixelSize} ${simplifiedPoints[i + 1] / pixelSize} `;
|
||||
}
|
||||
});
|
||||
|
||||
svgPath += `${type} ${points[i] / pixelSize} ${points[i + 1] / pixelSize} `;
|
||||
}
|
||||
|
||||
svgPath += `" fill="none" stroke="${color}" stroke-width="${pathWidth}" stroke-opacity="${pathOpacity}" stroke-linecap="round" stroke-linejoin="round"`;
|
||||
let svgPath = `<path d="${commands}" fill="none" stroke="${color}" stroke-width="${pathWidth}" stroke-opacity="${pathOpacity}" stroke-linecap="round" stroke-linejoin="round"`;
|
||||
|
||||
if (type === RawMapEntityType.PredictedPath) {
|
||||
svgPath += " stroke-dasharray=\"1,1\"";
|
||||
|
||||
@ -74,7 +74,7 @@ class RobotCoverageMap extends Map<CleanupCoverageMapProps, CleanupCoverageMapSt
|
||||
this.drawableComponents.push(this.mapLayerManager.getCanvas());
|
||||
|
||||
const coveragePathImage = await PathDrawer.drawPaths( {
|
||||
paths: this.props.rawMap.entities.filter(e => {
|
||||
pathMapEntities: this.props.rawMap.entities.filter(e => {
|
||||
return e.type === RawMapEntityType.Path;
|
||||
}),
|
||||
mapWidth: this.props.rawMap.size.x,
|
||||
@ -87,7 +87,7 @@ class RobotCoverageMap extends Map<CleanupCoverageMapProps, CleanupCoverageMapSt
|
||||
this.drawableComponents.push(coveragePathImage);
|
||||
|
||||
const pathsImage = await PathDrawer.drawPaths( {
|
||||
paths: this.props.rawMap.entities.filter(e => {
|
||||
pathMapEntities: this.props.rawMap.entities.filter(e => {
|
||||
return e.type === RawMapEntityType.Path || e.type === RawMapEntityType.PredictedPath;
|
||||
}),
|
||||
mapWidth: this.props.rawMap.size.x,
|
||||
|
||||
117
frontend/src/map/utils/simplify_js.ts
Normal file
117
frontend/src/map/utils/simplify_js.ts
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
(c) 2017, Vladimir Agafonkin
|
||||
Simplify.js, a high-performance JS polyline simplification library
|
||||
mourner.github.io/simplify-js
|
||||
|
||||
Licensed under BSD 2-Clause "Simplified" License
|
||||
|
||||
Adapted for use in Valetudo
|
||||
*/
|
||||
|
||||
// square distance between 2 points
|
||||
function getSqDist(points: number[], i: number, j: number): number {
|
||||
const dx = points[i] - points[j];
|
||||
const dy = points[i + 1] - points[j + 1];
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
// square distance from a point to a segment
|
||||
function getSqSegDist(points: number[], p: number, p1: number, p2: number): number {
|
||||
let x = points[p1];
|
||||
let y = points[p1 + 1];
|
||||
const dx = points[p2] - x;
|
||||
const dy = points[p2 + 1] - y;
|
||||
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
const t = ((points[p] - x) * dx + (points[p + 1] - y) * dy) / (dx * dx + dy * dy);
|
||||
|
||||
if (t > 1) {
|
||||
x = points[p2];
|
||||
y = points[p2 + 1];
|
||||
} else if (t > 0) {
|
||||
x += dx * t;
|
||||
y += dy * t;
|
||||
}
|
||||
}
|
||||
|
||||
const dX = points[p] - x;
|
||||
const dY = points[p + 1] - y;
|
||||
|
||||
return dX * dX + dY * dY;
|
||||
}
|
||||
|
||||
// basic distance-based simplification
|
||||
function simplifyRadialDist(points: number[], sqTolerance: number): number[] {
|
||||
const newPoints: number[] = points.slice(0, 2);
|
||||
|
||||
for (let i = 2; i < points.length; i += 2) {
|
||||
if (getSqDist(points, i, i - 2) > sqTolerance) {
|
||||
newPoints.push(points[i], points[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (points.length >= 2 && (newPoints[newPoints.length - 2] !== points[points.length - 2] ||
|
||||
newPoints[newPoints.length - 1] !== points[points.length - 1])) {
|
||||
newPoints.push(points[points.length - 2], points[points.length - 1]);
|
||||
}
|
||||
|
||||
return newPoints;
|
||||
}
|
||||
|
||||
function simplifyDPStep(
|
||||
points: number[],
|
||||
first: number,
|
||||
last: number,
|
||||
sqTolerance: number,
|
||||
simplified: number[]
|
||||
): void {
|
||||
let maxSqDist = sqTolerance;
|
||||
let index = -1;
|
||||
|
||||
for (let i = first + 2; i < last; i += 2) {
|
||||
const sqDist = getSqSegDist(points, i, first, last);
|
||||
|
||||
if (sqDist > maxSqDist) {
|
||||
index = i;
|
||||
maxSqDist = sqDist;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxSqDist > sqTolerance && index !== -1) {
|
||||
if (index - first > 2) {
|
||||
simplifyDPStep(points, first, index, sqTolerance, simplified);
|
||||
}
|
||||
simplified.push(points[index], points[index + 1]);
|
||||
if (last - index > 2) {
|
||||
simplifyDPStep(points, index, last, sqTolerance, simplified);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// simplification using Ramer-Douglas-Peucker algorithm
|
||||
function simplifyDouglasPeucker(points: number[], sqTolerance: number): number[] {
|
||||
const last = points.length - 2;
|
||||
const simplified = [points[0], points[1]];
|
||||
|
||||
simplifyDPStep(points, 0, last, sqTolerance, simplified);
|
||||
simplified.push(points[last], points[last + 1]);
|
||||
|
||||
return simplified;
|
||||
}
|
||||
|
||||
export function simplify(
|
||||
points: number[],
|
||||
tolerance?: number,
|
||||
highestQuality?: boolean
|
||||
): number[] {
|
||||
if (points.length <= 4) {
|
||||
return points;
|
||||
}
|
||||
|
||||
const sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1;
|
||||
|
||||
points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
|
||||
points = simplifyDouglasPeucker(points, sqTolerance);
|
||||
|
||||
return points;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user