landscape-widget 0.1.18 → 0.3.0-alpha

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.
@@ -0,0 +1,440 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
13
+ import Graph from 'graphology';
14
+ import classNames from 'classnames';
15
+ import { ControlsContainer, SigmaContainer, useLoadGraph, useRegisterEvents, useSigma, useCamera, } from '@react-sigma/core';
16
+ import forceAtlas2 from 'graphology-layout-forceatlas2';
17
+ import { Lasso } from './icons/Lasso';
18
+ import { Plus, Minus, Reset,
19
+ // FullScreenEnter,
20
+ // FullScreenExit,
21
+ ClearSelection, Invert, } from './icons';
22
+ import { defaultSettings, ForceAtlasAnimator } from './ForceAtlasAnimator';
23
+ import '@react-sigma/core/lib/react-sigma.min.css';
24
+ import './GraphLayout.scss';
25
+ export const LoadGraph = ({ data, nodeColors, nodeLabels, nodeSizes, animate, selectedNodes, setSelectedNodes, addSelectedNodes, setHoveredNode, }) => {
26
+ const sigma = useSigma();
27
+ const loadGraph = useLoadGraph();
28
+ const registerEvents = useRegisterEvents();
29
+ const animator = useRef(new ForceAtlasAnimator(sigma.getGraph(), { settings: defaultSettings }, { maxiters: 1500, miniters: 200, convergenceThreshold: 0.1 }));
30
+ useEffect(() => {
31
+ let graph;
32
+ graph = new Graph();
33
+ data.nodes.forEach((node) => {
34
+ graph.addNode(node.key, node.attributes);
35
+ });
36
+ data.edges.forEach((edge) => {
37
+ graph.addEdge(edge.source, edge.target);
38
+ });
39
+ // run a few steps of the layout for less initial motion when graph renders
40
+ const initialSettings = Object.assign(Object.assign({}, defaultSettings), forceAtlas2.inferSettings(graph));
41
+ forceAtlas2.assign(graph, { iterations: 100, settings: initialSettings });
42
+ loadGraph(graph);
43
+ animator.current.kill();
44
+ animator.current.params.settings = Object.assign(Object.assign({}, defaultSettings), forceAtlas2.inferSettings(graph));
45
+ animator.current.graph = sigma.getGraph();
46
+ if (animate) {
47
+ animator.current.start();
48
+ }
49
+ }, [data, loadGraph]);
50
+ useEffect(() => {
51
+ const graph = sigma.getGraph();
52
+ graph.updateEachNodeAttributes((key, attr) => {
53
+ const n = Number(key);
54
+ if (selectedNodes.some((node) => node === n)) {
55
+ return Object.assign(Object.assign({}, attr), { highlighted: true });
56
+ }
57
+ else {
58
+ return Object.assign(Object.assign({}, attr), { highlighted: false });
59
+ }
60
+ });
61
+ }, [selectedNodes]);
62
+ useEffect(() => {
63
+ const graph = sigma.getGraph();
64
+ if (!nodeColors)
65
+ return;
66
+ graph.updateEachNodeAttributes((key, attr) => {
67
+ const n = Number(key);
68
+ return Object.assign(Object.assign({}, attr), { color: nodeColors[n] });
69
+ });
70
+ }, [data, nodeColors]);
71
+ useEffect(() => {
72
+ const graph = sigma.getGraph();
73
+ if (!nodeLabels)
74
+ return;
75
+ graph.updateEachNodeAttributes((key, attr) => {
76
+ const n = Number(key);
77
+ if (nodeLabels[n]) {
78
+ return Object.assign(Object.assign({}, attr), { label: nodeLabels[n] });
79
+ }
80
+ else {
81
+ return Object.assign(Object.assign({}, attr), { label: undefined });
82
+ }
83
+ });
84
+ }, [data, nodeLabels]);
85
+ useEffect(() => {
86
+ const graph = sigma.getGraph();
87
+ if (!nodeSizes)
88
+ return;
89
+ graph.updateEachNodeAttributes((key, attr) => {
90
+ const n = Number(key);
91
+ return Object.assign(Object.assign({}, attr), { size: nodeSizes[n] });
92
+ });
93
+ }, [data, nodeSizes]);
94
+ useEffect(() => {
95
+ if (animate) {
96
+ animator.current.start();
97
+ }
98
+ else {
99
+ animator.current.stop();
100
+ }
101
+ }, [animate]);
102
+ const handleSelectionClear = useCallback(() => {
103
+ setSelectedNodes([]);
104
+ const graph = sigma.getGraph();
105
+ selectedNodes.forEach((node) => {
106
+ graph.setNodeAttribute(node, 'highlighted', false);
107
+ });
108
+ }, [setSelectedNodes, selectedNodes]);
109
+ const onNodeDoubleClick = (e, graph) => {
110
+ e.preventSigmaDefault();
111
+ const nodeValue = Number(e.node);
112
+ const isNodeSelectedAlready = selectedNodes.some((n) => n === nodeValue);
113
+ if (isNodeSelectedAlready) {
114
+ setSelectedNodes(selectedNodes.filter((n) => n !== nodeValue));
115
+ }
116
+ else {
117
+ addSelectedNodes([nodeValue]);
118
+ }
119
+ };
120
+ useEffect(() => {
121
+ let draggedNode;
122
+ let isDragging = false;
123
+ let isDraggedNodeSelected = false;
124
+ let previousPosition = null;
125
+ let lastScrollTime = null;
126
+ let currentAnimationTarget = null;
127
+ const graph = sigma.getGraph();
128
+ registerEvents({
129
+ enterNode: (e) => {
130
+ const nodeID = Number(e.node);
131
+ setHoveredNode(nodeID);
132
+ },
133
+ leaveNode: (e) => {
134
+ setHoveredNode(-1);
135
+ },
136
+ downNode: (e) => {
137
+ isDragging = true;
138
+ draggedNode = e.node;
139
+ const nodeValue = Number(e.node);
140
+ graph.setNodeAttribute(draggedNode, 'highlighted', true);
141
+ isDraggedNodeSelected = selectedNodes.some((n) => n === nodeValue);
142
+ if (isDraggedNodeSelected) {
143
+ selectedNodes.map((node) => graph.updateNodeAttribute(node, 'fixed', () => true));
144
+ }
145
+ else {
146
+ graph.setNodeAttribute(draggedNode, 'fixed', true);
147
+ }
148
+ previousPosition = sigma.viewportToGraph(e.event);
149
+ if (!animator.current.isRunning() && animate) {
150
+ animator.current.start();
151
+ }
152
+ animator.current.resetNIters();
153
+ },
154
+ mousemovebody: (e) => {
155
+ if (!isDragging || !draggedNode || !previousPosition)
156
+ return;
157
+ animator.current.resetNIters();
158
+ const pos = sigma.viewportToGraph(e);
159
+ const dx = pos.x - previousPosition.x;
160
+ const dy = pos.y - previousPosition.y;
161
+ previousPosition = pos;
162
+ if (isDraggedNodeSelected) {
163
+ selectedNodes.map((node) => graph.updateNodeAttributes(node, (attr) => (Object.assign(Object.assign({}, attr), { x: attr.x + dx, y: attr.y + dy }))));
164
+ }
165
+ else {
166
+ graph.updateNodeAttributes(draggedNode, (attr) => (Object.assign(Object.assign({}, attr), { x: attr.x + dx, y: attr.y + dy })));
167
+ }
168
+ // Prevent sigma to move camera:
169
+ e.preventSigmaDefault();
170
+ e.original.preventDefault();
171
+ e.original.stopPropagation();
172
+ },
173
+ mouseup: () => {
174
+ if (draggedNode) {
175
+ if (isDraggedNodeSelected) {
176
+ selectedNodes.map((node) => graph.removeNodeAttribute(node, 'fixed'));
177
+ }
178
+ else {
179
+ graph.removeNodeAttribute(draggedNode, 'fixed');
180
+ graph.removeNodeAttribute(draggedNode, 'highlighted');
181
+ }
182
+ previousPosition = null;
183
+ }
184
+ isDragging = false;
185
+ draggedNode = null;
186
+ isDraggedNodeSelected = false;
187
+ },
188
+ mousedown: () => {
189
+ if (!sigma.getCustomBBox())
190
+ sigma.setCustomBBox(sigma.getBBox());
191
+ },
192
+ doubleClickNode: (e) => onNodeDoubleClick(e, graph),
193
+ doubleClickStage: (e) => {
194
+ handleSelectionClear();
195
+ e.preventSigmaDefault();
196
+ },
197
+ wheel: (e) => {
198
+ const camera = sigma.getCamera();
199
+ const cameraState = camera.getState();
200
+ const delta = Math.max(-1.5, Math.min(1.5, e.delta));
201
+ const oldRatio = currentAnimationTarget !== null ? currentAnimationTarget : cameraState.ratio;
202
+ const newRatio = camera.getBoundedRatio(oldRatio * (1 - delta / 8));
203
+ const event = e.original;
204
+ const scrollTime = Date.now();
205
+ const timeDelta = lastScrollTime ? scrollTime - lastScrollTime : 100;
206
+ lastScrollTime = scrollTime;
207
+ if (event.deltaMode !== WheelEvent.DOM_DELTA_PIXEL) {
208
+ console.warn('WheelEvent.deltaMode is not DOM_DELTA_PIXEL. Zoom behavior may not be as expected.');
209
+ }
210
+ // TODO: animation if delta is large enough?
211
+ const newState = sigma.getViewportZoomedState(e, newRatio);
212
+ // trying to do the animation to smooth out bigger steps may not be worth it
213
+ if (false && Math.abs(delta) > 0.05 && timeDelta > 30) {
214
+ currentAnimationTarget = newRatio;
215
+ camera.animate(newState, {
216
+ easing: 'quadraticOut',
217
+ duration: timeDelta * 1.5,
218
+ });
219
+ }
220
+ else {
221
+ if (camera.isAnimated()) {
222
+ camera.animate(newState, { duration: 1 });
223
+ camera.setState(newState);
224
+ currentAnimationTarget = newRatio;
225
+ }
226
+ else {
227
+ currentAnimationTarget = null;
228
+ camera.setState(newState);
229
+ }
230
+ }
231
+ e.preventSigmaDefault(); // why doesn't this call work?
232
+ e.sigmaDefaultPrevented = true;
233
+ },
234
+ });
235
+ }, [registerEvents, setSelectedNodes, addSelectedNodes, selectedNodes, handleSelectionClear]);
236
+ return null;
237
+ };
238
+ export const SigmaLasso = ({ active, setSelectedNodes, addSelectedNodes }) => {
239
+ const sigma = useSigma();
240
+ const lassoCanvasRef = useRef(null);
241
+ const getLassoCanvas = useCallback(() => {
242
+ if (active)
243
+ return lassoCanvasRef.current;
244
+ return null;
245
+ }, [active]);
246
+ const isDrawing = useRef(false);
247
+ const drawnPoints = useRef([]);
248
+ const onDrawingStart = (event) => {
249
+ const lassoCanvas = getLassoCanvas();
250
+ if (!lassoCanvas)
251
+ return;
252
+ isDrawing.current = true;
253
+ drawnPoints.current = [];
254
+ const drawingRectangle = lassoCanvas.getBoundingClientRect();
255
+ drawnPoints.current.push({
256
+ x: event.clientX - drawingRectangle.left,
257
+ y: event.clientY - drawingRectangle.top,
258
+ });
259
+ event.stopPropagation();
260
+ };
261
+ const onDrawing = (event) => {
262
+ const lassoCanvas = getLassoCanvas();
263
+ if (!lassoCanvas)
264
+ return;
265
+ if (isDrawing.current) {
266
+ let x;
267
+ let y;
268
+ const drawingRectangle = lassoCanvas.getBoundingClientRect();
269
+ const drawingContext = lassoCanvas.getContext('2d');
270
+ switch (event.type) {
271
+ case 'touchmove':
272
+ x = event.touches[0].clientX;
273
+ y = event.touches[0].clientY;
274
+ break;
275
+ default:
276
+ x = event.clientX;
277
+ y = event.clientY;
278
+ break;
279
+ }
280
+ drawnPoints.current.push({
281
+ x: x - drawingRectangle.left,
282
+ y: y - drawingRectangle.top,
283
+ });
284
+ // Drawing styles
285
+ drawingContext.strokeStyle = 'black';
286
+ drawingContext.lineWidth = 2;
287
+ drawingContext.fillStyle = 'rgba(200, 200, 200, 0.25)';
288
+ drawingContext.lineJoin = 'round';
289
+ drawingContext.lineCap = 'round';
290
+ // Clear the canvas
291
+ drawingContext.clearRect(0, 0, drawingContext.canvas.width, drawingContext.canvas.height);
292
+ // Redraw the complete path for a smoother effect
293
+ // Even smoother with quadratic curves
294
+ let sourcePoint = drawnPoints.current[0];
295
+ let destinationPoint = drawnPoints.current[1];
296
+ const pointsLength = drawnPoints.current.length;
297
+ const getMiddlePointCoordinates = (firstPoint, secondPoint) => ({
298
+ x: firstPoint.x + (secondPoint.x - firstPoint.x) / 2,
299
+ y: firstPoint.y + (secondPoint.y - firstPoint.y) / 2,
300
+ });
301
+ drawingContext.beginPath();
302
+ drawingContext.moveTo(sourcePoint.x, sourcePoint.y);
303
+ for (let i = 1; i < pointsLength; i += 1) {
304
+ const middlePoint = getMiddlePointCoordinates(sourcePoint, destinationPoint);
305
+ drawingContext.quadraticCurveTo(sourcePoint.x, sourcePoint.y, middlePoint.x, middlePoint.y);
306
+ sourcePoint = drawnPoints.current[i];
307
+ destinationPoint = drawnPoints.current[i + 1];
308
+ }
309
+ drawingContext.lineTo(sourcePoint.x, sourcePoint.y);
310
+ drawingContext.stroke();
311
+ drawingContext.fill();
312
+ event.stopPropagation();
313
+ }
314
+ };
315
+ const onDrawingEnd = useCallback((event) => {
316
+ const lassoCanvas = getLassoCanvas();
317
+ const viewRectangle = sigma.viewRectangle();
318
+ const graph = sigma.getGraph();
319
+ if (!lassoCanvas)
320
+ return;
321
+ isDrawing.current = false;
322
+ // @ts-ignore - There are no other ways to get visible nodes
323
+ const quadtreeRectangle = sigma.quadtree.rectangle(viewRectangle.x1, 1 - viewRectangle.y1, viewRectangle.x2, 1 - viewRectangle.y2, viewRectangle.height);
324
+ const visibleNodes = Array.from(new Set(quadtreeRectangle));
325
+ const drawingContext = lassoCanvas.getContext('2d');
326
+ let newSelectedNodes = [];
327
+ visibleNodes.forEach((key) => {
328
+ const nodeValue = Number(key);
329
+ const { x, y } = graph.getNodeAttributes(nodeValue);
330
+ const coordinates = sigma.graphToViewport({ x, y });
331
+ if (drawingContext.isPointInPath(coordinates.x, coordinates.y)) {
332
+ newSelectedNodes.push(nodeValue);
333
+ }
334
+ });
335
+ if (event.shiftKey) {
336
+ addSelectedNodes(newSelectedNodes);
337
+ }
338
+ else {
339
+ setSelectedNodes(newSelectedNodes);
340
+ }
341
+ drawingContext.clearRect(0, 0, lassoCanvas.width, lassoCanvas.height);
342
+ drawnPoints.current = [];
343
+ event.stopPropagation();
344
+ }, [setSelectedNodes, addSelectedNodes, getLassoCanvas, sigma]);
345
+ const onLassoScroll = (e) => {
346
+ const mouseCaptor = sigma.getMouseCaptor();
347
+ mouseCaptor.handleWheel(e.nativeEvent);
348
+ };
349
+ if (!active)
350
+ return null;
351
+ return (React.createElement("canvas", { ref: lassoCanvasRef, width: sigma.getContainer().offsetWidth, height: sigma.getContainer().offsetHeight, style: { position: 'absolute', top: '0px', cursor: 'crosshair' }, onMouseDown: onDrawingStart, onMouseMove: onDrawing, onMouseUp: onDrawingEnd, onWheel: onLassoScroll }));
352
+ };
353
+ export const GraphContainer = ({ data, nodeColors, nodeLabels, nodeSizes, animate, selectedNodes, setSelectedNodes, addSelectedNodes, setHoveredNode,
354
+ // isFullScreen,
355
+ // toggleFullScreen,
356
+ }) => {
357
+ const [isLassoActive, setIsLassoActive] = useState(false);
358
+ const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 });
359
+ const sigma = useSigma();
360
+ const onClearSelection = useCallback(() => {
361
+ setSelectedNodes([]);
362
+ }, [setSelectedNodes]);
363
+ const onInvertSelection = useCallback(() => {
364
+ const graph = sigma.getGraph();
365
+ let newSelectedNodes = [];
366
+ graph.forEachNode((node, attr) => {
367
+ const nodeID = Number(node);
368
+ if (!selectedNodes.some((n) => n === nodeID)) {
369
+ newSelectedNodes.push(nodeID);
370
+ }
371
+ });
372
+ setSelectedNodes(newSelectedNodes);
373
+ }, [selectedNodes, setSelectedNodes]);
374
+ // useEffect(() => {
375
+ // sigma.refresh();
376
+ // }, [sigma, isFullScreen]);
377
+ return (React.createElement(React.Fragment, null,
378
+ React.createElement(LoadGraph, { data: data, nodeColors: nodeColors, nodeLabels: nodeLabels, nodeSizes: nodeSizes, animate: animate, selectedNodes: selectedNodes, setSelectedNodes: setSelectedNodes, addSelectedNodes: addSelectedNodes, setHoveredNode: setHoveredNode }),
379
+ React.createElement(SigmaLasso, { active: isLassoActive, setSelectedNodes: setSelectedNodes, addSelectedNodes: addSelectedNodes }),
380
+ React.createElement(ControlsContainer, { position: 'bottom-right' },
381
+ React.createElement("div", { className: 'react-sigma-control' },
382
+ React.createElement("button", { type: 'button', className: 'zoom-in', onClick: () => zoomIn() },
383
+ React.createElement(Plus, { width: 20, height: 20 }))),
384
+ React.createElement("div", { className: 'react-sigma-control' },
385
+ React.createElement("button", { type: 'button', className: 'zoom-out', onClick: () => zoomOut() },
386
+ React.createElement(Minus, { width: 20, height: 20 }))),
387
+ React.createElement("div", { className: 'react-sigma-control' },
388
+ React.createElement("button", { type: 'button', className: 'reset-scale', onClick: () => reset() },
389
+ React.createElement(Reset, { width: 20, height: 20 }))),
390
+ React.createElement("div", { className: 'react-sigma-control' },
391
+ React.createElement("button", { type: 'button', className: classNames('lasso-selection', {
392
+ active: isLassoActive,
393
+ }), onClick: () => setIsLassoActive((prevState) => !prevState) },
394
+ React.createElement(Lasso, { width: 20, height: 20 }))),
395
+ React.createElement("div", { className: 'react-sigma-control' },
396
+ React.createElement("button", { type: 'button', disabled: selectedNodes.length === 0, className: 'clear-selection', onClick: onClearSelection },
397
+ React.createElement(ClearSelection, { width: 20, height: 20 }))),
398
+ React.createElement("div", { className: 'react-sigma-control' },
399
+ React.createElement("button", { type: 'button', className: 'invert-selection', onClick: onInvertSelection },
400
+ React.createElement(Invert, { width: 20, height: 20 }))))));
401
+ };
402
+ export const GraphLayout = (_a) => {
403
+ // const [isFullScreen, setFullScreen] = useState(false);
404
+ var { data, setSelectedNodes, addSelectedNodes, setHoveredNode, selectedNodes } = _a, props = __rest(_a, ["data", "setSelectedNodes", "addSelectedNodes", "setHoveredNode", "selectedNodes"]);
405
+ // const toggleFullScreen = useCallback(() => setFullScreen((prevState) => !prevState), []);
406
+ // const fullScreenStyle = useMemo<Partial<CSSProperties>>(
407
+ // () =>
408
+ // isFullScreen
409
+ // ? {
410
+ // position: 'fixed',
411
+ // top: 0,
412
+ // left: 0,
413
+ // zIndex: 1400,
414
+ // }
415
+ // : {},
416
+ // [isFullScreen]
417
+ // );
418
+ // const handleEscKey = useCallback(
419
+ // (event: KeyboardEvent) => {
420
+ // if (event.key === 'Escape' && isFullScreen) {
421
+ // toggleFullScreen();
422
+ // }
423
+ // },
424
+ // [isFullScreen, toggleFullScreen]
425
+ // );
426
+ // useEffect(() => {
427
+ // if (isFullScreen) {
428
+ // document.addEventListener('keyup', handleEscKey);
429
+ // }
430
+ // return () => {
431
+ // if (isFullScreen) {
432
+ // document.removeEventListener('keyup', handleEscKey);
433
+ // }
434
+ // };
435
+ // }, [isFullScreen, handleEscKey]);
436
+ // if (!data) return null;
437
+ return (React.createElement(SigmaContainer, { id: 'graph-container' },
438
+ React.createElement(GraphContainer, Object.assign({}, props, { data: data, selectedNodes: selectedNodes, addSelectedNodes: addSelectedNodes, setSelectedNodes: setSelectedNodes, setHoveredNode: setHoveredNode }))));
439
+ };
440
+ //# sourceMappingURL=GraphLayout.js.map
@@ -0,0 +1,6 @@
1
+ export var GraphTypes;
2
+ (function (GraphTypes) {
3
+ GraphTypes["Data"] = "dataLandscape";
4
+ GraphTypes["Feature"] = "featureLandscape";
5
+ })(GraphTypes || (GraphTypes = {}));
6
+ //# sourceMappingURL=GraphLayout.types.js.map
@@ -0,0 +1,31 @@
1
+ import { createEdgeWeightGetter } from 'graphology-utils/getters';
2
+ import isGraph from 'graphology-utils/is-graph';
3
+ import { connectedComponents } from 'graphology-components';
4
+ import { explicitComponentLayout } from './explicitComponentLayout';
5
+ import { iterateNoIntercomponentRepel } from './IterateNoIntercomponentRepel';
6
+ // import helpers from 'graphology-layout-forceatlas2/helpers'
7
+ // avoid TS errors from import statements when types are not provided.
8
+ // tried using declaration files but it didn't work
9
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
10
+ const DEFAULT_SETTINGS = require('graphology-layout-forceatlas2/defaults');
11
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
12
+ const helpers = require('graphology-layout-forceatlas2/helpers');
13
+ export const getInitialLayout = (graph, params, componentsIters) => {
14
+ if (!isGraph(graph))
15
+ throw new Error('graphology-layout-forceatlas2: the given graph is not a valid graphology instance.');
16
+ const { iterations: layoutIters } = params;
17
+ const connComponents = connectedComponents(graph);
18
+ const getEdgeWeight = createEdgeWeightGetter(
19
+ // @ts-ignore
20
+ 'getEdgeWeight' in params ? params.getEdgeWeight : 'weight').fromEntry;
21
+ const outputReducer = typeof params.outputReducer === 'function' ? params.outputReducer : null;
22
+ // Validating settings
23
+ const settings = helpers.assign({}, DEFAULT_SETTINGS, params.settings);
24
+ const matrices = helpers.graphToByteArrays(graph, getEdgeWeight);
25
+ for (let i = 0; i < layoutIters; i++) {
26
+ iterateNoIntercomponentRepel(settings, matrices.nodes, matrices.edges, connComponents);
27
+ }
28
+ explicitComponentLayout(matrices.nodes, connComponents, componentsIters);
29
+ helpers.assignLayoutChanges(graph, matrices.nodes, outputReducer);
30
+ };
31
+ //# sourceMappingURL=InitialLayoutWorker.js.map