svg-path-simplify 0.1.3 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +10 -0
  2. package/dist/svg-path-simplify.esm.js +3905 -1533
  3. package/dist/svg-path-simplify.esm.min.js +13 -1
  4. package/dist/svg-path-simplify.js +3923 -1551
  5. package/dist/svg-path-simplify.min.js +13 -1
  6. package/dist/svg-path-simplify.min.js.gz +0 -0
  7. package/index.html +61 -31
  8. package/package.json +3 -5
  9. package/src/constants.js +3 -0
  10. package/src/index-node.js +0 -1
  11. package/src/index.js +26 -0
  12. package/src/pathData_simplify_cubic.js +74 -31
  13. package/src/pathData_simplify_cubicsToArcs.js +566 -0
  14. package/src/pathData_simplify_harmonize_cpts.js +170 -0
  15. package/src/pathData_simplify_revertToquadratics.js +21 -0
  16. package/src/pathSimplify-main.js +253 -86
  17. package/src/poly-fit-curve-schneider.js +570 -0
  18. package/src/simplify_poly_RDP.js +146 -0
  19. package/src/simplify_poly_radial_distance.js +100 -0
  20. package/src/svg_getViewbox.js +1 -1
  21. package/src/svgii/geometry.js +389 -63
  22. package/src/svgii/geometry_area.js +2 -1
  23. package/src/svgii/pathData_analyze.js +259 -212
  24. package/src/svgii/pathData_convert.js +91 -663
  25. package/src/svgii/pathData_fromPoly.js +12 -0
  26. package/src/svgii/pathData_parse.js +90 -89
  27. package/src/svgii/pathData_parse_els.js +3 -0
  28. package/src/svgii/pathData_parse_fontello.js +449 -0
  29. package/src/svgii/pathData_remove_collinear.js +44 -37
  30. package/src/svgii/pathData_reorder.js +2 -1
  31. package/src/svgii/pathData_simplify_redraw.js +343 -0
  32. package/src/svgii/pathData_simplify_refineCorners.js +18 -9
  33. package/src/svgii/pathData_simplify_refineExtremes.js +19 -78
  34. package/src/svgii/pathData_split.js +42 -45
  35. package/src/svgii/pathData_toPolygon.js +130 -4
  36. package/src/svgii/poly_analyze.js +470 -14
  37. package/src/svgii/poly_to_pathdata.js +224 -19
  38. package/src/svgii/rounding.js +55 -112
  39. package/src/svgii/svg_cleanup.js +13 -1
  40. package/src/svgii/visualize.js +8 -3
  41. package/{debug.cjs → tests/debug.cjs} +3 -0
  42. /package/{test.js → tests/test.js} +0 -0
  43. /package/{testSVG.js → tests/testSVG.js} +0 -0
