@statelyai/layout 0.1.3 → 0.3.0

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 (85) hide show
  1. package/NOTICE.md +43 -0
  2. package/README.md +153 -6
  3. package/dist/elkjs/index.cjs +9 -0
  4. package/dist/elkjs/index.d.cts +155 -0
  5. package/dist/elkjs/index.d.mts +51 -39
  6. package/dist/elkjs/index.mjs +3 -1484
  7. package/dist/elkjs/main.cjs +17 -0
  8. package/dist/elkjs/main.d.cts +10 -0
  9. package/dist/elkjs/main.d.mts +11 -0
  10. package/dist/elkjs/main.mjs +17 -0
  11. package/dist/elkjs/worker-api.cjs +3 -0
  12. package/dist/elkjs/worker-api.d.cts +3 -0
  13. package/dist/elkjs/worker-api.d.mts +3 -0
  14. package/dist/elkjs/worker-api.mjs +3 -0
  15. package/dist/elkjs/worker.cjs +86 -0
  16. package/dist/elkjs/worker.d.cts +17 -0
  17. package/dist/elkjs/worker.d.mts +17 -0
  18. package/dist/elkjs/worker.mjs +86 -0
  19. package/dist/elkjs-DaHMISnQ.mjs +1677 -0
  20. package/dist/elkjs-dMghiiWW.cjs +1706 -0
  21. package/dist/index-BEA_pzeT.d.cts +72 -0
  22. package/dist/{index-8xYkohbz.d.mts → index-D9dhxrRl.d.cts} +111 -5
  23. package/dist/index-DNTLRsDl.d.mts +72 -0
  24. package/dist/index-DNixtE3h.d.mts +1318 -0
  25. package/dist/index.cjs +215 -0
  26. package/dist/index.d.cts +78 -0
  27. package/dist/index.d.mts +4 -3
  28. package/dist/index.mjs +25 -25
  29. package/dist/layered/index.cjs +38 -0
  30. package/dist/layered/index.d.cts +3 -0
  31. package/dist/layered/index.d.mts +3 -2
  32. package/dist/layered/index.mjs +1 -1
  33. package/dist/layered-B2buJqMf.cjs +12009 -0
  34. package/dist/{layered-CoeH9LRO.mjs → layered-DGFTeQSl.mjs} +2323 -149
  35. package/dist/{spore-BtTSZbb6.mjs → layout-engine-Cs5tatA6.mjs} +159 -5
  36. package/dist/layout-engine-Dn1PiFwP.cjs +712 -0
  37. package/dist/public-types-9NCPoqbp.d.mts +109 -0
  38. package/dist/public-types-BcRZoJE3.d.cts +109 -0
  39. package/dist/routing/index.cjs +28 -0
  40. package/dist/routing/index.d.cts +3 -0
  41. package/dist/routing/index.d.mts +3 -0
  42. package/dist/routing/index.mjs +3 -0
  43. package/dist/routing-A2hrK9MP.mjs +2396 -0
  44. package/dist/routing-ujEP7Qh6.cjs +2606 -0
  45. package/dist/types-CT2akdK0.d.mts +133 -0
  46. package/dist/types-uxPlGRof.d.cts +133 -0
  47. package/dist/worker-client-BNjwT1AJ.mjs +92 -0
  48. package/dist/worker-client-BwGGRD5y.d.mts +19 -0
  49. package/dist/worker-client-D4oEvhAl.cjs +98 -0
  50. package/dist/worker-client-D4zNyPEI.d.cts +19 -0
  51. package/package.json +45 -10
  52. package/src/authoring/coordinates.ts +114 -0
  53. package/src/authoring/partial.ts +584 -0
  54. package/src/authoring/routing.ts +211 -0
  55. package/src/box.ts +22 -0
  56. package/src/constraints.ts +222 -0
  57. package/src/elkjs/index.ts +104 -39
  58. package/src/elkjs/main.ts +19 -0
  59. package/src/elkjs/public-types.ts +125 -0
  60. package/src/elkjs/routes.ts +147 -0
  61. package/src/elkjs/worker-api.ts +29 -0
  62. package/src/elkjs/worker-client.ts +138 -0
  63. package/src/elkjs/worker.ts +117 -0
  64. package/src/index.ts +19 -0
  65. package/src/internal/elkjs-compatibility.ts +17 -0
  66. package/src/internal/layout-engine.ts +126 -0
  67. package/src/layered/compound.ts +417 -0
  68. package/src/layered/index.ts +58 -105
  69. package/src/layered/types.ts +21 -0
  70. package/src/layout.ts +35 -28
  71. package/src/packing.ts +22 -0
  72. package/src/random.ts +75 -3
  73. package/src/routing/adapters.ts +111 -0
  74. package/src/routing/algorithms.ts +676 -0
  75. package/src/routing/coordination.ts +86 -0
  76. package/src/routing/engine.ts +473 -0
  77. package/src/routing/index.ts +68 -0
  78. package/src/routing/layout-cache.ts +65 -0
  79. package/src/routing/model.ts +218 -0
  80. package/src/routing/path.ts +324 -0
  81. package/src/routing/persistent.ts +117 -0
  82. package/src/routing/search.ts +276 -0
  83. package/src/routing/spatial.ts +78 -0
  84. package/src/routing/types.ts +146 -0
  85. package/src/types.ts +17 -2
