pdf-oxide 0.3.24
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.
- package/README.md +218 -0
- package/binding.gyp +35 -0
- package/package.json +78 -0
- package/src/builders/annotation-builder.ts +367 -0
- package/src/builders/conversion-options-builder.ts +257 -0
- package/src/builders/index.ts +12 -0
- package/src/builders/metadata-builder.ts +317 -0
- package/src/builders/pdf-builder.ts +386 -0
- package/src/builders/search-options-builder.ts +151 -0
- package/src/document-editor-manager.ts +318 -0
- package/src/errors.ts +1629 -0
- package/src/form-field-manager.ts +666 -0
- package/src/hybrid-ml-manager.ts +283 -0
- package/src/index.ts +453 -0
- package/src/managers/accessibility-manager.ts +338 -0
- package/src/managers/annotation-manager.ts +439 -0
- package/src/managers/barcode-manager.ts +235 -0
- package/src/managers/batch-manager.ts +533 -0
- package/src/managers/cache-manager.ts +486 -0
- package/src/managers/compliance-manager.ts +375 -0
- package/src/managers/content-manager.ts +339 -0
- package/src/managers/document-utility-manager.ts +922 -0
- package/src/managers/dom-pdf-creator.ts +365 -0
- package/src/managers/editing-manager.ts +514 -0
- package/src/managers/enterprise-manager.ts +478 -0
- package/src/managers/extended-managers.ts +437 -0
- package/src/managers/extraction-manager.ts +583 -0
- package/src/managers/final-utilities.ts +429 -0
- package/src/managers/hybrid-ml-advanced.ts +479 -0
- package/src/managers/index.ts +239 -0
- package/src/managers/layer-manager.ts +500 -0
- package/src/managers/metadata-manager.ts +303 -0
- package/src/managers/ocr-manager.ts +756 -0
- package/src/managers/optimization-manager.ts +262 -0
- package/src/managers/outline-manager.ts +196 -0
- package/src/managers/page-manager.ts +289 -0
- package/src/managers/pattern-detection.ts +440 -0
- package/src/managers/rendering-manager.ts +863 -0
- package/src/managers/search-manager.ts +385 -0
- package/src/managers/security-manager.ts +345 -0
- package/src/managers/signature-manager.ts +1664 -0
- package/src/managers/streams.ts +618 -0
- package/src/managers/xfa-manager.ts +500 -0
- package/src/pdf-creator-manager.ts +494 -0
- package/src/properties.ts +522 -0
- package/src/result-accessors-manager.ts +867 -0
- package/src/tests/advanced-features.test.ts +414 -0
- package/src/tests/advanced.test.ts +266 -0
- package/src/tests/extended-managers.test.ts +316 -0
- package/src/tests/final-utilities.test.ts +455 -0
- package/src/tests/foundation.test.ts +315 -0
- package/src/tests/high-demand.test.ts +257 -0
- package/src/tests/specialized.test.ts +97 -0
- package/src/thumbnail-manager.ts +272 -0
- package/src/types/common.ts +142 -0
- package/src/types/document-types.ts +457 -0
- package/src/types/index.ts +6 -0
- package/src/types/manager-types.ts +284 -0
- package/src/types/native-bindings.ts +517 -0
- package/src/workers/index.ts +7 -0
- package/src/workers/pool.ts +274 -0
- package/src/workers/worker.ts +131 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manager for PDF layers (Optional Content Groups - OCG)
|
|
3
|
+
*
|
|
4
|
+
* Provides methods to manage and interact with PDF layers which are used
|
|
5
|
+
* for optional content groups in PDF documents.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { LayerManager } from 'pdf_oxide';
|
|
10
|
+
*
|
|
11
|
+
* const doc = PdfDocument.open('document.pdf');
|
|
12
|
+
* const layerManager = new LayerManager(doc);
|
|
13
|
+
*
|
|
14
|
+
* // Check if document has layers
|
|
15
|
+
* if (layerManager.hasLayers()) {
|
|
16
|
+
* const layers = layerManager.getLayers();
|
|
17
|
+
* console.log(`Document has ${layers.length} layers`);
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface Layer {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
visible: boolean;
|
|
26
|
+
index: number;
|
|
27
|
+
parentId: string | null;
|
|
28
|
+
printable: boolean;
|
|
29
|
+
export: boolean;
|
|
30
|
+
description?: string;
|
|
31
|
+
dependsOn?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LayerNode extends Layer {
|
|
35
|
+
children: LayerNode[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LayerHierarchy {
|
|
39
|
+
root: LayerNode[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LayerStatistics {
|
|
43
|
+
count: number;
|
|
44
|
+
rootCount: number;
|
|
45
|
+
maxDepth: number;
|
|
46
|
+
visible: number;
|
|
47
|
+
hidden: number;
|
|
48
|
+
printable: number;
|
|
49
|
+
exportable: number;
|
|
50
|
+
hasConflicts: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LayerValidation {
|
|
54
|
+
isValid: boolean;
|
|
55
|
+
issues: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class LayerManager {
|
|
59
|
+
private _document: any;
|
|
60
|
+
private _layerCache: Layer[] | null;
|
|
61
|
+
private _hierarchyCache: LayerHierarchy | null;
|
|
62
|
+
private _statisticsCache: LayerStatistics | null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a new LayerManager for the given document
|
|
66
|
+
* @param document - The PDF document
|
|
67
|
+
* @throws Error if document is null or undefined
|
|
68
|
+
*/
|
|
69
|
+
constructor(document: any) {
|
|
70
|
+
if (!document) {
|
|
71
|
+
throw new Error('Document is required');
|
|
72
|
+
}
|
|
73
|
+
this._document = document;
|
|
74
|
+
// Performance optimization: cache layer data
|
|
75
|
+
this._layerCache = null;
|
|
76
|
+
this._hierarchyCache = null;
|
|
77
|
+
this._statisticsCache = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clears the layer cache
|
|
82
|
+
* Useful when document content might have changed
|
|
83
|
+
*/
|
|
84
|
+
clearCache(): void {
|
|
85
|
+
this._layerCache = null;
|
|
86
|
+
this._hierarchyCache = null;
|
|
87
|
+
this._statisticsCache = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Checks if document has layers
|
|
92
|
+
* @returns True if document contains layers
|
|
93
|
+
*/
|
|
94
|
+
hasLayers(): boolean {
|
|
95
|
+
try {
|
|
96
|
+
return this.getLayerCount() > 0;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Gets number of layers in document
|
|
104
|
+
* @returns Number of layers
|
|
105
|
+
*/
|
|
106
|
+
getLayerCount(): number {
|
|
107
|
+
const layers = this.getLayers();
|
|
108
|
+
return layers.length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gets all layers in document
|
|
113
|
+
* @returns Array of layer objects with id, name, visible, etc.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* const layers = manager.getLayers();
|
|
118
|
+
* layers.forEach(layer => {
|
|
119
|
+
* console.log(`Layer: ${layer.name} (visible: ${layer.visible})`);
|
|
120
|
+
* });
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
getLayers(): Layer[] {
|
|
124
|
+
// Performance optimization: cache layer data
|
|
125
|
+
if (this._layerCache !== null) {
|
|
126
|
+
return this._layerCache;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const rawLayers = this._document.getLayers();
|
|
131
|
+
// Convert native LayerInfo to the expected JS format
|
|
132
|
+
const layers: Layer[] = rawLayers.map((layer: any) => ({
|
|
133
|
+
id: layer.id,
|
|
134
|
+
name: layer.name,
|
|
135
|
+
visible: layer.visible,
|
|
136
|
+
index: layer.index,
|
|
137
|
+
parentId: null, // OCGs don't have hierarchy in PDF spec
|
|
138
|
+
printable: true, // Default assumption
|
|
139
|
+
export: true, // Default assumption
|
|
140
|
+
}));
|
|
141
|
+
this._layerCache = layers;
|
|
142
|
+
return layers;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Gets layer by name
|
|
150
|
+
* @param name - Layer name to find
|
|
151
|
+
* @returns Layer object or null if not found
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* const layer = manager.getLayerByName('Background');
|
|
156
|
+
* if (layer) {
|
|
157
|
+
* console.log(`Found layer: ${layer.name}`);
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
getLayerByName(name: string): Layer | null {
|
|
162
|
+
if (!name || typeof name !== 'string') {
|
|
163
|
+
throw new Error('Layer name must be a non-empty string');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const layers = this.getLayers();
|
|
167
|
+
return layers.find(layer => layer.name === name) || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Gets layer by ID
|
|
172
|
+
* @param id - Layer ID to find
|
|
173
|
+
* @returns Layer object or null if not found
|
|
174
|
+
*/
|
|
175
|
+
getLayerById(id: string): Layer | null {
|
|
176
|
+
if (!id || typeof id !== 'string') {
|
|
177
|
+
throw new Error('Layer ID must be a non-empty string');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const layers = this.getLayers();
|
|
181
|
+
return layers.find(layer => layer.id === id) || null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Gets root-level layers (not nested under other layers)
|
|
186
|
+
* @returns Array of root-level layers
|
|
187
|
+
*/
|
|
188
|
+
getRootLayers(): Layer[] {
|
|
189
|
+
const layers = this.getLayers();
|
|
190
|
+
return layers.filter(layer => !layer.parentId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Gets the full layer hierarchy as a tree structure
|
|
195
|
+
* @returns Layer hierarchy tree
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* const hierarchy = manager.getLayerHierarchy();
|
|
200
|
+
* // { root: [{ name: 'Layer1', children: [...] }, ...] }
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
getLayerHierarchy(): LayerHierarchy {
|
|
204
|
+
// Performance optimization: cache hierarchy
|
|
205
|
+
if (this._hierarchyCache !== null) {
|
|
206
|
+
return this._hierarchyCache;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const layers = this.getLayers();
|
|
210
|
+
const hierarchy: LayerHierarchy = { root: [] };
|
|
211
|
+
|
|
212
|
+
// Build parent-child relationships
|
|
213
|
+
const layerMap = new Map<string, LayerNode>(
|
|
214
|
+
layers.map((l) => [l.id, { ...l, children: [] } as LayerNode])
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
for (const layer of layerMap.values()) {
|
|
218
|
+
if (layer.parentId) {
|
|
219
|
+
const parent = layerMap.get(layer.parentId);
|
|
220
|
+
if (parent) {
|
|
221
|
+
parent.children.push(layer);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
hierarchy.root.push(layer);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this._hierarchyCache = hierarchy;
|
|
229
|
+
return hierarchy;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Gets child layers of a parent layer
|
|
234
|
+
* @param parentId - Parent layer ID
|
|
235
|
+
* @returns Array of child layers
|
|
236
|
+
*/
|
|
237
|
+
getChildLayers(parentId: string): Layer[] {
|
|
238
|
+
if (!parentId || typeof parentId !== 'string') {
|
|
239
|
+
throw new Error('Parent layer ID must be a non-empty string');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const layers = this.getLayers();
|
|
243
|
+
return layers.filter(layer => layer.parentId === parentId);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Gets parent layer of a layer
|
|
248
|
+
* @param layerId - Layer ID
|
|
249
|
+
* @returns Parent layer object or null if no parent
|
|
250
|
+
*/
|
|
251
|
+
getParentLayer(layerId: string): Layer | null {
|
|
252
|
+
if (!layerId || typeof layerId !== 'string') {
|
|
253
|
+
throw new Error('Layer ID must be a non-empty string');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const layer = this.getLayerById(layerId);
|
|
257
|
+
if (!layer || !layer.parentId) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return this.getLayerById(layer.parentId);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Checks if a layer is visible
|
|
266
|
+
* @param layerId - Layer ID
|
|
267
|
+
* @returns True if layer is visible
|
|
268
|
+
*/
|
|
269
|
+
isLayerVisible(layerId: string): boolean {
|
|
270
|
+
const layer = this.getLayerById(layerId);
|
|
271
|
+
return layer ? layer.visible !== false : false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Gets visibility chain from root to layer
|
|
276
|
+
* Shows visibility state of all parent layers
|
|
277
|
+
* @param layerId - Layer ID
|
|
278
|
+
* @returns Array of layers from root to target
|
|
279
|
+
*/
|
|
280
|
+
getVisibilityChain(layerId: string): Layer[] {
|
|
281
|
+
const chain: Layer[] = [];
|
|
282
|
+
let current = this.getLayerById(layerId);
|
|
283
|
+
|
|
284
|
+
while (current) {
|
|
285
|
+
chain.unshift(current);
|
|
286
|
+
current = current.parentId ? this.getLayerById(current.parentId) : null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return chain;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Gets layer usage information
|
|
294
|
+
* @returns Layer usage { view, print, export }
|
|
295
|
+
*/
|
|
296
|
+
getLayerUsages(): Record<string, number> {
|
|
297
|
+
const layers = this.getLayers();
|
|
298
|
+
return {
|
|
299
|
+
view: layers.filter(l => l.printable === false).length,
|
|
300
|
+
print: layers.filter(l => l.printable === true).length,
|
|
301
|
+
export: layers.filter(l => l.export !== false).length,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Gets statistics about layers
|
|
307
|
+
* @returns Layer statistics
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```typescript
|
|
311
|
+
* const stats = manager.getLayerStatistics();
|
|
312
|
+
* console.log(`Total layers: ${stats.count}`);
|
|
313
|
+
* console.log(`Max depth: ${stats.maxDepth}`);
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
getLayerStatistics(): LayerStatistics {
|
|
317
|
+
// Performance optimization: cache statistics
|
|
318
|
+
if (this._statisticsCache !== null) {
|
|
319
|
+
return this._statisticsCache;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const layers = this.getLayers();
|
|
323
|
+
const hierarchy = this.getLayerHierarchy();
|
|
324
|
+
|
|
325
|
+
// Calculate max depth
|
|
326
|
+
const calculateDepth = (node: any, depth: number = 0): number => {
|
|
327
|
+
if (!node.children || node.children.length === 0) {
|
|
328
|
+
return depth;
|
|
329
|
+
}
|
|
330
|
+
return Math.max(...node.children.map((child: any) => calculateDepth(child, depth + 1)));
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
let maxDepth = 0;
|
|
334
|
+
for (const rootLayer of hierarchy.root) {
|
|
335
|
+
maxDepth = Math.max(maxDepth, calculateDepth(rootLayer));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const stats: LayerStatistics = {
|
|
339
|
+
count: layers.length,
|
|
340
|
+
rootCount: hierarchy.root.length,
|
|
341
|
+
maxDepth,
|
|
342
|
+
visible: layers.filter(l => l.visible !== false).length,
|
|
343
|
+
hidden: layers.filter(l => l.visible === false).length,
|
|
344
|
+
printable: layers.filter(l => l.printable !== false).length,
|
|
345
|
+
exportable: layers.filter(l => l.export !== false).length,
|
|
346
|
+
hasConflicts: this._detectLayerConflicts().length > 0,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
this._statisticsCache = stats;
|
|
350
|
+
return stats;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Gets layer dependencies
|
|
355
|
+
* @returns Layer dependencies map
|
|
356
|
+
* @private
|
|
357
|
+
*/
|
|
358
|
+
private getLayerDependencies(): Record<string, any> {
|
|
359
|
+
const layers = this.getLayers();
|
|
360
|
+
const dependencies: Record<string, any> = {};
|
|
361
|
+
|
|
362
|
+
layers.forEach(layer => {
|
|
363
|
+
dependencies[layer.id] = {
|
|
364
|
+
dependsOn: layer.dependsOn || [],
|
|
365
|
+
dependents: [],
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Build reverse dependencies
|
|
370
|
+
Object.entries(dependencies).forEach(([layerId, deps]) => {
|
|
371
|
+
deps.dependsOn.forEach((depId: string) => {
|
|
372
|
+
if (dependencies[depId]) {
|
|
373
|
+
dependencies[depId].dependents.push(layerId);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
return dependencies;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Finds layers by pattern
|
|
383
|
+
* @param pattern - Pattern to match
|
|
384
|
+
* @returns Matching layers
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```typescript
|
|
388
|
+
* const backgroundLayers = manager.findLayersByPattern(/background/i);
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
391
|
+
findLayersByPattern(pattern: RegExp | string): Layer[] {
|
|
392
|
+
if (!pattern) {
|
|
393
|
+
throw new Error('Pattern must be provided');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'i');
|
|
397
|
+
const layers = this.getLayers();
|
|
398
|
+
|
|
399
|
+
return layers.filter(layer =>
|
|
400
|
+
regex.test(layer.name) ||
|
|
401
|
+
(layer.description && regex.test(layer.description))
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Validates layer state for conflicts and issues
|
|
407
|
+
* @returns Validation result { isValid, issues }
|
|
408
|
+
*/
|
|
409
|
+
validateLayerState(): LayerValidation {
|
|
410
|
+
const issues: string[] = [];
|
|
411
|
+
const conflicts = this._detectLayerConflicts();
|
|
412
|
+
const cycles = this._detectLayerCycles();
|
|
413
|
+
|
|
414
|
+
if (conflicts.length > 0) {
|
|
415
|
+
issues.push(...conflicts.map(c => `Conflict: ${c}`));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (cycles.length > 0) {
|
|
419
|
+
issues.push(...cycles.map(c => `Cycle detected: ${c}`));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
isValid: issues.length === 0,
|
|
424
|
+
issues,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Detects layer conflicts
|
|
430
|
+
* @returns Array of conflict descriptions
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
private _detectLayerConflicts(): string[] {
|
|
434
|
+
const conflicts: string[] = [];
|
|
435
|
+
const layers = this.getLayers();
|
|
436
|
+
|
|
437
|
+
// Check for layers with same name
|
|
438
|
+
const nameMap = new Map<string, string>();
|
|
439
|
+
layers.forEach(layer => {
|
|
440
|
+
if (nameMap.has(layer.name)) {
|
|
441
|
+
conflicts.push(`Duplicate layer name: ${layer.name}`);
|
|
442
|
+
}
|
|
443
|
+
nameMap.set(layer.name, layer.id);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Check for orphaned layers
|
|
447
|
+
const parentIds = new Set(layers.map(l => l.parentId).filter(id => id));
|
|
448
|
+
const layerIds = new Set(layers.map(l => l.id));
|
|
449
|
+
|
|
450
|
+
parentIds.forEach(parentId => {
|
|
451
|
+
if (!layerIds.has(parentId as string)) {
|
|
452
|
+
conflicts.push(`Orphaned layer reference: ${parentId}`);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
return conflicts;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Detects cycles in layer hierarchy
|
|
461
|
+
* @returns Array of cycle descriptions
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
private _detectLayerCycles(): string[] {
|
|
465
|
+
const cycles: string[] = [];
|
|
466
|
+
const layers = this.getLayers();
|
|
467
|
+
const visited = new Set<string>();
|
|
468
|
+
const stack = new Set<string>();
|
|
469
|
+
|
|
470
|
+
const detectCycle = (layerId: string, path: string[] = []): void => {
|
|
471
|
+
if (stack.has(layerId)) {
|
|
472
|
+
cycles.push(`Cycle detected: ${path.join(' -> ')} -> ${layerId}`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (visited.has(layerId)) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
visited.add(layerId);
|
|
481
|
+
stack.add(layerId);
|
|
482
|
+
path.push(layerId);
|
|
483
|
+
|
|
484
|
+
const layer = this.getLayerById(layerId);
|
|
485
|
+
if (layer && layer.parentId) {
|
|
486
|
+
detectCycle(layer.parentId, [...path]);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
stack.delete(layerId);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
layers.forEach(layer => {
|
|
493
|
+
if (!visited.has(layer.id)) {
|
|
494
|
+
detectCycle(layer.id);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
return cycles;
|
|
499
|
+
}
|
|
500
|
+
}
|