Skip to content

Commit

Permalink
feat(ui): Simplify and merge paths to improve performance
Browse files Browse the repository at this point in the history
  • Loading branch information
Hypfer committed Jan 10, 2025
1 parent 0bdf391 commit ac5e096
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 25 deletions.
2 changes: 1 addition & 1 deletion frontend/src/map/EditMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
60 changes: 39 additions & 21 deletions frontend/src/map/PathDrawer.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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(() => {
Expand All @@ -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":
Expand All @@ -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,
Expand All @@ -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";

svgPath += `${type} ${points[i] / pixelSize} ${points[i + 1] / pixelSize} `;
}
if (i === 0) {
type = "M";
}

commands += `${type} ${simplifiedPoints[i] / pixelSize} ${simplifiedPoints[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\"";
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/map/RobotCoverageMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
117 changes: 117 additions & 0 deletions frontend/src/map/utils/simplify_js.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit ac5e096

Please sign in to comment.