package/src/layout.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import { getGraphIssues, type Graph, type GraphPatch, type VisualGraph } from "@statelyai/graph";
2
2
  import { LayoutError, UnsupportedLayoutError } from "./errors";
3
- import { boxAlgorithm } from "./box";
4
- import { fixedAlgorithm } from "./fixed";
5
- import { layeredAlgorithm } from "./layered";
6
- import { rectanglePackingAlgorithm } from "./packing";
7
- import { randomAlgorithm } from "./random";
8
- import { sporeCompactionAlgorithm, sporeOverlapRemovalAlgorithm } from "./spore";
3
+ import {
4
+ builtInLayoutAlgorithms,
5
+ executeLayoutAlgorithm,
6
+ getBuiltInLayoutAlgorithm,
7
+ } from "./internal/layout-engine";
9
8
  import type {
10
9
  LayoutAlgorithm,
11
10
  LayoutDiagnostic,
@@ -16,15 +15,12 @@ import type {
16
15
  LayoutScope,
17
16
  } from "./types";
18
17
 
19
- const algorithms = new Map<string, LayoutAlgorithm<never>>([
20
- ["box", boxAlgorithm as LayoutAlgorithm<never>],
21
- ["layered", layeredAlgorithm as LayoutAlgorithm<never>],
22
- ["fixed", fixedAlgorithm as LayoutAlgorithm<never>],
23
- ["rectpacking", rectanglePackingAlgorithm as LayoutAlgorithm<never>],
24
- ["random", randomAlgorithm as LayoutAlgorithm<never>],
25
- ["sporeCompaction", sporeCompactionAlgorithm as LayoutAlgorithm<never>],
26
- ["sporeOverlap", sporeOverlapRemovalAlgorithm as LayoutAlgorithm<never>],
27
- ]);
18
+ const algorithms = new Map<string, LayoutAlgorithm<never>>(
19
+ Object.entries(builtInLayoutAlgorithms).map(([id, algorithm]) => [
20
+ id,
21
+ algorithm as LayoutAlgorithm<never>,
22
+ ]),
23
+ );
28
24
 
29
25
  function supportsScope(algorithm: LayoutAlgorithm<unknown>, scope: LayoutScope): boolean {
30
26
  switch (scope.mode) {
@@ -73,11 +69,11 @@ function getLayoutPatches<N, E, G, P>(
73
69
  op: "updateNode",
74
70
  id: node.id,
75
71
  data: {
76
- x: next.x,
77
- y: next.y,
78
- width: next.width,
79
- height: next.height,
80
- ...(next.ports === undefined ? {} : { ports: next.ports }),
72
+ ...(node.x === next.x ? {} : { x: next.x ?? null }),
73
+ ...(node.y === next.y ? {} : { y: next.y ?? null }),
74
+ ...(node.width === next.width ? {} : { width: next.width ?? null }),
75
+ ...(node.height === next.height ? {} : { height: next.height ?? null }),
76
+ ...(node.ports === next.ports ? {} : { ports: next.ports ?? null }),
81
77
  },
82
78
  description: "Apply layout geometry",
83
79
  });
@@ -98,12 +94,12 @@ function getLayoutPatches<N, E, G, P>(
98
94
  op: "updateEdge",
99
95
  id: edge.id,
100
96
  data: {
101
- x: next.x,
102
- y: next.y,
103
- width: next.width,
104
- height: next.height,
105
- ...(next.routing === undefined ? {} : { routing: next.routing }),
106
- ...(next.points === undefined ? {} : { points: next.points }),
97
+ ...(edge.x === next.x ? {} : { x: next.x ?? null }),
98
+ ...(edge.y === next.y ? {} : { y: next.y ?? null }),
99
+ ...(edge.width === next.width ? {} : { width: next.width ?? null }),
100
+ ...(edge.height === next.height ? {} : { height: next.height ?? null }),
101
+ ...(edge.routing === next.routing ? {} : { routing: next.routing ?? null }),
102
+ ...(samePoints(edge.points, next.points) ? {} : { points: next.points ?? null }),
107
103
  },
108
104
  description: "Apply layout geometry",
109
105
  });
@@ -123,7 +119,9 @@ export function registerLayoutAlgorithm<O>(algorithm: LayoutAlgorithm<O>): () =>
123
119
  }
