thi-ng/umbrella

View on GitHub
packages/geom/src/simplify.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import type { Maybe } from "@thi.ng/api";
import { peek } from "@thi.ng/arrays/peek";
import type { MultiFn1O } from "@thi.ng/defmulti";
import { defmulti } from "@thi.ng/defmulti/defmulti";
import type { IShape, PathSegment } from "@thi.ng/geom-api";
import { simplify as _simplify } from "@thi.ng/geom-resample/simplify";
import type { Vec } from "@thi.ng/vectors";
import { ComplexPolygon } from "./api/complex-polygon.js";
import { Path } from "./api/path.js";
import { Polygon } from "./api/polygon.js";
import { Polyline } from "./api/polyline.js";
import { __copyAttribs } from "./internal/copy.js";
import { __dispatch } from "./internal/dispatch.js";
import { vertices } from "./vertices.js";

/**
 * Simplifies given 2D shape boundary using Douglas-Peucker algorithm
 * (implemented by
 * [`simplify()`](https://docs.thi.ng/umbrella/geom-resample/functions/simplify.html))
 * and given `threshold` distance (default: 1e-6, which removes only co-linear
 * vertices).
 *
 * @remarks
 * Currently only implemented for:
 *
 * - {@link ComplexPolygon}
 * - {@link Path}
 * - {@link Polygon}
 * - {@link Polyline}
 *
 * Use {@link asPath}, {@link asPolygon} or {@link asPolyline} to convert other
 * shape types first.
 *
 * @param shape
 * @param threshold
 */
export const simplify: MultiFn1O<IShape, number, IShape> = defmulti<
    any,
    Maybe<number>,
    IShape
>(
    __dispatch,
    {},
    {
        complexpoly: ($: ComplexPolygon, eps?) =>
            new ComplexPolygon(
                <Polygon>simplify($.boundary, eps),
                $.children.map((child) => <Polygon>simplify(child, eps)),
                __copyAttribs($)
            ),

        path: ($: Path, eps = 1e-6) => {
            const $simplifySegments = (segments: PathSegment[]) => {
                const res: PathSegment[] = [];
                const n = segments.length;
                let points!: Vec[] | null;
                let lastP!: Vec;
                for (let i = 0; i < n; i++) {
                    const s = segments[i];
                    if (s.type === "l" || s.type === "p") {
                        points = points
                            ? points.concat(vertices(s.geo!))
                            : vertices(s.geo!);
                        lastP = peek(points);
                    } else if (points) {
                        points.push(lastP);
                        res.push({
                            geo: new Polyline(_simplify(points, eps)),
                            type: "p",
                        });
                        points = null;
                    } else {
                        res.push({ ...s });
                    }
                }
                if (points) {
                    points.push(lastP);
                    res.push({
                        geo: new Polyline(points),
                        type: "p",
                    });
                }
                return res;
            };
            return new Path(
                $simplifySegments($.segments),
                $.subPaths.map($simplifySegments),
                __copyAttribs($)
            );
        },

        poly: ($: Polygon, eps = 1e-6) =>
            new Polygon(_simplify($.points, eps, true), __copyAttribs($)),

        polyline: ($: Polyline, eps = 1e-6) =>
            new Polyline(_simplify($.points, eps), __copyAttribs($)),
    }
);