maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
259 lines (218 loc) • 8.99 kB
text/typescript
import {describe, test, expect} from 'vitest';
import Point from '@mapbox/point-geometry';
import {getTileUnitsForMeters, roundPolygonCornersIfNeeded, roundedWallNormals} from './round_polygon_corners.ts';
import {CanonicalTileID} from '../../tile/tile_id.ts';
function round(p: Point):GeoJSON.Position {
// add 0 to normalize -0 into 0
const x = Math.round(p.x * 100) / 100 + 0;
const y = Math.round(p.y * 100) / 100 + 0;
return [x, y];
}
function normals(ring: Point[]): Array<{start: GeoJSON.Position; end: GeoJSON.Position} | null> {
return roundedWallNormals(ring).map(wall => wall && {start: round(wall.start), end: round(wall.end)});
}
const square = [
new Point(0, 0),
new Point(10, 0),
new Point(10, 10),
new Point(0, 10),
new Point(0, 0)
];
describe('roundPolygonCornersIfNeeded', () => {
const canonical = new CanonicalTileID(10, 500, 300);
const tileUnits = (distanceInMeters: number) => getTileUnitsForMeters(distanceInMeters, canonical);
test('returns original polygon reference when distance is zero', () => {
const input = [[
new Point(0, 0),
new Point(100, 0),
new Point(100, 100),
new Point(0, 100),
new Point(0, 0)
]];
const output = roundPolygonCornersIfNeeded(input, tileUnits(0));
expect(output).toBe(input);
});
test('returns original polygon reference when distance is negative', () => {
const input = [[
new Point(0, 0),
new Point(100, 0),
new Point(100, 100),
new Point(0, 100),
new Point(0, 0)
]];
const output = roundPolygonCornersIfNeeded(input, tileUnits(-1));
expect(output).toBe(input);
});
test('returns unchanged ring for degenerate line rings', () => {
const input = [[
new Point(0, 0),
new Point(50, 50),
new Point(0, 0)
]];
const output = roundPolygonCornersIfNeeded(input, tileUnits(10));
expect(output).toStrictEqual(input);
});
test('collapses every arc back onto its corner when the distance is below one tile unit', () => {
const output = roundPolygonCornersIfNeeded([square], tileUnits(0.1));
expect(output[0].map(round)).toEqual(square.map(round));
});
test('rounds corners of a square polygon into arc vertices', () => {
const output = roundPolygonCornersIfNeeded([square], tileUnits(2));
const points = output[0].map(round);
expect(points).toEqual([
[0, 1], [0, 0], [1, 0], [9, 0], [10, 0], [10, 1],
[10, 9], [10, 10], [9, 10], [1, 10], [0, 10], [0, 9], [0, 1]
]);
});
test('clamps corner rounding distance to 20% of edge length when requested distance is large', () => {
const output = roundPolygonCornersIfNeeded([square], tileUnits(1000));
const points = output[0].map(round);
expect(points).toEqual([
[0, 2], [0, 1], [1, 0], [2, 0], [8, 0], [9, 0], [10, 1], [10, 2],
[10, 8], [10, 9], [9, 10], [8, 10], [2, 10], [1, 10], [0, 9], [0, 8], [0, 2]
]);
});
test('emits only integer tile coordinates, so that the triangulator cannot merge arc points itself', () => {
const input = [[
new Point(0, 0),
new Point(137, 41),
new Point(96, 158),
new Point(13, 111),
new Point(0, 0)
]];
for (const distance of [0.5, 1, 2, 5, 50, 1000]) {
for (const ring of roundPolygonCornersIfNeeded(input, tileUnits(distance))) {
for (const point of ring) {
expect(point.x).toBe(Math.round(point.x));
expect(point.y).toBe(Math.round(point.y));
}
}
}
});
test('leaves the corners of a cut sharp and rounds the rest of the same ring', () => {
const input = [[
new Point(1000, -100),
new Point(3000, -100),
new Point(3000, 4000),
new Point(1000, 4000),
new Point(1000, -100)
]];
const output = roundPolygonCornersIfNeeded(input, tileUnits(1000));
const points = output[0].map(round);
expect(points).toEqual([
[1000, -100],
[3000, -100],
[3000, 3600],
[2946, 3800],
[2800, 3946],
[2600, 4000],
[1400, 4000],
[1200, 3946],
[1054, 3800],
[1000, 3600],
[1000, -100]
]);
});
test('rounds a corner of the feature that sits in the buffer, since only a cut stays sharp', () => {
const input = [[
new Point(1200, 200),
new Point(2000, -100),
new Point(2800, 200),
new Point(2000, 3000),
new Point(1200, 200)
]];
const output = roundPolygonCornersIfNeeded(input, tileUnits(1000));
const points = output[0].map(round);
expect(points).not.toContainEqual([2000, -100]);
expect(output[0].length).toBeGreaterThan(input[0].length);
});
test('drops arc points that collapse onto their neighbour', () => {
const output = roundPolygonCornersIfNeeded([square], tileUnits(2));
const ring = output[0];
for (let i = 1; i < ring.length; i++) {
expect([ring[i].x, ring[i].y]).not.toEqual([ring[i - 1].x, ring[i - 1].y]);
}
expect([ring[0].x, ring[0].y]).toEqual([ring[ring.length - 1].x, ring[ring.length - 1].y]);
});
test('preserves near-zero-degree spike vertex without adding arc points', () => {
const input = [[
new Point(0, 0),
new Point(100, 0),
new Point(0, 0.4),
new Point(0, 0)
]];
const output = roundPolygonCornersIfNeeded(input, tileUnits(5));
const points = output[0].map(round);
expect(points).toEqual([[0, 0], [100, 0], [0, 0.4], [0, 0]]);
});
test('preserves collinear 180-degree vertices without adding arc points', () => {
const input = [[
new Point(0, 0),
new Point(5, 0),
new Point(10, 0),
new Point(10, 10),
new Point(0, 10),
new Point(0, 0)
]];
const output = roundPolygonCornersIfNeeded(input, tileUnits(5));
const points = output[0].map(round);
expect(points).toEqual([
[0, 1], [0, 0], [1, 0], [5, 0], [9, 0], [10, 0], [10, 1],
[10, 8], [10, 9], [9, 10], [8, 10], [2, 10], [1, 10], [0, 9], [0, 8], [0, 1]
]);
});
test('scales arc points with corner sharpness up to the segment maximum', () => {
const input = [[
new Point(0, 0),
new Point(100, 0),
new Point(50, 20),
new Point(0, 0)
]];
const output = roundPolygonCornersIfNeeded(input, tileUnits(5));
const points = output[0].map(round);
expect(points).toEqual([
[2, 1], [2, 0], [98, 0], [98, 1], [52, 19], [50, 20], [48, 19], [2, 1]
]);
});
});
describe('roundedWallNormals', () => {
test('averages the normals of two walls meeting at a shallow turn', () => {
expect(normals([new Point(0, 0), new Point(10, 0), new Point(20, 5)])).toEqual([
null,
{start: [0, 1], end: [-0.24, 0.97]},
{start: [-0.24, 0.97], end: [-0.45, 0.89]}
]);
});
test('keeps the normal of a long wall where it meets a short one', () => {
expect(normals([new Point(0, 0), new Point(100, 0), new Point(102, 1)])).toEqual([
null,
{start: [0, 1], end: [0, 1]},
{start: [0, 1], end: [-0.45, 0.89]}
]);
});
test('keeps each wall its own normal at a sharp turn', () => {
expect(normals([new Point(0, 0), new Point(10, 0), new Point(20, 10)])).toEqual([
null,
{start: [0, 1], end: [0, 1]},
{start: [-0.71, 0.71], end: [-0.71, 0.71]}
]);
});
test('blends the last wall of a closed ring into the first', () => {
expect(normals([new Point(0, 0), new Point(10, 0), new Point(20, 5), new Point(-10, 5), new Point(0, 0)])).toEqual([
null,
{start: [0.24, 0.97], end: [-0.24, 0.97]},
{start: [-0.24, 0.97], end: [-0.45, 0.89]},
{start: [0, -1], end: [0, -1]},
{start: [0.45, 0.89], end: [0.24, 0.97]}
]);
});
test('skips boundary and zero-length walls and does not blend into them', () => {
expect(normals([new Point(-10, 0), new Point(-10, 10), new Point(0, 10), new Point(0, 10), new Point(10, 12)])).toEqual([
null,
null,
{start: [0, 1], end: [0, 1]},
null,
{start: [-0.2, 0.98], end: [-0.2, 0.98]}
]);
});
});