124
120
 
125
121
  export function getLayoutAlgorithm(id: string): LayoutAlgorithm<unknown> | undefined {
126
- return algorithms.get(id) as LayoutAlgorithm<unknown> | undefined;
122
+ return (
123
+ (algorithms.get(id) as LayoutAlgorithm<unknown> | undefined) ?? getBuiltInLayoutAlgorithm(id)
124
+ );
127
125
  }
128
126
 
129
127
  /**
@@ -152,6 +150,9 @@ export async function getLayout<N, E, G, P, O = unknown>(
152
150
  throw new UnsupportedLayoutError(`${algorithm.id} does not support ${scope.mode} layout yet`);
153
151
  }
154
152
 
153
+ if (request.constraints?.length && !algorithm.capabilities.constraints) {
154
+ throw new UnsupportedLayoutError(`${algorithm.id} does not support geometry constraints`);
155
+ }
155
156
  const issues = getGraphIssues(request.graph as Graph);
156
157
  if (issues.length > 0) {
157
158
  throw new LayoutError(issues.map((issue) => issue.message).join("; "), "INVALID_GRAPH");
@@ -159,6 +160,7 @@ export async function getLayout<N, E, G, P, O = unknown>(
159
160
 
160
161
  const context: LayoutExecutionContext = {
161
162
  scope,
163
+ constraints: request.constraints,
162
164
  diagnostics,
163
165
  ...(request.signal === undefined ? {} : { signal: request.signal }),
164
166
  measurePhase(id, run) {
@@ -185,7 +187,12 @@ export async function getLayout<N, E, G, P, O = unknown>(
185
187
  },
186
188
  };
187
189
  context.throwIfAborted();
188
- const graph = await algorithm.layout(request.graph, request.options as O, context);
190
+ const graph = await executeLayoutAlgorithm({
191
+ algorithm: algorithm as LayoutAlgorithm<O>,
192
+ graph: request.graph,
193
+ options: request.options as O,
194
+ context,
195
+ });
189
196
  context.throwIfAborted();
190
197
 
191
198
  return {
package/src/packing.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Graph, VisualGraph, VisualNode } from "@statelyai/graph";
2
2
  import { getNodeSize, type LayoutOptions } from "@statelyai/graph/layout";
3
3
  import { getFixedLayout } from "./fixed";
4
+ import { setElkjs0111ResultPolicy } from "./internal/elkjs-compatibility";
4
5
  import type { LayoutPadding } from "./layered";
5
6
  import type { LayoutAlgorithm } from "./types";
6
7
 
@@ -72,3 +73,24 @@ export const rectanglePackingAlgorithm: LayoutAlgorithm<RectanglePackingLayoutOp
72
73
  return getRectanglePackingLayout(graph, options ?? {});
73
74
  },
74
75
  };
76
+
77
+ /** Pinned output policy used only by the elkjs 0.11.1 compatibility adapter. */
78
+ export const elkjs0111RectanglePackingAlgorithm: LayoutAlgorithm<RectanglePackingLayoutOptions> = {
79
+ ...rectanglePackingAlgorithm,
80
+ layout(graph, options = {}) {
81
+ const result = getRectanglePackingLayout(graph, options);
82
+ const padding = getPadding(options.padding);
83
+ return setElkjs0111ResultPolicy(result, {
84
+ providerBounds:
85
+ result.nodes.length === 0
86
+ ? { width: 0, height: 0 }
87
+ : {
88
+ width: Math.max(...result.nodes.map((node) => node.x + node.width)) + padding.right,
89
+ height:
90
+ Math.max(...result.nodes.map((node) => node.y + node.height)) + padding.bottom,
91
+ },
92
+ preserveEdgeSections: true,
93
+ skipBoundsNormalization: true,
94
+ });
95
+ },
96
+ };
package/src/random.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import type { Graph, Point, VisualGraph, VisualNode } from "@statelyai/graph";
9
9
  import { getNodeSize, type LayoutOptions } from "@statelyai/graph/layout";
