graf: přidána funkce pro manuální pozicování vrcholů a perzistence pozice
This commit is contained in:
		
							parent
							
								
									bd5e5a6a48
								
							
						
					
					
						commit
						a264dbead9
					
				
					 6 changed files with 241 additions and 28 deletions
				
			
		|  | @ -11,6 +11,7 @@ | ||||||
|   let clicked: string[] = []; |   let clicked: string[] = []; | ||||||
|   let graph: Graph; |   let graph: Graph; | ||||||
|   let currentTask: TaskDescriptor | null = null; |   let currentTask: TaskDescriptor | null = null; | ||||||
|  |   let nodeDraggingEnabled: boolean = false; | ||||||
| 
 | 
 | ||||||
|   function clickTask(e: CustomEvent<TaskDescriptor>) { |   function clickTask(e: CustomEvent<TaskDescriptor>) { | ||||||
|     // ukladani seznamu poslednich kliknuti |     // ukladani seznamu poslednich kliknuti | ||||||
|  | @ -39,6 +40,11 @@ | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async function saveCurrentStateWithPositions() { | ||||||
|  |     tasks.positions = graph.getNodePositions(); | ||||||
|  |     await saveTasks(tasks); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async function saveCurrentState() { |   async function saveCurrentState() { | ||||||
|     await saveTasks(tasks); |     await saveTasks(tasks); | ||||||
|   } |   } | ||||||
|  | @ -106,22 +112,36 @@ | ||||||
|         {repulsionForce} |         {repulsionForce} | ||||||
|         on:selectTask={clickTask} |         on:selectTask={clickTask} | ||||||
|         on:preSelectTask={startHovering} |         on:preSelectTask={startHovering} | ||||||
|         bind:this={graph} /> |         bind:this={graph}  | ||||||
|  |         {nodeDraggingEnabled} /> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   <div class="right"> |   <div class="right"> | ||||||
|     <div class="toolbox"> |     <div class="toolbox"> | ||||||
|       <div>Toolbox</div> |       <div>Toolbox</div> | ||||||
|       <div> |       <div> | ||||||
|         <button disabled={clicked.length <= 1} on:click={addEdge}>Přidat hranu {clicked[clicked.length - 2]} -> {clicked[clicked.length - 1]}</button> |         <button disabled={clicked.length <= 1} on:click={addEdge}>Přidat hranu {clicked[clicked.length - 2]} | ||||||
|  |           -> {clicked[clicked.length - 1]}</button> | ||||||
|  |       </div> | ||||||
|  |       <div> | ||||||
|  |         <button on:click={graph.runSimulation}>Spustit simulaci</button> | ||||||
|       </div> |       </div> | ||||||
|       <div><button on:click={graph.runSimulation}>Spustit simulaci</button></div> |  | ||||||
|       <div> |       <div> | ||||||
|         Repulsion force: <input type="number" bind:value={repulsionForce} name="repulsionForceInput" max="1000" min="-10000" /> |         Repulsion force: <input type="number" bind:value={repulsionForce} name="repulsionForceInput" max="1000" min="-10000" /> | ||||||
|       </div> |       </div> | ||||||
|       <div> |       <div> | ||||||
|         <button on:click={saveCurrentState}>Uložit aktuální stav</button> |         <button on:click={saveCurrentState}>Uložit aktuální stav</button> | ||||||
|       </div> |       </div> | ||||||
|  |       <div> | ||||||
|  |         <button on:click={saveCurrentStateWithPositions}>Uložit aktuální stav | ||||||
|  |           včetně pozic nodů</button> | ||||||
|  |       </div> | ||||||
|  |       <div> | ||||||
|  |         <label> | ||||||
|  |           <input type="checkbox" bind:checked={nodeDraggingEnabled} /> Povolit přesouvání | ||||||
|  |           vrcholů | ||||||
|  |         </label> | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <div class="taskDetails"> |     <div class="taskDetails"> | ||||||
|       {#if currentTask != null} |       {#if currentTask != null} | ||||||
|  |  | ||||||
|  | @ -6,11 +6,12 @@ | ||||||
|   import type { TasksFile, TaskDescriptor } from "./task-loader"; |   import type { TasksFile, TaskDescriptor } from "./task-loader"; | ||||||
|   import { createNodesAndEdges } from "./graph-types"; |   import { createNodesAndEdges } from "./graph-types"; | ||||||
|   import { taskForce } from "./task-force"; |   import { taskForce } from "./task-force"; | ||||||
|   import { zoom } from "d3"; |  | ||||||
| 
 | 
 | ||||||
|   export let tasks: TasksFile; |   export let tasks: TasksFile; | ||||||
|   let hoveredTask: null | string = null; |  | ||||||
|   export let repulsionForce: number = -1000; |   export let repulsionForce: number = -1000; | ||||||
|  |   export let nodeDraggingEnabled: boolean = false; | ||||||
|  | 
 | ||||||
|  |   let hoveredTask: null | string = null; | ||||||
| 
 | 
 | ||||||
|   // Svelte automatically fills these with a reference |   // Svelte automatically fills these with a reference | ||||||
|   let container: HTMLElement; |   let container: HTMLElement; | ||||||
|  | @ -23,7 +24,7 @@ | ||||||
|   let [nodes, edges] = createNodesAndEdges(tasks); |   let [nodes, edges] = createNodesAndEdges(tasks); | ||||||
|   function hack() { |   function hack() { | ||||||
|     [nodes, edges] = createNodesAndEdges(tasks, nodes, edges); |     [nodes, edges] = createNodesAndEdges(tasks, nodes, edges); | ||||||
|     runSimulation(); |     //runSimulation(); | ||||||
|   } |   } | ||||||
|   $: { |   $: { | ||||||
|     tasks; |     tasks; | ||||||
|  | @ -74,12 +75,18 @@ | ||||||
|       nodes = nodes; |       nodes = nodes; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   const zoomer = d3.zoom().scaleExtent([0.1, 2]) |  | ||||||
| 
 | 
 | ||||||
|   $: { |   export function getNodePositions(): Map<string, [number, number]> { | ||||||
|     // zoomer.extent([[-clientWidth / 2,-clientHeight / 2],[clientWidth,clientHeight]]) |     let res = new Map(); | ||||||
|  |     for (let n of nodes) { | ||||||
|  |       if (n.x != undefined && n.y != undefined) { | ||||||
|  |         res.set(n.id, [n.x, n.y]); | ||||||
|  |       } | ||||||
|  |     }  | ||||||
|  |     return res | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|   // start simulation and center view on create |   // start simulation and center view on create | ||||||
|   onMount(() => { |   onMount(() => { | ||||||
|     // set center of the SVG at (0,0) |     // set center of the SVG at (0,0) | ||||||
|  | @ -89,10 +96,9 @@ | ||||||
|     function zoomed(e) { |     function zoomed(e) { | ||||||
|       svg.attr("transform", e.transform); |       svg.attr("transform", e.transform); | ||||||
|     } |     } | ||||||
|  |     const zoomer = d3.zoom().scaleExtent([0.1, 2]) | ||||||
|     zoomer.on("zoom", zoomed); |     zoomer.on("zoom", zoomed); | ||||||
|     d3.select(container).call(zoomer); |     d3.select(container).call(zoomer); | ||||||
| 
 |  | ||||||
|     runSimulation(); |  | ||||||
|   }); |   }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -129,7 +135,9 @@ | ||||||
|             {task} |             {task} | ||||||
|             on:taskClick |             on:taskClick | ||||||
|             on:click={nodeClick(task.task)} |             on:click={nodeClick(task.task)} | ||||||
|             on:hoveringChange={nodeHover(task.task)} /> |             on:hoveringChange={nodeHover(task.task)} | ||||||
|  |             on:positionChange={() => { tasks = tasks; }} | ||||||
|  |             draggingEnabled={nodeDraggingEnabled} /> | ||||||
|         {/each} |         {/each} | ||||||
|       </g> |       </g> | ||||||
|     </g> |     </g> | ||||||
|  |  | ||||||
|  | @ -1,9 +1,12 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  |   import * as d3 from "d3"; | ||||||
|  | 
 | ||||||
|   import { createEventDispatcher, onMount } from "svelte"; |   import { createEventDispatcher, onMount } from "svelte"; | ||||||
|   import type { TaskId } from "./graph-types"; |   import type { TaskId } from "./graph-types"; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   export let task: TaskId; |   export let task: TaskId; | ||||||
|  |   export let draggingEnabled: boolean = false; | ||||||
|  | 
 | ||||||
|   let hovering: boolean = false; |   let hovering: boolean = false; | ||||||
|   let text_element: SVGTextElement; |   let text_element: SVGTextElement; | ||||||
| 
 | 
 | ||||||
|  | @ -31,6 +34,34 @@ | ||||||
|     const bbox = text_element.getBBox(); |     const bbox = text_element.getBBox(); | ||||||
|     ellipse_rx = bbox.width / 2 + 8; |     ellipse_rx = bbox.width / 2 + 8; | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   // dragging | ||||||
|  |   let dragging: boolean = false; | ||||||
|  |   function dragStart(e: MouseEvent) { | ||||||
|  |     if (!draggingEnabled) return; | ||||||
|  | 
 | ||||||
|  |     dragging = true; | ||||||
|  |     e.preventDefault() | ||||||
|  |     e.stopPropagation(); | ||||||
|  |   } | ||||||
|  |   function drag(e: MouseEvent) { | ||||||
|  |     if (!draggingEnabled) return; | ||||||
|  |     if (!dragging) return; | ||||||
|  | 
 | ||||||
|  |     let [x, y] = d3.pointer(e); | ||||||
|  |     task.x = x; | ||||||
|  |     task.y = y; | ||||||
|  |     eventDispatcher("positionChange"); | ||||||
|  |     e.preventDefault() | ||||||
|  |     e.stopPropagation() | ||||||
|  |   } | ||||||
|  |   function dragStop(e: MouseEvent) { | ||||||
|  |     if (!draggingEnabled) return; | ||||||
|  | 
 | ||||||
|  |     dragging = false; | ||||||
|  |     e.preventDefault(); | ||||||
|  |     e.stopPropagation(); | ||||||
|  |   } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|  | @ -45,7 +76,7 @@ | ||||||
|   } |   } | ||||||
| </style> | </style> | ||||||
| 
 | 
 | ||||||
| <g on:mouseenter={enter} on:mouseleave={leave} on:click={click}> | <g on:mouseenter={enter} on:mouseleave={leave} on:click={click} on:mousedown={dragStart} on:mouseup={dragStop} on:mousemove={drag}> | ||||||
|   <ellipse rx={ellipse_rx} ry={20} {cx} {cy} /> |   <ellipse rx={ellipse_rx} ry={20} {cx} {cy} /> | ||||||
|   <text |   <text | ||||||
|     bind:this={text_element} |     bind:this={text_element} | ||||||
|  |  | ||||||
|  | @ -21,11 +21,14 @@ function toMapById(nodes: TaskId[]): Map<string, TaskId> { | ||||||
| function createNodes(tasks: TasksFile, old?: TaskId[]): TaskId[] { | function createNodes(tasks: TasksFile, old?: TaskId[]): TaskId[] { | ||||||
|     let m = (old == undefined) ? new Map<string, TaskId>() : toMapById(old); |     let m = (old == undefined) ? new Map<string, TaskId>() : toMapById(old); | ||||||
| 
 | 
 | ||||||
|     let res = tasks.tasks.map((t) => { |     let res: TaskId[] = tasks.tasks.map((t) => { | ||||||
|         return { id: t.id, task: t }; |         return { id: t.id, task: t }; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     for (let t of res) { |     for (let t of res) { | ||||||
|  |         if (tasks.positions.has(t.id)) { | ||||||
|  |             [t.x, t.y] = tasks.positions.get(t.id)! | ||||||
|  |         } | ||||||
|         if (m.has(t.id)) { |         if (m.has(t.id)) { | ||||||
|             Object.assign(t, m.get(t.id)) |             Object.assign(t, m.get(t.id)) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -9,20 +9,31 @@ export type TaskDescriptor = { | ||||||
| export type TasksFile = { | export type TasksFile = { | ||||||
|     tasks: TaskDescriptor[] |     tasks: TaskDescriptor[] | ||||||
|     clusters: { [name: string]: string[] } |     clusters: { [name: string]: string[] } | ||||||
|  |     positions: Map<string, [number, number]> | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type TaskMap = Map<string, TaskDescriptor>; | export type TaskMap = Map<string, TaskDescriptor>; | ||||||
| 
 | 
 | ||||||
| export async function loadTasks(): Promise<TasksFile> { | export async function loadTasks(): Promise<TasksFile> { | ||||||
|     const r = await fetch("/tasks.json") |     const r = await fetch("/tasks.json") | ||||||
|     return await r.json() |     const j = await r.json() | ||||||
|  |     if (j.positions == null) | ||||||
|  |         j.positions = new Map(); | ||||||
|  |     else | ||||||
|  |         j.positions = new Map(Object.entries(j.positions)) | ||||||
|  |     return j | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function saveTasks(tasks: TasksFile) { | export async function saveTasks(tasks: TasksFile) { | ||||||
|  |     let p: any = {} | ||||||
|  |     for (let [key, val] of tasks.positions.entries()) | ||||||
|  |         p[key] = val; | ||||||
|  |     const data = {...tasks, positions: p} | ||||||
|  | 
 | ||||||
|     // request options
 |     // request options
 | ||||||
|     const options = { |     const options = { | ||||||
|         method: 'POST', |         method: 'POST', | ||||||
|         body: JSON.stringify(tasks, null, 4), |         body: JSON.stringify(data, null, 4), | ||||||
|         headers: { |         headers: { | ||||||
|             'Content-Type': 'application/json' |             'Content-Type': 'application/json' | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										160
									
								
								tasks.json
									
									
									
									
									
								
							
							
						
						
									
										160
									
								
								tasks.json
									
									
									
									
									
								
							|  | @ -158,52 +158,62 @@ | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "id": "29-Z3-2","type": "open-data", |             "id": "29-Z3-2", | ||||||
|  |             "type": "open-data", | ||||||
|             "comment": "Písemka z angličtiny — voser implementovat, easy dřevorubecký řešení, optimálně trie, což na Z IMHO hard", |             "comment": "Písemka z angličtiny — voser implementovat, easy dřevorubecký řešení, optimálně trie, což na Z IMHO hard", | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "id": "26-Z1-4","type": "open-data", |             "id": "26-Z1-4", | ||||||
|  |             "type": "open-data", | ||||||
|             "comment": "Hroch v jezeře - BFS či jiné prohledávání, počítání velikosti komponent v 2D poli, ", |             "comment": "Hroch v jezeře - BFS či jiné prohledávání, počítání velikosti komponent v 2D poli, ", | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "id": "26-Z4-4","type": "open-data", |             "id": "26-Z4-4", | ||||||
|  |             "type": "open-data", | ||||||
|             "comment": "Hlídači v labyrintu - policajti hlídající na grafu, konkrétně na stromě, rekurze, technicky asi až DP", |             "comment": "Hlídači v labyrintu - policajti hlídající na grafu, konkrétně na stromě, rekurze, technicky asi až DP", | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "id": "26-Z3-4","type": "open-data", |             "id": "26-Z3-4", | ||||||
|  |             "type": "open-data", | ||||||
|             "comment": "Tvar labyrintu - nejdelší cesta ve stromě, graf", |             "comment": "Tvar labyrintu - nejdelší cesta ve stromě, graf", | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "id": "29-Z1-4","type": "open-data", |             "id": "29-Z1-4", | ||||||
|  |             "type": "open-data", | ||||||
|             "comment": "Zuzčin výlet — DFS (topologické pořadí)", |             "comment": "Zuzčin výlet — DFS (topologické pořadí)", | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "id": "31-Z1-2","type": "open-data", |             "id": "31-Z1-2", | ||||||
|  |             "type": "open-data", | ||||||
|             "comment": "BFS (šachovnice, custom figurka, nejkratší cesta)  ", |             "comment": "BFS (šachovnice, custom figurka, nejkratší cesta)  ", | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "id": "31-Z3-2","type": "open-data", |             "id": "31-Z3-2", | ||||||
|  |             "type": "open-data", | ||||||
|             "comment": "DFS (hledání cesty v grafu po písmenech)", |             "comment": "DFS (hledání cesty v grafu po písmenech)", | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "id": "31-Z3-3","type": "open-data", |             "id": "31-Z3-3", | ||||||
|  |             "type": "open-data", | ||||||
|             "comment": "barvení bipartitního grafu (hledání partit), na vstupu hrany", |             "comment": "barvení bipartitního grafu (hledání partit), na vstupu hrany", | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "id": "26-Z4-2","type": "open-data", |             "id": "26-Z4-2", | ||||||
|  |             "type": "open-data", | ||||||
|             "comment": "Sbírání vajíček - hledení mediánu, musí se to ale vymyslet, nejkratší cesta při chození tam a zpět", |             "comment": "Sbírání vajíček - hledení mediánu, musí se to ale vymyslet, nejkratší cesta při chození tam a zpět", | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "id": "26-Z3-1","type": "open-data", |             "id": "26-Z3-1", | ||||||
|  |             "type": "open-data", | ||||||
|             "comment": "Zámky labyrintu - hromada ifů, vhodné možná na code review, hledání čísla z trojice takového, že je trojice aritmetrická posloupnost", |             "comment": "Zámky labyrintu - hromada ifů, vhodné možná na code review, hledání čísla z trojice takového, že je trojice aritmetrická posloupnost", | ||||||
|             "requires": [] |             "requires": [] | ||||||
|         } |         } | ||||||
|  | @ -259,5 +269,135 @@ | ||||||
|         "Nápad": [ |         "Nápad": [ | ||||||
|             "26-Z4-2" |             "26-Z4-2" | ||||||
|         ] |         ] | ||||||
|  |     }, | ||||||
|  |     "positions": { | ||||||
|  |         "start": [ | ||||||
|  |             60.9366784537015, | ||||||
|  |             15.282753057729023 | ||||||
|  |         ], | ||||||
|  |         "jak-resit-ulohy": [ | ||||||
|  |             57.60463651559811, | ||||||
|  |             85.05400883098152 | ||||||
|  |         ], | ||||||
|  |         "31-Z1-1": [ | ||||||
|  |             176, | ||||||
|  |             112 | ||||||
|  |         ], | ||||||
|  |         "26-Z1-1": [ | ||||||
|  |             -86.4374292583939, | ||||||
|  |             114.1704716787137 | ||||||
|  |         ], | ||||||
|  |         "26-Z2-1": [ | ||||||
|  |             -115, | ||||||
|  |             184 | ||||||
|  |         ], | ||||||
|  |         "27-Z2-1": [ | ||||||
|  |             -30, | ||||||
|  |             181 | ||||||
|  |         ], | ||||||
|  |         "26-Z1-2": [ | ||||||
|  |             -324, | ||||||
|  |             235 | ||||||
|  |         ], | ||||||
|  |         "26-Z4-3": [ | ||||||
|  |             -220.83782675574338, | ||||||
|  |             190.72741511636147 | ||||||
|  |         ], | ||||||
|  |         "29-Z3-1": [ | ||||||
|  |             -158, | ||||||
|  |             276 | ||||||
|  |         ], | ||||||
|  |         "31-Z1-4": [ | ||||||
|  |             -245, | ||||||
|  |             284 | ||||||
|  |         ], | ||||||
|  |         "29-Z1-1": [ | ||||||
|  |             154, | ||||||
|  |             199 | ||||||
|  |         ], | ||||||
|  |         "29-Z2-1": [ | ||||||
|  |             259, | ||||||
|  |             272 | ||||||
|  |         ], | ||||||
|  |         "29-Z4-3": [ | ||||||
|  |             164, | ||||||
|  |             364 | ||||||
|  |         ], | ||||||
|  |         "26-Z2-4": [ | ||||||
|  |             403.46860248688955, | ||||||
|  |             23.996420431821353 | ||||||
|  |         ], | ||||||
|  |         "29-Z1-3": [ | ||||||
|  |             -442.93377199520177, | ||||||
|  |             -73.3461550905827 | ||||||
|  |         ], | ||||||
|  |         "26-Z2-2": [ | ||||||
|  |             -64.3269017874483, | ||||||
|  |             -91.7677899309802 | ||||||
|  |         ], | ||||||
|  |         "26-Z3-3": [ | ||||||
|  |             450.612997715014, | ||||||
|  |             -65.45002256579735 | ||||||
|  |         ], | ||||||
|  |         "26-Z4-1": [ | ||||||
|  |             -551.4441557449455, | ||||||
|  |             -38.58561957493338 | ||||||
|  |         ], | ||||||
|  |         "29-Z3-3": [ | ||||||
|  |             302.6193712343388, | ||||||
|  |             13.535655571354772 | ||||||
|  |         ], | ||||||
|  |         "26-Z1-3": [ | ||||||
|  |             39.68234662392814, | ||||||
|  |             -102.30393169322402 | ||||||
|  |         ], | ||||||
|  |         "26-Z2-3": [ | ||||||
|  |             -320.2744362098852, | ||||||
|  |             -72.1967458848867 | ||||||
|  |         ], | ||||||
|  |         "26-Z3-2": [ | ||||||
|  |             644.167217052611, | ||||||
|  |             -0.4551191547699971 | ||||||
|  |         ], | ||||||
|  |         "29-Z3-2": [ | ||||||
|  |             -368.51198400620785, | ||||||
|  |             1.6854832115582556 | ||||||
|  |         ], | ||||||
|  |         "26-Z1-4": [ | ||||||
|  |             147.37372961796666, | ||||||
|  |             -89.40554252368531 | ||||||
|  |         ], | ||||||
|  |         "26-Z4-4": [ | ||||||
|  |             335.75328660449225, | ||||||
|  |             -81.50932588628959 | ||||||
|  |         ], | ||||||
|  |         "26-Z3-4": [ | ||||||
|  |             -480.95284944300494, | ||||||
|  |             29.587091058893556 | ||||||
|  |         ], | ||||||
|  |         "29-Z1-4": [ | ||||||
|  |             554.2061258687584, | ||||||
|  |             -44.42093615098819 | ||||||
|  |         ], | ||||||
|  |         "31-Z1-2": [ | ||||||
|  |             -236.06692326455527, | ||||||
|  |             -77.65095973572805 | ||||||
|  |         ], | ||||||
|  |         "31-Z3-2": [ | ||||||
|  |             -154.07095173801807, | ||||||
|  |             -65.44506844403092 | ||||||
|  |         ], | ||||||
|  |         "31-Z3-3": [ | ||||||
|  |             514.5773720938831, | ||||||
|  |             41.05028681292239 | ||||||
|  |         ], | ||||||
|  |         "26-Z4-2": [ | ||||||
|  |             -633.6757896792913, | ||||||
|  |             1.4284188628810082 | ||||||
|  |         ], | ||||||
|  |         "26-Z3-1": [ | ||||||
|  |             234.170971842641, | ||||||
|  |             -59.230241172550606 | ||||||
|  |         ] | ||||||
|     } |     } | ||||||
| } | } | ||||||
		Reference in a new issue