@@ -0,0 +1,570 @@
1
+
2
+ /**
3
+ * Algorithm for Automatically Fitting Digitized Curves
4
+ * by Philip J. Schneider
5
+ * "Graphics Gems", Academic Press, 1990
6
+ * The MIT License (MIT)
7
+ * https://github.com/soswow/fit-curves
8
+ *
9
+ */
10
+
11
+ import { harmonizeCubicCpts, harmonizeCubicCptsThird } from "./pathData_simplify_harmonize_cpts";
12
+ import { getAngle, getDistance, pointAtT, rotatePoint } from "./svgii/geometry";
13
+ import { renderPoint } from "./svgii/visualize";
14
+
15
+
16
+ let polyPtsToArray = (pts) => {
17
+ return Array.from(pts).map(pt => [pt.x, pt.y])
18
+ }
19
+
20
+ // convert to pathdata
21
+ let bezierPtsToPathData = (beziers) => {
22
+ //let pathData = [{ type: 'M', values: [beziers[0][0][0], beziers[0][0][1]] }];
23
+ let pathData = [];
24
+
25
+ beziers.forEach(bez => {
26
+ let cp1 = bez[1]
27
+ let cp2 = bez[2]
28
+ let p = bez[3]
29
+ let com = { type: 'C', values: [cp1[0], cp1[1], cp2[0], cp2[1], p[0], p[1]] }
30
+ pathData.push(com)
31
+ })
32
+
33
+ return pathData
34
+ }
35
+
36
+
37
+
38
+ /**
39
+ * Fit one or more Bezier curves to a set of pts.
40
+ *
41
+ */
42
+ export function fitCurveN(pts, maxError, adjustCpts = true, harmonize= true) {
43
+
44
+ if (!Array.isArray(pts) || (pts[0].x !== undefined)) {
45
+
46
+ if (pts[0].x !== undefined) {
47
+ pts = polyPtsToArray(pts)
48
+ } else {
49
+ throw Error("Not a valid point array");
50
+ }
51
+ }
52
+
53
+ //console.log(pts);
54
+
55
+ // Remove duplicate pts
56
+ pts = pts.filter(function (point, i) {
57
+ return i === 0 || !point.every((val, j) => {
58
+ return val === pts[i - 1][j];
59
+ });
60
+ });
61
+
62
+ if (pts.length === 1) {
63
+ //return [{ type: 'L', values: [pts[0][0], pts[0][1]] }];
64
+ return [];
65
+ }
66
+
67
+
68
+ // single lineto
69
+ if (pts.length === 2) {
70
+ return [
71
+ { type: 'L', values: [pts[0][0], pts[0][1]] },
72
+ { type: 'L', values: [pts[1][0], pts[1][1]] }
73
+ ]
74
+ }
75
+
76
+
77
+ let len = pts.length;
78
+
79
+ let leftTangent = createTangent(pts[1], pts[0]);
80
+ let rightTangent = createTangent(pts[len - 2], pts[len - 1]);
81
+
82
+ let beziers = fitCubic(pts, leftTangent, rightTangent, maxError);
83
+
84
+ // create pathdata
85
+ let pathData = bezierPtsToPathData(beziers)
86
+
87
+
88
+
89
+ // adjustCpts -post
90
+ //adjustCpts = false
91
+ //harmonize= false;
92
+
93
+ let cp1, cp2;
94
+ if (adjustCpts) {
95
+
96
+ let len2 = pathData.length;
97
+ let com1 = pathData[0]
98
+
99
+ // last cubic segment
100
+ let com2 = pathData[len2 - 1]
101
+
102
+ //adjust 1st and last angle
103
+ let p0 = { x: pts[0][0], y: pts[0][1] }
104
+ let p1 = { x: pts[1][0], y: pts[1][1] }
105
+ let p2 = pts[2] ? { x: pts[2][0], y: pts[2][1] } : null
106
+
107
+ if (p2) {
108
+ cp1 = { x: com1.values[0], y: com1.values[1] }
109
+ cp1 = adjustTangentAngle(cp1, p0, p1, p2)
110
+ com1.values[0] = cp1.x
111
+ com1.values[1] = cp1.y
112
+ }
113
+
114
+ let pL = { x: pts[len - 1][0], y: pts[len - 1][1] }
115
+ let pL1 = { x: pts[len - 2][0], y: pts[len - 2][1] }
116
+ let pL2 = pts[len - 3] ? { x: pts[len - 3][0], y: pts[len - 3][1] } : null
117
+
118
+ if (pL2) {
119
+ cp2 = { x: com2.values[2], y: com2.values[3] }
120
+ cp2 = adjustTangentAngle(cp2, pL, pL1, pL2)
121
+ com2.values[2] = cp2.x
122
+ com2.values[3] = cp2.y
123
+ }
124
+
125
+ // harmonize too tight tangents
126
+ //let harmonize= true;
127
+ if(harmonize){
128
+ pathData = harmonizeCubicCptsThird([{ type: 'M', values: [pts[0][0], pts[0][1]] },
129
+ ...pathData])
130
+ pathData.shift()
131
+ }
132
+
133
+
134
+ }
135
+
136
+ //console.log('pathData schneider', pathData);
137
+ return pathData
138
+ }
139
+
140
+
141
+ function adjustTangentAngle(cp, p0, p1, p2) {
142
+ let ang1 = getAngle(p0, p1)
143
+ let ang2 = getAngle(p0, p2)
144
+ let angDiff = (ang2 - ang1)
145
+ cp = rotatePoint(cp, p0.x, p0.y, -angDiff)
146
+ return cp
147
+ }
148
+
149
+
150
+ /**
151
+ * Use least-squares method to find Bezier control pts for region.
152
+ */
153
+ let generateBezier = (pts, parameters, leftTangent, rightTangent) => {
154
+
155
+ //Bezier curve ctl pts
156
+ let a, tmp, u, ux, firstPoint = pts[0], lastPoint = pts[pts.length - 1];
157
+
158
+ let bezCurve = [firstPoint, null, null, lastPoint];
159
+ let A = zeros_Xx2x2(parameters.length);
160
+ let len = parameters.length;
161
+
162
+ for (let i = 0; i < len; i++) {
163
+ u = parameters[i];
164
+ ux = 1 - u;
165
+ a = A[i];
166
+
167
+ a[0] = mulItems(leftTangent, 3 * u * (ux * ux));
168
+ a[1] = mulItems(rightTangent, 3 * ux * (u * u));
169
+ }
170
+
171
+ //Create the C and X matrices
172
+ let C = [[0, 0], [0, 0]];
173
+ let X = [0, 0];
174
+ let l = pts.length;
175
+
176
+ for (let i = 0; i < l; i++) {
177
+ u = parameters[i];
178
+ a = A[i];
179
+
180
+ C[0][0] += dot(a[0], a[0]);
181
+ C[0][1] += dot(a[0], a[1]);
182
+ C[1][0] += dot(a[0], a[1]);
183
+ C[1][1] += dot(a[1], a[1]);
184
+
185
+ tmp = subtract(pts[i], pointAtT([firstPoint, firstPoint, lastPoint, lastPoint], u));
186
+
187
+ X[0] += dot(a[0], tmp);
188
+ X[1] += dot(a[1], tmp);
189
+ }
190
+
191
+ //Compute the determinants of C and X
192
+ let det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
193
+ let det_C0_X = C[0][0] * X[1] - C[1][0] * X[0];
194
+ let det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
195
+
196
+ //Finally, derive alpha values
197
+ let alpha_l = det_C0_C1 === 0 ? 0 : det_X_C1 / det_C0_C1;
198
+ let alpha_r = det_C0_C1 === 0 ? 0 : det_C0_X / det_C0_C1;
199
+ let segLength = getDistance(firstPoint, lastPoint, true);
200
+ let epsilon = 1.0e-6 * segLength;
201
+
202
+ if (alpha_l < epsilon || alpha_r < epsilon) {
203
+ //Fall back on standard (probably inaccurate) formula, and subdivide further if needed.
204
+ bezCurve[1] = addArrays(firstPoint, mulItems(leftTangent, segLength * 0.333));
205
+ bezCurve[2] = addArrays(lastPoint, mulItems(rightTangent, segLength * 0.333));
206
+ } else {
207
+ // First and last control pts of the Bezier curve
208
+ bezCurve[1] = addArrays(firstPoint, mulItems(leftTangent, alpha_l));
209
+ bezCurve[2] = addArrays(lastPoint, mulItems(rightTangent, alpha_r));
210
+ }
211
+
212
+ return bezCurve;
213
+ };
214
+
215
+
216
+ /**
217
+ * Fit a Bezier curve to a (sub)set of digitized pts.
218
+ * Your code should not call this function directly. Use {@link fitCurve} instead.
219
+ *control-point-1, control-point-2, second-point] and pts are [x, y]
220
+ */
221
+ let fitCubic = (pts, leftTangent, rightTangent, error) => {
222
+ //Max times to try iterating (to find an acceptable curve)
223
+ let MaxIterations = 20;
224
+ let bezCurve;
225
+
226
+ //Use heuristic if region only has two pts in it
227
+ if (pts.length === 2) {
228
+ let dist = getDistance(pts[0], pts[1], true) * 0.333;
229
+ bezCurve = [pts[0], addArrays(pts[0], mulItems(leftTangent, dist)), addArrays(pts[1], mulItems(rightTangent, dist)), pts[1]];
230
+ return [bezCurve];
231
+ }
232
+
233
+ //Parameterize pts, and attempt to fit curve
234
+ let u = chordLengthParameterize(pts);
235
+ let _generateAndReport = generateAndReport(pts, u, u, leftTangent, rightTangent);
236
+
237
+ bezCurve = _generateAndReport[0];
238
+ let maxError = _generateAndReport[1];
239
+ let splitPoint = _generateAndReport[2];
240
+
241
+ if (maxError === 0 || maxError < error) {
242
+ return [bezCurve];
243
+ }
244
+ //If error not too large, try some reparameterization and iteration
245
+ if (maxError < error * error) {
246
+
247
+ let uPrime = u;
248
+ let prevErr = maxError;
249
+ let prevSplit = splitPoint;
250
+
251
+ for (let i = 0; i < MaxIterations; i++) {
252
+
253
+ uPrime = reparameterize(bezCurve, pts, uPrime);
254
+
255
+ let _generateAndReport2 = generateAndReport(pts, u, uPrime, leftTangent, rightTangent);
256
+
257
+ bezCurve = _generateAndReport2[0];
258
+ maxError = _generateAndReport2[1];
259
+ splitPoint = _generateAndReport2[2];
260
+
261
+
262
+ if (maxError < error) {
263
+ return [bezCurve];
264
+ }
265
+ //If the development of the fitted curve grinds to a halt,
266
+ //we abort this attempt (and try a shorter curve):
267
+ else if (splitPoint === prevSplit) {
268
+ let errChange = maxError / prevErr;
269
+ if (errChange > .9999 && errChange < 1.0001) {
270
+ break;
271
+ }
272
+ }
273
+
274
+ prevErr = maxError;
275
+ prevSplit = splitPoint;
276
+ }
277
+ }
278
+
279
+ //Fitting failed -- split at max error point and fit recursively
280
+ let beziers = [];
281
+ let centerVector = subtract(pts[splitPoint - 1], pts[splitPoint + 1]);
282
+
283
+ if (centerVector.every(function (val) {
284
+ return val === 0;
285
+ })) {
286
+ //[x,y] -> [-y,x]: http://stackoverflow.com/a/4780141/1869660
287
+ centerVector = subtract(pts[splitPoint - 1], pts[splitPoint]);
288
+ let _ref = [-centerVector[1], centerVector[0]];
289
+ centerVector[0] = _ref[0];
290
+ centerVector[1] = _ref[1];
291
+ }
292
+
293
+ let toCenterTangent = normalize(centerVector);
294
+ //To and from need to point in opposite directions:
295
+ let fromCenterTangent = mulItems(toCenterTangent, -1);
296
+
297
+
298
+ beziers = beziers.concat(fitCubic(pts.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error));
299
+ beziers = beziers.concat(fitCubic(pts.slice(splitPoint), fromCenterTangent, rightTangent, error));
300
+ return beziers;
301
+ };
302
+
303
+
304
+ const generateAndReport = (pts, paramsOrig, paramsPrime, leftTangent, rightTangent) => {
305
+ let bezCurve, maxError, splitPoint;
306
+
307
+ bezCurve = generateBezier(pts, paramsPrime, leftTangent, rightTangent);
308
+ let _computeMaxError = computeMaxError(pts, bezCurve, paramsOrig);
309
+
310
+ maxError = _computeMaxError[0];
311
+ splitPoint = _computeMaxError[1];
312
+
313
+ return [bezCurve, maxError, splitPoint];
314
+ }
315
+
316
+
317
+
318
+ /**
319
+ * Given set of pts and their parameterization, try to find a better parameterization.
320
+ */
321
+ function reparameterize(bezier, pts, parameters) {
322
+ return parameters.map((p, i) => {
323
+ return newtonRaphsonRootFind(bezier, pts[i], p);
324
+ });
325
+ };
326
+
327
+ /**
328
+ * Use Newton-Raphson iteration to find better root.
329
+ */
330
+
331
+ function newtonRaphsonRootFind(bez, point, u) {
332
+ // bez is [p0, cp1, cp2, p1] where each is [x, y]
333
+ // point is the target point [x, y] we're trying to get close to
334
+ // u is our current parameter value (0-1)
335
+
336
+ // Calculate q(u) - point (the vector from target to curve point)
337
+ //let q = bezierQ(bez, u);
338
+ let q = pointAtT(bez, u)
339
+
340
+ let dx = q[0] - point[0];
341
+ let dy = q[1] - point[1];
342
+
343
+ // First derivative (tangent vector at u)
344
+ let qp = bezierQprime(bez, u);
345
+
346
+ // Numerator: dot product of (q(u) - point) and q'(u)
347
+ // This represents how much the error aligns with the tangent
348
+ let numerator = dx * qp[0] + dy * qp[1];
349
+
350
+ // Denominator: |q'(u)|² + 2 * (q(u)-point) · q''(u)
351
+ // First part: squared length of tangent vector
352
+ let qpLenSq = qp[0] * qp[0] + qp[1] * qp[1];
353
+
354
+ // Second derivative
355
+ let qpp = bezierQprime(bez, u, true);
356
+
357
+ // Second part: 2 * (q(u)-point) · q''(u)
358
+ let secondPart = 2 * (dx * qpp[0] + dy * qpp[1]);
359
+
360
+ let denominator = qpLenSq + secondPart;
361
+
362
+ if (Math.abs(denominator) < 1e-10) { // Avoid division by zero
363
+ return u;
364
+ }
365
+
366
+ // Newton-Raphson step: u_new = u - f(u)/f'(u)
367
+ return u - numerator / denominator;
368
+ }
369
+
370
+
371
+
372
+
373
+ /**
374
+ * Assign parameter values to digitized pts using relative distances between pts.
375
+ */
376
+ function chordLengthParameterize(pts) {
377
+ let u = [];
378
+ let l = pts.length;
379
+ let p0 = pts[0];
380
+ let p = pts[1];
381
+ let currU = 0
382
+ let prevU = 0
383
+
384
+ //prevU = 0
385
+ //console.log('prevU', prevU);
386
+
387
+
388
+ for (let i = 0; i < l; i++) {
389
+ p = pts[i];
390
+ //currU = i ? prevU + length(subtract(p, p0)) : 0;
391
+ currU = prevU + getDistance(p, p0, true);
392
+ u.push(currU);
393
+ prevU = currU;
394
+
395
+ p0 = p;
396
+ };
397
+
398
+
399
+ u = u.map(function (x) {
400
+ return x / prevU;
401
+ });
402
+
403
+ return u;
404
+ };
405
+
406
+ /**
407
+ * Find the maximum squared distance of digitized pts to fitted curve.
408
+ */
409
+ function computeMaxError(pts, bez, parameters) {
410
+ let dist,
411
+ maxDist,
412
+ splitPoint,
413
+ v,
414
+ i, point, t;
415
+
416
+ maxDist = 0;
417
+ splitPoint = Math.floor(pts.length * 0.5);
418
+
419
+ //console.log('computeMaxError', pts, bez, parameters);
420
+
421
+ let t_distMap = mapTtoRelativeDistances(bez, 10);
422
+ let l = pts.length
423
+
424
+ for (i = 0; i < l; i++) {
425
+ point = pts[i];
426
+ //Find 't' for a point on the bez curve that's as close to 'point' as possible:
427
+ t = find_t(parameters[i], t_distMap, 10);
428
+
429
+ v = subtract(pointAtT(bez, t), point);
430
+ dist = v[0] * v[0] + v[1] * v[1];
431
+
432
+ if (dist > maxDist) {
433
+ maxDist = dist;
434
+ splitPoint = i;
435
+ }
436
+ }
437
+
438
+ return [maxDist, splitPoint];
439
+ };
440
+
441
+ //Sample 't's and map them to relative distances along the curve:
442
+ function mapTtoRelativeDistances(bez, B_parts) {
443
+ let B_t_curr;
444
+ let B_t_dist = [0];
445
+ let B_t_prev = bez[0];
446
+ let sumLen = 0;
447
+
448
+ for (let i = 1; i <= B_parts; i++) {
449
+ B_t_curr = pointAtT(bez, i / B_parts);
450
+ sumLen += getDistance(B_t_curr, B_t_prev, true);
451
+ B_t_dist.push(sumLen);
452
+ B_t_prev = B_t_curr;
453
+ }
454
+
455
+ //Normalize B_length to the same interval as the parameter distances; 0 to 1:
456
+ B_t_dist = B_t_dist.map(function (x) {
457
+ return x / sumLen;
458
+ });
459
+ return B_t_dist;
460
+ };
461
+
462
+ function find_t(param, t_distMap, B_parts) {
463
+
464
+ if (param < 0) {
465
+ return 0;
466
+ }
467
+ if (param > 1) {
468
+ return 1;
469
+ }
470
+
471
+ let lenMax, lenMin, tMax, tMin, t;
472
+
473
+ //Find the two t-s that the current param distance lies between,
474
+ //and then interpolate a somewhat accurate value for the exact t:
475
+ for (let i = 1; i <= B_parts; i++) {
476
+
477
+ if (param <= t_distMap[i]) {
478
+ tMin = (i - 1) / B_parts;
479
+ tMax = i / B_parts;
480
+ lenMin = t_distMap[i - 1];
481
+ lenMax = t_distMap[i];
482
+ t = (param - lenMin) / (lenMax - lenMin) * (tMax - tMin) + tMin;
483
+ break;
484
+ }
485
+ }
486
+ return t;
487
+ }
488
+
489
+ /**
490
+ * Creates a vector of length 1 which shows the direction from B to A
491
+ */
492
+ function createTangent(p1, p2) {
493
+ // Returns unit vector pointing from B to A
494
+ let dx = p1[0] - p2[0];
495
+ let dy = p1[1] - p2[1];
496
+ let length = Math.sqrt(dx * dx + dy * dy);
497
+
498
+ if (length === 0) return [0, 0];
499
+ return [dx / length, dy / length];
500
+ }
501
+
502
+
503
+ /**
504
+ * Math helpers
505
+ */
506
+
507
+ // Basic vector utilities (only what's absolutely necessary)
508
+ function subtract(a, b) {
509
+ return [a[0] - b[0], a[1] - b[1]];
510
+ }
511
+
512
+ function addArrays(a, b) {
513
+ return [a[0] + b[0], a[1] + b[1]];
514
+ }
515
+
516
+
517
+ function mulItems(v, s) {
518
+ return [v[0] * s, v[1] * s];
519
+ }
520
+
521
+
522
+ function normalize(v) {
523
+ let len = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
524
+ return len === 0 ? [0, 0] : [v[0] / len, v[1] / len];
525
+ }
526
+
527
+ function dot(a, b) {
528
+ return a[0] * b[0] + a[1] * b[1];
529
+ }
530
+
531
+ function zeros_Xx2x2(x) {
532
+ let zs = [];
533
+ while (x--) {
534
+ zs.push([0, 0]);
535
+ }
536
+ return zs;
537
+ }
538
+
539
+
540
+ // First derivative (tangent vector)
541
+ function bezierQprime(bez, u, second = false) {
542
+ let p0 = bez[0], cp1 = bez[1], cp2 = bez[2], p1 = bez[3];
543
+ let t = u;
544
+ let mt = 1 - t;
545
+ let mt2 = mt * mt;
546
+ let t2 = t * t;
547
+ let dx, dy;
548
+
549
+ if (second) {
550
+ dx = 6 * mt * (cp2[0] - 2 * cp1[0] + p0[0]) +
551
+ 6 * t * (p1[0] - 2 * cp2[0] + cp1[0]);
552
+
553
+ dy = 6 * mt * (cp2[1] - 2 * cp1[1] + p0[1]) +
554
+ 6 * t * (p1[1] - 2 * cp2[1] + cp1[1]);
555
+
556
+ } else {
557
+ dx = 3 * mt2 * (cp1[0] - p0[0]) +
558
+ 6 * mt * t * (cp2[0] - cp1[0]) +
559
+ 3 * t2 * (p1[0] - cp2[0]);
560
+
561
+ dy = 3 * mt2 * (cp1[1] - p0[1]) +
562
+ 6 * mt * t * (cp2[1] - cp1[1]) +
563
+ 3 * t2 * (p1[1] - cp2[1]);
564
+ }
565
+
566
+ return [dx, dy];
567
+ }
568
+
569
+
570
+
@@ -0,0 +1,146 @@
1
+ import { reducePoints } from "./svgii/geometry";
2
+ import { getPolyBBox } from "./svgii/geometry_bbox";
3
+ import { renderPoint } from "./svgii/visualize";
4
+
5
+
6
+ export function simplifyRDP(pts, {
7
+ quality = 0.9,
8
+ width = 0,
9
+ height = 0,
10
+ absolute = false,
11
+ // use square or manhattan distances
12
+ manhattan = false,
13
+ exclude = []
14
+ } = {}) {
15
+
16
+
17
+ //console.log(exclude);
18
+
19
+ let excludeSet = new Set(exclude);
20
+
21
+
22
+ /**
23
+ * switch between absolute or
24
+ * quality based relative thresholds
25
+ */
26
+ if (typeof quality === 'string') {
27
+ absolute = true;
28
+ quality = parseFloat(quality);
29
+ }
30
+
31
+ if (pts.length < 4 || (!absolute && quality) >= 1) return pts;
32
+
33
+ // convert quality to squaredistance or manhattan tolerance
34
+ let tolerance = quality;
35
+
36
+ if (!absolute) {
37
+
38
+ tolerance = 1 - quality;
39
+
40
+ // adjust for higher qualities
41
+ if (quality > 0.5) tolerance /= 2;
42
+
43
+ /**
44
+ * approximate dimensions
45
+ * adjust tolerance for
46
+ * very small polygons e.g geodata
47
+ */
48
+ if (!width && !height) {
49
+ let polyS = reducePoints(pts, 12);
50
+ ({ width, height } = getPolyBBox(polyS));
51
+ }
52
+
53
+ if (!manhattan) {
54
+ // average side lengths
55
+ let dimAvg = (width + height) / 2;
56
+ let scale = dimAvg / 100;
57
+ tolerance = (tolerance * (scale)) ** 2
58
+ } else {
59
+ // use manhattan
60
+ tolerance = (width + height) * 0.003 * (1 - quality)
61
+ }
62
+
63
+ }
64
+
65
+
66
+ const segmentDistance = (p, p1, p2, manhattan = false) => {
67
+ let x = p1.x, y = p1.y;
68
+ let dx = p2.x - x, dy = p2.y - y;
69
+
70
+ if (dx !== 0 || dy !== 0) {
71
+ let t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
72
+
73
+ if (t > 1) {
74
+ x = p2.x;
75
+ y = p2.y;
76
+ } else if (t > 0) {
77
+ x += dx * t;
78
+ y += dy * t;
79
+ }
80
+ }
81
+
82
+ // use manhattan or square distance
83
+ return !manhattan ? (p.x - x) ** 2 + (p.y - y) ** 2 : Math.abs(p.x - x) + Math.abs(p.y - y);
84
+ };
85
+
86
+
87
+ // start collecting ptsSmp polyline
88
+ let ptsSmp = [pts[0]];
89
+
90
+ // create processing stack
91
+ let stack = [];
92
+ stack.push([0, pts.length - 1]);
93
+
94
+ let maxDist = tolerance;
95
+ let currentDist = 0;
96
+ let index = -1;
97
+ let lenExclude = exclude.length;
98
+
99
+ while (stack.length > 0) {
100
+ let [first, last] = stack.pop();
101
+ maxDist = tolerance;
102
+ index = -1;
103
+
104
+ // Check if there is an excluded point inside this segment
105
+ let forcedIndex = -1;
106
+ for (let i = first + 1; i < last; i++) {
107
+ if (excludeSet.has(i)) {
108
+ forcedIndex = i;
109
+ break;
110
+ }
111
+ }
112
+
113
+ if (forcedIndex !== -1) {
114
+ // Force split at excluded point
115
+ stack.push([forcedIndex, last], [first, forcedIndex]);
116
+ continue;
117
+ }
118
+
119
+ // Normal RDP distance check
120
+ for (let i = first + 1; i < last; i++) {
121
+ currentDist = segmentDistance(
122
+ pts[i],
123
+ pts[first],
124
+ pts[last],
125
+ manhattan
126
+ );
127
+
128
+ if (currentDist > maxDist) {
129
+ index = i;
130
+ maxDist = currentDist;
131
+ }
132
+ }
133
+
134
+ if (maxDist > tolerance) {
135
+ stack.push([index, last], [first, index]);
136
+ } else {
137
+ ptsSmp.push(pts[last]);
138
+ }
139
+ }
140
+
141
+
142
+
143
+ return ptsSmp;
144
+ }
145
+
146
+