10
10
  import { getFixedLayout } from "./fixed";
11
+ import { setElkjs0111ResultPolicy } from "./internal/elkjs-compatibility";
11
12
  import { JavaRandom } from "./java-random";
12
13
  import type { LayoutPadding } from "./layered";
13
14
  import type { LayoutAlgorithm } from "./types";
@@ -19,6 +20,21 @@ export interface RandomLayoutOptions extends Pick<LayoutOptions, "direction" | "
19
20
  seed?: number;
20
21
  }
21
22
 
23
+ interface RandomLayoutBehavior {
24
+ elkjs0111EdgeTargetBug: boolean;
25
+ preserveProviderBounds: boolean;
26
+ }
27
+
28
+ const nativeBehavior: RandomLayoutBehavior = {
29
+ elkjs0111EdgeTargetBug: false,
30
+ preserveProviderBounds: false,
31
+ };
32
+
33
+ const elkjs0111Behavior: RandomLayoutBehavior = {
34
+ elkjs0111EdgeTargetBug: true,
35
+ preserveProviderBounds: true,
36
+ };
37
+
22
38
  function getPadding(value: RandomLayoutOptions["padding"]): LayoutPadding {
23
39
  if (typeof value === "number") {
24
40
  return { top: value, right: value, bottom: value, left: value };
@@ -54,6 +70,14 @@ function borderPoint(source: VisualNode, target: VisualNode): Point {
54
70
  export function getRandomLayout<N, E, G, P>(
55
71
  graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
56
72
  options: RandomLayoutOptions = {},
73
+ ): VisualGraph<N, E, G, P> {
74
+ return getRandomLayoutWithBehavior(graph, options, nativeBehavior);
75
+ }
76
+
77
+ function getRandomLayoutWithBehavior<N, E, G, P>(
78
+ graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
79
+ options: RandomLayoutOptions,
80
+ behavior: RandomLayoutBehavior,
57
81
  ): VisualGraph<N, E, G, P> {
58
82
  if (graph.nodes.length === 0) return getFixedLayout(graph, options);
59
83
  const random = new JavaRandom(options.seed && options.seed !== 0 ? options.seed : Date.now());
@@ -82,10 +106,36 @@ export function getRandomLayout<N, E, G, P>(
82
106
  const nodeById = new Map(nodes.map((node) => [node.id, node]));
83
107
  const totalWidth = drawWidth + padding.left + padding.right;
84
108
  const totalHeight = drawHeight + padding.top + padding.bottom;
85
- const edges = graph.edges.map((edge) => {
109
+ const layoutEdge = (edge: (typeof graph.edges)[number]) => {
86
110
  const source = nodeById.get(edge.sourceId);
87
111
  const target = nodeById.get(edge.targetId);
88
112
  if (!source || !target) return edge;
113
+ if (behavior.elkjs0111EdgeTargetBug) {
114
+ const start = { x: source.x + source.width, y: source.y + source.height / 2 };
115
+ const end = { x: source.x + source.width / 2, y: source.y + source.height };
116
+ const bendCount = random.nextInt(5) + 1;
117
+ const xDifference = end.x - start.x;
118
+ const yDifference = end.y - start.y;
119
+ const maximumDeviation = Math.hypot(xDifference, yDifference) * Math.fround(0.2);
120
+ const xIncrement = xDifference / (bendCount + 1);
121
+ const yIncrement = yDifference / (bendCount + 1);
122
+ let x = start.x;
123
+ let y = start.y;
124
+ const points: Point[] = [start];
125
+ for (let index = 0; index < bendCount; index++) {
126
+ x += xIncrement;
127
+ y += yIncrement;
128
+ let randomX = x + random.nextFloat() * maximumDeviation - maximumDeviation / 2;
129
+ if (randomX < 0) randomX = 1;
130
+ else if (randomX > totalWidth) randomX = totalWidth - 1;
131
+ let randomY = y + random.nextFloat() * maximumDeviation - maximumDeviation / 2;
132
+ if (randomY < 0) randomY = 1;
133
+ else if (randomY > totalHeight) randomY = totalHeight - 1;
134
+ points.push({ x: randomX, y: randomY });
135
+ }
136
+ points.push(end);
137
+ return { ...edge, points, routing: "polyline" as const };
138
+ }
89
139
  const start = borderPoint(source, target);
90
140
  const end = borderPoint(target, source);
91
141
  const bendCount = random.nextInt(5) + (source === target ? 1 : 0);
@@ -119,11 +169,25 @@ export function getRandomLayout<N, E, G, P>(
119
169
  }
120
170
  points.push(end);
121
171
  return { ...edge, points, routing: "polyline" as const };
122
- });
123
- return getFixedLayout(
172
+ };
173
+ const edges = behavior.elkjs0111EdgeTargetBug
174
+ ? graph.nodes.flatMap((node) =>
175
+ graph.edges.filter((edge) => edge.sourceId === node.id).map(layoutEdge),
176
+ )
177
+ : graph.edges.map(layoutEdge);
178
+ const result = getFixedLayout(
124
179
  { ...graph, nodes, edges },
125
180
  { direction: options.direction ?? graph.direction },
126
181
  );
182
+ if (behavior.preserveProviderBounds) {
183
+ setElkjs0111ResultPolicy(result, {
184
+ providerBounds: {
185
+ width: drawWidth + 2 * (padding.left + padding.right),
186
+ height: drawHeight + 2 * (padding.top + padding.bottom),
187
+ },
188
+ });
189
+ }
190
+ return result;
127
191
  }
128
192
 
129
193
  export const randomAlgorithm: LayoutAlgorithm<RandomLayoutOptions> = {
@@ -140,3 +204,11 @@ export const randomAlgorithm: LayoutAlgorithm<RandomLayoutOptions> = {
140
204
  return getRandomLayout(graph, options ?? {});
141
205
  },
142
206
  };
207
+
208
+ /** Pinned behavior used only by the elkjs 0.11.1 compatibility adapter. */
209
+ export const elkjs0111RandomAlgorithm: LayoutAlgorithm<RandomLayoutOptions> = {
210
+ ...randomAlgorithm,
211
+ layout(graph, options) {
212
+ return getRandomLayoutWithBehavior(graph, options ?? {}, elkjs0111Behavior);
213
+ },
214
+ };
@@ -0,0 +1,111 @@
1
+ import type { GraphPatch, Point, VisualGraph } from "@statelyai/graph";
2
+ import { hasCurrentRouteGeometry } from "./layout-cache";
3
+ import { worldGeometry } from "../authoring/coordinates";
4
+ import { freeze } from "./model";
5
+ import { flattenPath, pathFromPoints } from "./path";
6
+ import { PersistentMap } from "./persistent";
7
+ import type { Route, RoutePath, RoutePoint, RouteSegment } from "./types";
8
+
9
+ /** ELK/native spline output: start, then repeating control1/control2/end triples. */
10
+ export function pathFromSplinePoints(points: readonly RoutePoint[]): RoutePath {
11
+ if (points.length < 4 || (points.length - 1) % 3 !== 0)
12
+ throw new RangeError("Cubic spline points require a start and control1/control2/end triples");
13
+ const segments: RouteSegment[] = [];
14
+ for (let i = 1; i < points.length; i += 3)
15
+ segments.push({
16
+ kind: "cubic",
17
+ control1: { ...points[i]! },
18
+ control2: { ...points[i + 1]! },
19
+ to: { ...points[i + 2]! },
20
+ });
21
+ return { start: { ...points[0]! }, segments };
22
+ }
23
+ /** Normalize routes from any native layout algorithm, without running layout again. */
24
+ export function getLayoutRoutes(graph: VisualGraph): ReadonlyMap<string, Route> {
25
+ const compound = graph as VisualGraph & { compoundRoutes?: ReadonlyMap<string, Route> };
26
+ if (compound.compoundRoutes?.size && hasCurrentRouteGeometry(graph, compound.compoundRoutes))
27
+ return compound.compoundRoutes;
28
+ const world = worldGeometry(graph),
29
+ nodes = new Map(world.nodes.map((n) => [n.id, n]));
30
+ let result = new PersistentMap<Route>();
31
+ for (const edge of world.edges) {
32
+ const points = edge.points;
33
+ let path: RoutePath | undefined;
34
+ if (points?.length && points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) {
35
+ if (edge.routing === "splines") {
36
+ if (points.length >= 4 && (points.length - 1) % 3 === 0)
37
+ path = pathFromSplinePoints(points);
38
+ } else path = pathFromPoints(points);
39
+ }
40
+ const routed = path !== undefined;
41
+ if (!path) {
42
+ const a = nodes.get(edge.sourceId),
43
+ b = nodes.get(edge.targetId);
44
+ path = pathFromPoints([
45
+ { x: a ? a.x + a.width : 0, y: a ? a.y + a.height / 2 : 0 },
46
+ { x: b?.x ?? 40, y: b ? b.y + b.height / 2 : 40 },
47
+ ]);
48
+ }
49
+ result = result.set(
50
+ edge.id,
51
+ freeze({
52
+ edgeId: edge.id,
53
+ status: routed ? "routed" : "fallback",
54
+ diagnostics: routed
55
+ ? []
56
+ : [
57
+ {
58
+ code: "MISSING_GEOMETRY",
59
+ edgeId: edge.id,
60
+ message: "Layout has no valid route; rendered a fallback connection",
61
+ },
62
+ ],
63
+ sections: [
64
+ {
65
+ id: `${edge.id}:0`,
66
+ from: {
67
+ kind: "node",
68
+ nodeId: edge.sourceId,
69
+ ...(edge.sourcePort === undefined ? {} : { port: edge.sourcePort }),
70
+ },
71
+ to: {
72
+ kind: "node",
73
+ nodeId: edge.targetId,
74
+ ...(edge.targetPort === undefined ? {} : { port: edge.targetPort }),
75
+ },
76
+ path,
77
+ },
78
+ ],
79
+ }),
80
+ );
81
+ }
82
+ return result;
83
+ }
84
+ /** One polyline per section preserves label gaps and branching for simple renderers. */
85
+ export function routeToPolylines(
86
+ route: Route,
87
+ options?: { readonly tolerance?: number },
88
+ ): readonly (readonly RoutePoint[])[] {
89
+ return route.sections.map((s) => flattenPath(s.path, options));
90
+ }
91
+ /** Compatibility for single-section native consumers. Refuses to erase gaps or topology. */
92
+ export function routeToGraphPatch(
93
+ route: Route,
94
+ options: { readonly tolerance?: number; readonly offset?: RoutePoint } = {},
95
+ ): GraphPatch {
96
+ if (
97
+ route.sections.length !== 1 ||
98
+ route.sections[0]!.sharedId ||
99
+ route.sections[0]!.from.kind !== "node" ||
100
+ route.sections[0]!.to.kind !== "node"
101
+ )
102
+ throw new Error(
103
+ "This route requires section-aware rendering; a single GraphEdge.points array would lose topology",
104
+ );
105
+ const offset = options.offset ?? { x: 0, y: 0 };
106
+ const points: Point[] = flattenPath(route.sections[0]!.path, options).map((p) => ({
107
+ x: p.x - offset.x,
108
+ y: p.y - offset.y,
109
+ }));
110
+ return { op: "updateEdge", id: route.edgeId, data: { points, routing: "polyline" } };
111
+ }