├── requirements.txt ├── splash.png ├── teaser.png ├── .gitignore ├── mooey-experiments-v2.zip ├── splash.py ├── fileformat_graphml.py ├── dialog_bend_penalty.py ├── ui.py ├── README.md ├── fileformat_loom.py ├── assign.py ├── render.py ├── Network.py ├── layout.py ├── mui.py ├── freiburg-bug.json └── loom-examples └── wuerzburg.json /requirements.txt: -------------------------------------------------------------------------------- 1 | scipy>=1.15 2 | ortools>=9.12 3 | PySide6>=6.8 -------------------------------------------------------------------------------- /splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcvdijk/mooey/HEAD/splash.png -------------------------------------------------------------------------------- /teaser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcvdijk/mooey/HEAD/teaser.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | *.pyc 4 | *.txt 5 | render.json 6 | render.svg -------------------------------------------------------------------------------- /mooey-experiments-v2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcvdijk/mooey/HEAD/mooey-experiments-v2.zip -------------------------------------------------------------------------------- /splash.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QApplication, QSplashScreen 2 | from PySide6.QtGui import QPixmap 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | # Minimal imports; show splash screen 7 | app = QApplication(sys.argv) 8 | pixmap = QPixmap("splash.png") 9 | splash = QSplashScreen(pixmap) 10 | splash.show() 11 | splash.raise_() 12 | splash.activateWindow() 13 | app.processEvents() 14 | 15 | # Now import the kitchen sink and start the app 16 | import mui 17 | window = mui.MainWindow() 18 | window.show() 19 | window.canvas.zoom_to_network() 20 | window.canvas.render() 21 | splash.finish(window) 22 | sys.exit(app.exec()) -------------------------------------------------------------------------------- /fileformat_graphml.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | import Network 4 | 5 | def read_network_from_graphml(filename): 6 | network = Network.Network() 7 | tree = ET.parse(filename) 8 | root = tree.getroot() 9 | 10 | # Get the nodes 11 | for node in root.iter('node'): 12 | name = node.get('id') 13 | x = -1 14 | y = -1 15 | for data in node: 16 | if data.tag=='data' and data.get('key')=='x': x = float(data.text) 17 | if data.tag=='data' and data.get('key')=='y': y = -float(data.text) 18 | if data.tag=='data' and data.get('key')=='label': label = data.text 19 | network.nodes[name] = Network.Node( x, y, name, label ) 20 | 21 | # Get the edges 22 | for edge in root.iter('edge'): 23 | s = network.nodes[edge.get('source')] 24 | t = network.nodes[edge.get('target')] 25 | network.edges.append( add_edge(s,t) ) 26 | 27 | return network 28 | 29 | def add_edge(s,t): 30 | e = Network.Edge(s,t) 31 | s.edges.append(e) 32 | t.edges.append(e) 33 | e.color = '000000' 34 | return e -------------------------------------------------------------------------------- /dialog_bend_penalty.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QDialog, QHBoxLayout, QSlider, QPushButton, QLabel 2 | from PySide6.QtCore import Qt 3 | 4 | class BendPenaltyDialog(QDialog): 5 | def __init__(self): 6 | super().__init__() 7 | self.setWindowTitle("Bend Penalty") 8 | self.setFixedWidth(400) 9 | 10 | self.layout = QHBoxLayout() 11 | 12 | self.label = QLabel("0.0") 13 | self.layout.addWidget(self.label) 14 | 15 | self.slider = QSlider(Qt.Horizontal) 16 | self.slider.setMinimum(0) 17 | self.slider.setMaximum(40) # 0 to 4 with one decimal place 18 | self.slider.setTickInterval(1) 19 | self.slider.setValue(10) 20 | self.update_label(10) 21 | self.slider.valueChanged.connect(self.update_label) 22 | self.layout.addWidget(self.slider) 23 | 24 | self.ok_button = QPushButton("OK") 25 | self.ok_button.clicked.connect(self.accept) 26 | self.layout.addWidget(self.ok_button) 27 | 28 | self.setLayout(self.layout) 29 | 30 | def update_label(self, value): 31 | self.label.setText(f"{self.get_value()}") 32 | 33 | def get_value(self): 34 | return self.slider.value() / 10.0 -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtGui import QColor, QPen, QBrush 2 | 3 | from Network import * 4 | 5 | # Some global UI state 6 | 7 | hover_node = None 8 | hover_edge = None 9 | hover_empty_port = None 10 | 11 | selected_node = None 12 | selected_edge = None 13 | 14 | ### Pens and brushes 15 | 16 | node_pen = QPen( QColor('black'), 5 ) 17 | node_brush = QBrush( QColor('white') ) 18 | 19 | rose_free_pen = QPen( QColor('lightgray'), 2 ) 20 | rose_free_brush = QBrush( QColor('white') ) 21 | 22 | rose_used_pen = QPen( QColor('black'), 2 ) 23 | rose_used_brush = QBrush( QColor('lightgray') ) 24 | 25 | edge_pen = QPen( QColor('black'), 2 ) 26 | 27 | highlight_brush = QBrush( QColor('orange')) 28 | selected_brush = QBrush( QColor('yellow')) 29 | active_handle_pen = QPen( QColor('black'), 2 ) 30 | 31 | # Parameters for UI geometry 32 | 33 | hover_node_radius = 60 34 | bezier_radius = 20 35 | bezier_cp = 60 36 | rose_radius = 20 37 | handle_radius = 20 38 | 39 | def update_params( view_scale ): 40 | # Set some of the pens widths based on zoom level (so they don't go invisible) 41 | edge_pen.setWidthF( max(4,0.35/view_scale) ) 42 | # Set some of the widget scales based on zoom level (so they don't get too small) 43 | global rose_radius 44 | rose_radius = max( 20, 15/view_scale ) 45 | global handle_radius 46 | handle_radius = max( 6, 4/view_scale ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Algorithmically-Assisted Schematic Transit Map Design: A System and Algorithmic Core for Fast Layout Iteration 2 | 3 | Thomas C. van Dijk, Soeren Terziadis - TU Eindhoven 4 | 5 | ![Teaser image with geographic input, screenshot of the GUI, and three metro map designs.](teaser.png) 6 | 7 | 8 | --- 9 | 10 | *Abstract* - [Demo video](https://youtu.be/Pgm2cPlT_vA) 11 | 12 | London's famous "tube map" is an iconic piece of design and perhaps represents the schematic visualization style most well-known to the general public: its octolinear style has become the de facto standard for transit maps around the world. 13 | Making a good schematic transit map is challenging and labour-intensive, and has attracted the attention of the optimization community. 14 | Much of the literature has focused on mathematically defining an optimal drawing and algorithms to compute one. 15 | However, achieving these "optimal" layouts is computationally challenging, often requiring multiple minutes of runtime. 16 | Crucially, what it means for a map to be good is actually highly dependent on factors that evade a general formal definition, like unique landmarks within the network, the context in which a map will be displayed, and preference of the designer and client. 17 | Rather than attempting to make an algorithm that produces a single high-quality and ready-to-use metro map, we propose it is more fruitful to support rapid layout iteration by a human designer, providing a workflow that enables efficient exploration of a wider range of designs than could be done by hand, and iterating on these designs. 18 | To this end we identify steps in the design process of schematic maps that are tedious to do by hand but are algorithmically feasible and present a framework around a simple linear program that computes network layouts almost instantaneously given a fixed direction for every edge, and let the designer decide these directions. 19 | These decisions are made in a graphical user interface with several interaction methods and a number of quality-of-life features demonstrating the flexibility of the framework; the implementation is available as open source. 20 | 21 | --- 22 | 23 | ## Installation 24 | 25 | It is probably wise to make a virtual environment first. 26 | Then install libraries (mostly `ortools`, `scipy`, `pyside6`) using the `pip` requirements file. 27 | 28 | ```pip install -r requirements.txt``` 29 | 30 | In order to render the map to SVG, you need to install [Loom](https://github.com/ad-freiburg/loom) so that `loom` and `transitmap` are available from the shell. 31 | 32 | ## Running 33 | 34 | Initial startup of the GUI can take a while, so please be patient; run `splash.py`. -------------------------------------------------------------------------------- /fileformat_loom.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import Network 4 | 5 | def read_network_from_loom(filename): 6 | network = Network.Network() 7 | with open(filename) as fp: 8 | edge_staging = [] # don't resolve edges until we have all the nodes 9 | data = json.load(fp) 10 | assert data['type'] == "FeatureCollection" 11 | for feat in data['features']: 12 | geom = feat['geometry'] 13 | if geom['type']=="Point": 14 | prop = feat['properties'] 15 | name = prop['id'] # station_id ? 16 | label = prop.get('station_label','') 17 | assert isinstance(name,str) 18 | assert isinstance(label,str) 19 | x, y = geom['coordinates'] 20 | network.nodes[name] = Network.Node( x, -y, name, label ) 21 | elif geom['type']=="LineString": 22 | prop = feat['properties'] 23 | s = prop['from'] 24 | t = prop['to'] 25 | if len(prop['lines'])==1: 26 | color = prop['lines'][0]['color'] 27 | else: 28 | color = '000000' 29 | edge_staging.append( (s,t,color) ) 30 | 31 | for s,t,color in edge_staging: 32 | s = network.nodes[s] 33 | assert isinstance(s,Network.Node) 34 | t = network.nodes[t] 35 | assert isinstance(t,Network.Node) 36 | e = add_edge(s,t) 37 | e.color = color 38 | network.edges.append( e ) 39 | 40 | return network, data 41 | 42 | def add_edge(s,t): 43 | e = Network.Edge(s,t) 44 | s.edges.append(e) 45 | t.edges.append(e) 46 | return e 47 | 48 | def export_loom( net, data ): 49 | # Put the layout from the Network into Loom's filedata, 50 | # so that when we run loom on it, it has our positions and bends. 51 | 52 | scale = 2e-5 # Scale the coordinates to play nice with Loom's assumptions 53 | for feat in data['features']: 54 | geom = feat['geometry'] 55 | if geom['type']=="Point": 56 | prop = feat['properties'] 57 | name = prop['id'] 58 | v = net.nodes[name] 59 | geom['coordinates'] = [scale*v.pos.x(), -scale*v.pos.y() ] 60 | if geom['type']=="LineString": 61 | prop = feat['properties'] 62 | s = net.nodes[ prop['from'] ] 63 | t = net.nodes[ prop['to'] ] 64 | geom = feat['geometry'] 65 | bend = [] 66 | for e in s.edges: 67 | if e.v[0]==t or e.v[1]==t: 68 | if e.bend: 69 | bend = [[scale*e.bend.x(),-scale*e.bend.y()]] 70 | break 71 | geom['coordinates'] = [[scale*s.pos.x(),-scale*s.pos.y()]]+bend+[[scale*t.pos.x(),-scale*t.pos.y()]] 72 | 73 | with open('render.json','w') as fp: 74 | json.dump(data,fp) 75 | 76 | def render_loom( fname_in, fname_out ): 77 | import subprocess 78 | cmd = f"cat {fname_in} | loom | transitmap -l > {fname_out}" 79 | subprocess.Popen(cmd,shell=True) -------------------------------------------------------------------------------- /assign.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | from time import perf_counter 3 | 4 | from mui import logline 5 | 6 | from Network import * 7 | 8 | ### GEOGRAPHIC COST CALCULATIONS ### 9 | 10 | from numpy import matrix 11 | def angle_error( a, b ): 12 | diff = abs(a-b) % (2*pi) 13 | return min(diff, 2*pi-diff) 14 | def cost_matrix( v ): 15 | port_angles = [ i*(pi/4) for i in range(8) ] 16 | edge_angles = [ e.geo_angle(v) for e in v.edges ] 17 | return matrix( [ [ angle_error(pa,ea)**2 for pa in port_angles ] for ea in edge_angles ] ) 18 | 19 | ### ROUNDING ### 20 | 21 | def assign_by_rounding( net ): 22 | # the way it is implemented now, we can mess up the rotation system unnecessarily 23 | net.evict_all_edges() 24 | for v in net.nodes.values(): 25 | for e in v.edges: 26 | port = round_angle_to_port(e.geo_angle(v)) 27 | v.assign( e, port, force=False ) 28 | 29 | 30 | ### MATCHING ### 31 | 32 | from scipy.optimize import linear_sum_assignment 33 | def assign_by_local_matching( net ): 34 | net.evict_all_edges() 35 | for v in net.nodes.values(): 36 | costs = cost_matrix(v) 37 | _, cols = linear_sum_assignment(costs) 38 | for i,p in enumerate(cols): 39 | v.assign( v.edges[int(i)], int(p) ) 40 | 41 | 42 | ### INTEGER LINEAR PROGRAMMING ### 43 | 44 | from ortools.linear_solver import pywraplp as lp 45 | def assign_by_ilp( net, bend_cost=1 ): 46 | 47 | # bend cost is relative to squared angle errors 48 | 49 | solver = lp.Solver.CreateSolver("SCIP") 50 | start = perf_counter() 51 | objective = solver.Sum([]) 52 | portvars = dict() 53 | for v in net.nodes.values(): 54 | costs = cost_matrix(v) 55 | for i,e in enumerate(v.edges): 56 | my_portvars = [solver.BoolVar(f'pass_{v.name}_{i}_{p}') for p in range(8)] 57 | for p in range(8): 58 | objective += costs[i,p] * my_portvars[p] 59 | # pick exactly one port for an edge 60 | solver.Add( solver.Sum(my_portvars)==1 ) 61 | portvars[(v,e)] = my_portvars 62 | for p in range(8): 63 | # assign at most one edge to a port 64 | solver.Add( solver.Sum([ portvars[(v,e)][p] for e in v.edges ]) <= 1 ) 65 | 66 | # consistent port assignment by identifying opposite sides of the same edge 67 | for e in net.edges: 68 | for p in range(8): 69 | solver.Add( portvars[(e.v[0],e)][p] == portvars[(e.v[1],e)][opposite_port(p)] ) 70 | 71 | # bend penalty 72 | for v in net.nodes.values(): 73 | if len(v.edges)==2: 74 | penalty = solver.BoolVar(f'bend_{v.name}') 75 | objective += bend_cost*penalty 76 | e = v.edges[0] 77 | f = v.edges[1] 78 | for p in range(8): 79 | solver.Add( penalty >= portvars[(v,e)][p] - portvars[(v,f)][opposite_port(p)]) 80 | 81 | solver.Minimize(objective) 82 | status = solver.Solve() 83 | runtime = perf_counter()-start 84 | logline( "pa-ilp\tPort assignment ILP runtime (s)\t" + str(runtime) ) 85 | print( 'Port assignment ILP runtime', runtime, 's' ) 86 | print( 'Solver status', status ) 87 | if status==0: 88 | net.evict_all_edges() 89 | for (v,e), x in portvars.items(): 90 | for p in range(8): 91 | if x[p].solution_value()>0.5: 92 | v.assign(e,p) 93 | else: 94 | print( 'Port assignment ILP infeasible' ) 95 | logline( "stats\tPort assignment ILP infeasible" ) 96 | -------------------------------------------------------------------------------- /render.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtGui import QColor, QPainterPath, QPen, QFont 2 | from PySide6.QtCore import Qt 3 | 4 | from Network import * 5 | from math import sqrt 6 | 7 | import ui 8 | 9 | diag = 1/sqrt(2) # notational convenience 10 | 11 | port_offset = [ QPointF(-1,0) 12 | , QPointF(-diag,diag) 13 | , QPointF(0,1) 14 | , QPointF(diag,diag) 15 | , QPointF(1,0) 16 | , QPointF(diag,-diag) 17 | , QPointF(0,-1) 18 | , QPointF(-diag,-diag) 19 | ] 20 | 21 | font = QFont("Helvetica", 30, QFont.Bold) 22 | 23 | def render_network( painter, net, show_labels ): 24 | 25 | # Coordinate system axes 26 | painter.setPen(QPen(QColor('lightgray'),10)) 27 | painter.setFont(font) 28 | painter.drawLine( 0, 0, 100, 0 ) 29 | painter.drawText( 130, 10, "x" ) 30 | painter.drawLine( 0, 0, 0, 100 ) 31 | painter.drawText( 1, 150, "y" ) 32 | 33 | # Draw the edges 34 | for e in net.edges: 35 | ui.edge_pen.setColor(QColor('#'+e.color)) 36 | painter.setPen( ui.edge_pen ) 37 | 38 | painter.setBrush(Qt.NoBrush ) 39 | 40 | a_start = e.v[0].pos 41 | if e.free_at(e.v[0]): 42 | if e.v[0]==ui.hover_node: a_1 = free_edge_handle_position(e.v[0],e) 43 | else: a_1 = e.v[0].pos + (ui.bezier_radius*e.direction(e.v[0])).toPointF() 44 | a_2 = e.v[0].pos + (ui.bezier_cp*e.direction(e.v[0])).toPointF() 45 | else: 46 | a_1 = e.v[0].pos + ui.bezier_radius*port_offset[e.port[0]] 47 | a_2 = e.v[0].pos + ui.bezier_cp*port_offset[e.port[0]] 48 | 49 | b_start = e.v[1].pos 50 | if e.free_at(e.v[1]): 51 | if e.v[1]==ui.hover_node: b_1 = free_edge_handle_position(e.v[1],e) 52 | else: b_1 = e.v[1].pos + (ui.bezier_radius*e.direction(e.v[1])).toPointF() 53 | b_2 = e.v[1].pos + (ui.bezier_cp*e.direction(e.v[1])).toPointF() 54 | else: 55 | b_1 = e.v[1].pos + ui.bezier_radius*port_offset[e.port[1]] 56 | b_2 = e.v[1].pos + ui.bezier_cp*port_offset[e.port[1]] 57 | 58 | path = QPainterPath() 59 | if e.free_at(e.v[0]): 60 | path.moveTo( a_1 ) 61 | else: 62 | path.moveTo( a_start ) 63 | path.lineTo( a_1 ) 64 | if e.bend is None: path.cubicTo( a_2, b_2, b_1 ) 65 | else: 66 | path.lineTo( e.bend ) 67 | path.lineTo( b_1 ) 68 | if not e.free_at(e.v[1]): 69 | path.lineTo( b_start) 70 | painter.drawPath(path) 71 | 72 | # Draw UI for the node close to the mouse 73 | if ui.hover_node: 74 | draw_rose( painter, ui.hover_node ) 75 | for e in ui.hover_node.edges: 76 | if e.free_at(ui.hover_node): 77 | painter.setPen( ui.rose_used_pen) 78 | painter.setBrush( ui.rose_used_brush ) 79 | if e==ui.selected_edge: painter.setBrush( ui.selected_brush ) 80 | if e==ui.hover_edge: painter.setBrush( ui.highlight_brush ) 81 | handle_pos = free_edge_handle_position(ui.hover_node, e) 82 | painter.drawEllipse(handle_pos,ui.handle_radius,ui.handle_radius) 83 | 84 | # Draw the nodes 85 | painter.setPen(ui.node_pen) 86 | painter.setBrush(ui.node_brush) 87 | for name, v in net.nodes.items(): 88 | painter.drawEllipse(v.pos, 10, 10) 89 | if show_labels: painter.drawText( v.pos + QPointF(ui.bezier_radius,10), v.name ) 90 | 91 | 92 | def handle_position( v, p ): 93 | return v.pos + ui.rose_radius*port_offset[p] 94 | 95 | def free_edge_handle_position( v, e ): 96 | dir = e.direction(v).toPointF() 97 | return v.pos + 2*ui.rose_radius*dir 98 | 99 | def is_hovered( v, i ): 100 | if v!=ui.hover_node: return False 101 | if v.ports[i] is None and i==ui.hover_empty_port: return True 102 | return ui.hover_edge is not None and v.ports[i]==ui.hover_edge 103 | 104 | def draw_rose( painter, v: Node ): 105 | ui.rose_free_pen.setCosmetic(True) 106 | ui.rose_used_pen.setCosmetic(True) 107 | ui.active_handle_pen.setCosmetic(True) 108 | for i in range(8): 109 | if v.ports[i] is None: 110 | painter.setPen(ui.rose_free_pen) 111 | painter.setBrush(ui.rose_free_brush) 112 | else: 113 | painter.setPen(ui.rose_used_pen) 114 | painter.setBrush(ui.rose_used_brush) 115 | if ui.selected_node is not None: 116 | painter.setPen( ui.active_handle_pen ) 117 | if ui.selected_node==v and ui.selected_edge is not None and ui.selected_edge==v.ports[i]: 118 | painter.setBrush(ui.selected_brush) 119 | if is_hovered( v, i ): 120 | painter.setBrush(ui.highlight_brush) 121 | 122 | painter.drawEllipse( handle_position(v,i), ui.handle_radius, ui.handle_radius ) 123 | -------------------------------------------------------------------------------- /Network.py: -------------------------------------------------------------------------------- 1 | from math import inf, atan2, pi 2 | 3 | from PySide6.QtCore import QPointF 4 | from PySide6.QtGui import QVector2D 5 | 6 | def opposite_port( p ): 7 | return (p+4)%8 8 | 9 | class Network: 10 | def __init__(self): 11 | self.nodes = {} 12 | self.edges = [] 13 | 14 | def clone(self): 15 | other = Network() 16 | node_clones = dict() 17 | for k,v in self.nodes.items(): 18 | other_v = Node(v.pos.x(), v.pos.y(), v.name, v.label) 19 | node_clones[v] = other_v 20 | other_v.geo_pos = v.geo_pos 21 | other.nodes[k] = other_v 22 | edge_clones = dict() 23 | for e in self.edges: 24 | a = other.nodes[ e.v[0].name ] 25 | b = other.nodes[ e.v[1].name ] 26 | other_e = Edge(a,b) 27 | other_e.color = e.color 28 | edge_clones[e] = other_e 29 | a.edges.append( other_e ) 30 | b.edges.append( other_e ) 31 | other.edges.append( other_e ) 32 | other_e.bend = e.bend 33 | other_e.port = e.port[:] # new copy of list 34 | for v in self.nodes.values(): 35 | node_clones[v].ports = [ edge_clones.get(e,None) for e in v.ports ] 36 | return other 37 | 38 | def scale_by_shortest_edge( self, lb ): 39 | min_length = min([ e.geo_vector(e.v[0]).length() for e in self.edges ]) 40 | factor = lb/min_length 41 | for v in self.nodes.values(): 42 | v.pos = factor * v.pos 43 | v.geo_pos = factor * v.geo_pos 44 | 45 | def evict_all_edges(self): 46 | for v in self.nodes.values(): 47 | for e in v.edges: 48 | v.try_evict(e) 49 | 50 | class Node: 51 | def __init__(self, x, y, name: str, label:str = "" ): 52 | self.pos = QPointF(x,y) 53 | self.geo_pos = self.pos 54 | self.name = name 55 | self.label = label 56 | self.edges = [] 57 | self.ports = [None]*8 58 | 59 | def set_position( self, x, y ): 60 | self.pos = QPointF(x,y) 61 | 62 | def neighbors(self): 63 | return [e.other(self) for e in self.edges] 64 | 65 | def sort_edges_by_geo(self): 66 | self.edges.sort(key=lambda e: e.geo_angle(self)) 67 | def sort_edges(self): 68 | self.edges.sort(key=lambda e: e.angle(self)) 69 | 70 | 71 | def assign(self, e, i, force=False) -> bool: 72 | if self.ports[i] is not None: 73 | if force: self.evict(self.ports[i]) 74 | else: return False 75 | me = e.id(self) 76 | old_port = e.port[me] 77 | if old_port is not None: self.ports[old_port] = None 78 | e.port[me] = i 79 | self.ports[i] = e 80 | return True 81 | 82 | def assign_both_ends( self, e, i, force=False ): 83 | self.assign(e,i,force) 84 | e.other(self).assign( e, opposite_port(i), force ) 85 | 86 | def evict( self, e ): 87 | me = e.id(self) 88 | assert self.ports[e.port[me]] == e 89 | self.ports[e.port[me]] = None 90 | e.port[me] = None 91 | e.bend = None 92 | 93 | def try_evict( self, e ): 94 | if not e.free_at(self): self.evict(e) 95 | 96 | 97 | def straighten_deg2( self, e ): 98 | port = e.port[e.id(self)] 99 | v = e.other(self) 100 | while len(v.edges)==2: 101 | if v==self: break # loop? 102 | prev_e = e 103 | e = v.edges[0] if v.edges[0]!=prev_e else v.edges[1] 104 | v.assign_both_ends(e,port,force=True) 105 | v = e.other(v) 106 | 107 | def is_straight_through( self ): 108 | if len(self.edges)==2: 109 | a = self.edges[0].port_at(self) 110 | b = self.edges[1].port_at(self) 111 | return a==opposite_port(b) 112 | 113 | def is_right_angle( self ): 114 | if len(self.edges)==2: 115 | a = self.edges[0].port_at(self) 116 | b = self.edges[1].port_at(self) 117 | return (a+2)%8==b or (b+2)%8==a 118 | else: return False 119 | 120 | def smoothen( self ): 121 | if self.is_right_angle(): 122 | a = self.edges[0].port_at(self) 123 | b = self.edges[1].port_at(self) 124 | if (a+2)%8==b: a = (a-1)%8 125 | else: a = (a+1)%8 126 | self.assign(self.edges[0],a) 127 | self.assign(self.edges[1],opposite_port(a)) 128 | else: return False 129 | 130 | 131 | class Edge: 132 | def __init__(self, a, b): 133 | self.v = [a,b] 134 | self.port = [None,None] 135 | self.bend = None 136 | self.color = '000000' 137 | 138 | def id(self,v): 139 | if self.v[0]==v: return 0 140 | if self.v[1]==v: return 1 141 | assert False 142 | def other(self, v): 143 | if self.v[0]==v: return self.v[1] 144 | if self.v[1]==v: return self.v[0] 145 | assert False 146 | def port_at(self, v): 147 | if self.v[0]==v: return self.port[0] 148 | if self.v[1]==v: return self.port[1] 149 | assert False 150 | 151 | def free_at(self,v): 152 | return self.port[self.id(v)]==None 153 | 154 | def direction(self,v): 155 | return QVector2D(self.v[1-self.id(v)].pos - v.pos).normalized() 156 | def geo_direction(self,v): 157 | return QVector2D(self.v[1-self.id(v)].geo_pos - v.geo_pos).normalized() 158 | 159 | def vector(self,v): 160 | return QVector2D(self.v[1-self.id(v)].pos - v.pos) 161 | def geo_vector(self,v): 162 | return QVector2D(self.v[1-self.id(v)].geo_pos - v.geo_pos) 163 | 164 | # CCW angles, start at 0 = left 165 | def angle(self,v): 166 | dir = self.vector(v) 167 | return pi-atan2(dir.y(),dir.x()) 168 | def geo_angle(self,v): 169 | dir = self.geo_vector(v) 170 | return pi-atan2(dir.y(),dir.x()) 171 | 172 | def consistent_ports(self): 173 | return self.port[0]==opposite_port(self.port[1]) 174 | 175 | def round_angle_to_port(angle): 176 | return int(((angle+pi/8)%(2*pi))/(pi/4)) -------------------------------------------------------------------------------- /layout.py: -------------------------------------------------------------------------------- 1 | from math import inf, sqrt, pi 2 | from time import perf_counter 3 | 4 | from Network import * 5 | 6 | from mui import logline 7 | 8 | from ortools.linear_solver import pywraplp as lp 9 | 10 | diag = 1/sqrt(2) # notational convenience 11 | 12 | # How long must the distance from station to station be? 13 | # in the "short" and "long" cases? 14 | min_dist = 100 15 | # Factor for how long the distance to bend must be 16 | # in the "short" and "long" cases 17 | bend_short = 0.5 18 | bend_long = 1 19 | 20 | def layout_lp( net, stable_node:Node = None ): 21 | 22 | 23 | start = perf_counter() 24 | solver = lp.Solver.CreateSolver('GLOP') 25 | 26 | if stable_node: 27 | # Track where the "stable node" was before 28 | old_stable_pos = stable_node.pos 29 | 30 | objective = solver.Sum([]) 31 | for v in net.nodes.values(): 32 | v.xvar = solver.NumVar(0,solver.infinity(), v.name+'_x') 33 | v.yvar = solver.NumVar(0,solver.infinity(), v.name+'_y') 34 | 35 | # Layout constraints and length minimization 36 | for e in net.edges: 37 | # Clear bends 38 | e.bend = None 39 | # Add direction and distance constraint: 40 | # - from any assigned endpoint of the edge 41 | # - or don't, if both sides unassigned 42 | if e.port[0] is None: 43 | if e.port[1] is None: 44 | continue # Unconstrained edge 45 | else: 46 | # Edge is assigned at v1 47 | objective += edge_constraint( solver, objective, e.v[1], e.port[1], e.v[0], min_dist ) 48 | else: 49 | if e.port[1] is None: 50 | # Edge is assigned at v0 51 | objective += edge_constraint( solver, objective, e.v[0], e.port[0], e.v[1], min_dist ) 52 | else: 53 | # Edge is assigned at both ends; could have a bend 54 | if e.port[0]==opposite_port(e.port[1]): 55 | # No bend; do arbitrary direction 56 | objective += edge_constraint( solver, objective, e.v[0], e.port[0], e.v[1], min_dist ) 57 | else: 58 | # Bend 59 | e.bend = Node(0,0,f"bend-{e.v[0].name}-{e.v[1].name}") 60 | e.bend.xvar = solver.NumVar(0,solver.infinity(), v.name+'_x') 61 | e.bend.yvar = solver.NumVar(0,solver.infinity(), v.name+'_y') 62 | objective += edge_constraint( solver, objective, e.v[0], e.port[0], e.bend, min_dist*bend_length( e, 0 ) ) 63 | objective += edge_constraint( solver, objective, e.v[1], e.port[1], e.bend, min_dist*bend_length( e, 1 ) ) 64 | 65 | # Space the stations on degree 2 paths 66 | seen = dict() 67 | for v in net.nodes.values(): 68 | if v in seen: continue 69 | if is_straight_deg2(v): 70 | seen[id(v)] = True 71 | path1 = spacewalk( v.edges[0].other(v), v, seen ) 72 | path2 = spacewalk( v.edges[1].other(v), v, seen ) 73 | walk = path1 + [v] + [v for v in reversed(path2)] 74 | spacevar = solver.NumVar(0,solver.infinity(),name=f"{v.name}-spacer") 75 | objective += spacevar 76 | for a, b in zip(walk,walk[1:]): 77 | solver.Add( a.xvar-b.xvar <= spacevar ) 78 | solver.Add( b.xvar-a.xvar <= spacevar ) 79 | solver.Add( a.yvar-b.yvar <= spacevar ) 80 | solver.Add( b.yvar-a.yvar <= spacevar ) 81 | 82 | 83 | # Solve the LP 84 | solver.Minimize( objective ) 85 | status = solver.Solve() 86 | if status==lp.Solver.OPTIMAL: 87 | runtime = perf_counter()-start 88 | logline( "layout\tLayout LP runtime (s)\t" + str(runtime) ) 89 | print( "Layout LP runtime",perf_counter()-start,"s") 90 | for v in net.nodes.values(): 91 | v.set_position( v.xvar.solution_value(), v.yvar.solution_value() ) 92 | del(v.xvar) 93 | del(v.yvar) 94 | for e in net.edges: 95 | if e.bend is not None: 96 | # Bend was a Node for solving; reduce it to a point 97 | e.bend = QPointF( e.bend.xvar.solution_value(), e.bend.yvar.solution_value() ) 98 | 99 | if stable_node is not None: return stable_node.pos - old_stable_pos 100 | else: return True 101 | else: 102 | logline( "stats\tlayout failed with status "+str(status)) 103 | print(status) 104 | print('OPTIMAL', status==lp.Solver.OPTIMAL) 105 | print('UNBOUNDED', status==lp.Solver.UNBOUNDED) 106 | print('INFEASIBLE', status==lp.Solver.INFEASIBLE) 107 | for v in net.nodes.values(): 108 | del(v.xvar) 109 | del(v.yvar) 110 | for e in net.edges: 111 | e.bend = None # clear bends 112 | return False 113 | 114 | 115 | def edge_constraint( solver, objective, a, port, b, min_dist ): 116 | match port: 117 | case 0: # W 118 | solver.Add( a.yvar == b.yvar ) 119 | solver.Add( b.xvar <= a.xvar - min_dist ) 120 | return a.xvar - b.xvar 121 | case 1: # SW 122 | solver.Add( a.xvar+a.yvar == b.xvar+b.yvar ) 123 | solver.Add( b.xvar <= a.xvar - diag*min_dist ) 124 | return 2*diag*a.xvar - 2*diag*b.xvar 125 | case 2: # S 126 | solver.Add( a.xvar == b.xvar ) 127 | solver.Add( b.yvar >= a.yvar + min_dist ) 128 | return b.yvar - a.yvar 129 | case 3: # SE 130 | solver.Add( a.xvar-a.yvar == b.xvar-b.yvar ) 131 | solver.Add( b.xvar >= a.xvar + diag*min_dist ) 132 | return 2*diag*b.xvar - 2*diag*a.xvar 133 | case 4: # E 134 | solver.Add( a.yvar == b.yvar ) 135 | solver.Add( b.xvar >= a.xvar + min_dist ) 136 | return b.xvar - a.xvar 137 | case 5: # NE 138 | solver.Add( a.xvar+a.yvar == b.xvar+b.yvar ) 139 | solver.Add( b.xvar >= a.xvar + diag*min_dist ) 140 | return 2*diag*b.xvar - 2*diag*a.xvar 141 | case 6: # N 142 | solver.Add( a.xvar == b.xvar ) 143 | solver.Add( b.yvar <= a.yvar - min_dist ) 144 | return a.yvar - b.yvar 145 | case 7: # NW 146 | solver.Add( a.xvar-a.yvar == b.xvar-b.yvar ) 147 | solver.Add( b.xvar <= a.xvar - diag*min_dist ) 148 | return 2*diag*a.xvar - 2*diag*b.xvar 149 | 150 | long_bends = { (1,1), (2,1), (3,1) 151 | , (1,2), (2,2) 152 | , (3,1) 153 | , (7,1) 154 | } 155 | def bend_length( e, i ): 156 | #return bend_long 157 | vertex_free = free_angle( e.v[i], e.port[i] ) 158 | bend_free = bend_angle( e.port[0], e.port[1] ) 159 | is_long = (bend_free,vertex_free) in long_bends 160 | return bend_long if is_long else bend_short 161 | 162 | def free_angle( v, p ): 163 | return min( num_free_ports(v,p,1), num_free_ports(v,p,-1) ) 164 | 165 | def num_free_ports( v, p, step ): 166 | p = (p+step)%8 167 | free = 1 168 | while v.ports[p] is None and free<8: 169 | p = (p+step)%8 170 | free += 1 171 | return free 172 | 173 | def bend_angle( p, q ): 174 | return min( (p-q)%8, (q-p)%8 ) 175 | 176 | 177 | def is_straight_deg2(v): 178 | return len(v.edges)==2 and v.edges[0].port_at(v)==opposite_port(v.edges[1].port_at(v)) 179 | 180 | def spacewalk( v, prev, seen ): 181 | # Find maximal degree 2 path for spacer variable 182 | seen[v] = True 183 | walk = [] 184 | if is_straight_deg2(v): 185 | # degree 2, straight through, no bends 186 | v0 = v.edges[0].other(v) 187 | v1 = v.edges[1].other(v) 188 | next = v0 if v1==prev else v1 189 | if not id(next) in seen: 190 | walk = spacewalk( next, v, seen ) 191 | walk.append(v) 192 | return walk 193 | 194 | -------------------------------------------------------------------------------- /mui.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from math import inf, pow 3 | import datetime 4 | 5 | def timestring(): 6 | return datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') 7 | logfile = open(f"mooey-log-{timestring()}.txt", "w") 8 | print("o hai", file=logfile) 9 | def logline( msg ): 10 | print( f"{datetime.datetime.now().strftime('%Y-%m-%d\t%H:%M:%S')}\t{msg}", file=logfile, flush=True ) 11 | 12 | from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QSizePolicy, QFrame, QLabel, QCheckBox, QMenu, QMessageBox, QFileDialog, QDialog 13 | from PySide6.QtGui import QPainter, QPixmap, QColor, Qt, QTransform, QVector2D, QAction, QKeySequence 14 | from PySide6.QtCore import QPointF, QEvent 15 | 16 | import render 17 | import ui 18 | 19 | from Network import opposite_port 20 | 21 | from assign import assign_by_rounding, assign_by_local_matching, assign_by_ilp 22 | from layout import layout_lp 23 | 24 | from fileformat_graphml import read_network_from_graphml 25 | from fileformat_loom import read_network_from_loom, export_loom, render_loom 26 | 27 | from dialog_bend_penalty import BendPenaltyDialog 28 | 29 | min_edge_scale = 80 30 | 31 | class Canvas(QWidget): 32 | def __init__(self): 33 | super().__init__() 34 | self.pixmap = QPixmap( self.size() ) 35 | self.pixmap.fill( QColor('white') ) 36 | self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 37 | self.setMouseTracking(True) 38 | self.grabGesture(Qt.PinchGesture) 39 | 40 | # history buffer 41 | self.history = [] 42 | self.history_index = -1 43 | 44 | # UI state 45 | self.old_mouse = None 46 | self.view = QTransform() 47 | 48 | # load a network 49 | filename = 'loom-examples/wien.json' 50 | self.network, self.filedata = read_network_from_loom(filename) 51 | self.network.scale_by_shortest_edge( min_edge_scale ) 52 | 53 | 54 | def zoom_to_network(self): 55 | min_x, min_y = inf, inf 56 | max_x, max_y = -inf, -inf 57 | for name, v in self.network.nodes.items(): 58 | min_x = min(min_x, v.pos.x()) 59 | min_y = min(min_y, v.pos.y()) 60 | max_x = max(max_x, v.pos.x()) 61 | max_y = max(max_y, v.pos.y()) 62 | x_scale = (0.9*self.width()) / (max_x - min_x) 63 | y_scale = (0.9*self.height()) / (max_y - min_y) 64 | scale = min(x_scale, y_scale) 65 | self.view = QTransform() 66 | self.view.translate(0.05*self.width(), 0.05*self.height()) 67 | self.view.scale(scale, scale) 68 | self.view.translate(-min_x, -min_y) 69 | 70 | def worldspace(self, pos): 71 | return self.view.inverted()[0].map(QPointF(pos)) 72 | 73 | 74 | def resizeEvent(self, event): 75 | if self.size() != self.pixmap.size(): 76 | new_pixmap = QPixmap(self.size()) 77 | new_pixmap.fill(QColor('white')) 78 | painter = QPainter(new_pixmap) 79 | self.render() 80 | painter.drawPixmap(0, 0, self.pixmap) 81 | self.pixmap = new_pixmap 82 | 83 | def paintEvent(self, event): 84 | painter = QPainter(self) 85 | painter.drawPixmap(0, 0, self.pixmap) 86 | 87 | def render(self): 88 | #self.network.clone() 89 | painter = QPainter(self.pixmap) 90 | painter.setRenderHint(QPainter.Antialiasing) 91 | # viewport 92 | painter.setTransform(self.view) 93 | # draw 94 | self.pixmap.fill( QColor('white') ) 95 | ui.update_params( self.view.m11() ) # element [1,1] of the view matrix is scale in our case 96 | render.render_network(painter, self.network, self.show_labels.isChecked() ) 97 | self.update() 98 | 99 | def mousePressEvent(self, event): self.handle_mouse(event,press=True) 100 | def mouseReleaseEvent(self, event): self.handle_mouse(event,release=True) 101 | def mouseMoveEvent(self, event): self.handle_mouse(event) 102 | def mouseDoubleClickEvent(self, event): self.handle_mouse(event,doubleclick=True) 103 | def handle_mouse(self,event, press=False, release=False, doubleclick=False): 104 | 105 | pos = self.worldspace(event.position()) 106 | if self.old_mouse is None: self.old_mouse = event.position() 107 | 108 | ### What is the mouse pointer close to? 109 | 110 | ui.hover_node = None 111 | ui.hover_edge = None 112 | ui.hover_empty_port = None 113 | closest_dist = ui.hover_node_radius 114 | for v in self.network.nodes.values(): 115 | dist = QVector2D(v.pos - pos).length() 116 | if dist < closest_dist: 117 | closest_dist = dist 118 | ui.hover_node = v 119 | if ui.hover_node: 120 | closest_dist = ui.handle_radius 121 | # consider the rose 122 | for i in range(8): 123 | dist = QVector2D(render.handle_position(ui.hover_node,i) - pos).length() 124 | if dist < closest_dist: 125 | closest_dist = dist 126 | if ui.hover_node.ports[i] is None: 127 | ui.hover_edge = None 128 | ui.hover_empty_port = i 129 | else: 130 | ui.hover_edge = ui.hover_node.ports[i] 131 | ui.hover_empty_port = None 132 | # consider free edges 133 | for e in ui.hover_node.edges: 134 | if e.free_at(ui.hover_node): 135 | dist = QVector2D(render.free_edge_handle_position(ui.hover_node,e) - pos).length() 136 | if dist < closest_dist: 137 | closest_dist = dist 138 | ui.hover_edge = e 139 | ui.hover_empty_port = None 140 | 141 | # for undo message: what did we change about the network, if anything? 142 | network_change = None 143 | 144 | ### recognize which actions, if any, to trigger based on this mouse event 145 | 146 | self.mouse_pos = pos 147 | if event.buttons() == Qt.MiddleButton: 148 | # drag view 149 | drag = (event.position() - self.old_mouse) / self.view.m11() # account for view scale 150 | self.view.translate( drag.x(), drag.y() ) 151 | 152 | if event.buttons() == Qt.RightButton: 153 | if ui.hover_edge: 154 | # Context menu when right-clicking a handle 155 | menu = QMenu(self) 156 | if not ui.hover_edge.free_at(ui.hover_node): 157 | straighten = menu.addAction("Straighten") 158 | menu.addSeparator() 159 | evict = menu.addAction("Evict") 160 | action = menu.exec(self.mapToGlobal(event.position().toPoint())) 161 | if action == evict: 162 | assert ui.hover_node is not None 163 | assert ui.hover_edge is not None 164 | ui.hover_node.try_evict(ui.hover_edge) 165 | network_change = f'Evict at "{ui.hover_node.label}" toward "{ui.hover_edge.other(ui.hover_node.name).label}"' 166 | if action == straighten: 167 | assert ui.hover_node is not None 168 | assert ui.hover_edge is not None 169 | ui.hover_node.straighten_deg2(ui.hover_edge) 170 | network_change = f'Straighten from "{ui.hover_node.label}" toward "{ui.hover_edge.other(ui.hover_node).label}" (context menu)' 171 | elif ui.hover_node is not None and ui.hover_empty_port is None: 172 | if ui.hover_node.is_right_angle(): 173 | menu = QMenu(self) 174 | smoothen = menu.addAction("Smoothen") 175 | action = menu.exec(self.mapToGlobal(event.position().toPoint())) 176 | if action == smoothen: 177 | ui.hover_node.smoothen() 178 | network_change = f'Smoothen "{ui.hover_node.label}"' 179 | if False and ui.hover_node.is_straight_through(): 180 | v = ui.hover_node 181 | if (not v.edges[0].consistent_ports()) ^ (not v.edges[1].consistent_ports()): 182 | menu = QMenu(self) 183 | bump = menu.addAction("Bump") 184 | action = menu.exec(self.mapToGlobal(event.position().toPoint())) 185 | if action == bump: 186 | from_id = 1 if v.edges[0].consistent_ports() else 0 187 | # edge that will become consistent: 188 | from_e = v.edges[from_id] 189 | # where does it come from? 190 | thru_port = from_e.port[1-from_e.id(v)] 191 | print('thru_port',thru_port) 192 | # consistently assign to us 193 | v.assign(from_e,opposite_port(thru_port),True) 194 | from_e.bend = None 195 | # go straight through here 196 | to_e = v.edges[1-from_id] 197 | v.assign(to_e,thru_port,True) 198 | 199 | if press and event.buttons() == Qt.LeftButton: 200 | print( ui.hover_node, ui.hover_edge, ui.hover_empty_port ) 201 | if ui.selected_node is None: 202 | # nothing was selected: select the thing we clicked on 203 | ui.selected_node = ui.hover_node 204 | ui.selected_edge = ui.hover_edge 205 | else: 206 | if ui.hover_node is None or (ui.hover_edge is None and ui.hover_empty_port is None): 207 | # clicked on empty space: deselect 208 | ui.selected_node = None 209 | ui.selected_edge = None 210 | elif ui.hover_node!=ui.selected_node: 211 | # clicked on another node: select that instead 212 | ui.selected_node = ui.hover_node 213 | ui.selected_edge = ui.hover_edge 214 | #else: leave selection alone, maybe mouse release will do something 215 | 216 | if release and ui.selected_edge is not None: 217 | # release mouse and there is a selected handle 218 | if ui.hover_edge is None and ui.hover_empty_port is None: 219 | # released on nothing: deselect 220 | ui.selected_node = None 221 | ui.selected_edge = None 222 | elif ui.selected_edge==ui.hover_edge: 223 | pass # leave it selected? 224 | else: 225 | # we dragged here, or selected first and now click something else 226 | if ui.hover_node==ui.selected_node and ui.hover_empty_port is not None: 227 | # we went from one handle to another handle on the same node. 228 | # assign symmetric / asymmetric based on modifier key. 229 | if event.modifiers() & Qt.ShiftModifier: 230 | ui.selected_node.assign( ui.selected_edge, ui.hover_empty_port, force=True) 231 | else: 232 | ui.selected_node.assign_both_ends( ui.selected_edge, ui.hover_empty_port, force=True ) 233 | network_change = f'Reassign at "{ui.selected_node.label}" - "{ui.selected_edge.other(ui.hover_node).label}" to port {ui.hover_empty_port}' 234 | # we did a thing; don't have a selection anymore 235 | ui.selected_node = None 236 | ui.selected_edge = None 237 | 238 | # double click handles to straighten 239 | if doubleclick and event.buttons() == Qt.LeftButton: 240 | if ui.hover_edge is not None: 241 | ui.hover_node.straighten_deg2( ui.hover_edge ) 242 | network_change = f'Straighten from "{ui.hover_node.label}" toward "{ui.hover_edge.other(ui.hover_node).label}" (double click)' 243 | 244 | 245 | ### Did we do anything? Then solve and render as appropriate, and to undo buffer 246 | 247 | if network_change is not None: 248 | if self.auto_update.isChecked(): 249 | resolve_shift = layout_lp(self.network,ui.hover_node) 250 | if resolve_shift: 251 | self.view.translate(-resolve_shift.x(), -resolve_shift.y()) 252 | if self.auto_render.isChecked(): 253 | export_loom(self.network,self.filedata) 254 | render_loom( "render.json", "render.svg" ) 255 | elif resolve_shift is False: 256 | m = QMessageBox() 257 | m.setText("Failed to realise layout.") 258 | m.setIcon(QMessageBox.Warning) 259 | m.setStandardButtons(QMessageBox.Ok) 260 | m.exec() 261 | self.history_checkpoint( network_change ) 262 | 263 | ### Remember mouse position for next time and redraw. 264 | 265 | self.old_mouse = event.position() 266 | self.render() 267 | 268 | def handle_scale_at(self, mouse_pos, scale): 269 | pos = self.worldspace(mouse_pos) 270 | scaleAt = QTransform( scale,0, 0,scale, (1-scale)*pos.x(), (1-scale)*pos.y() ) 271 | self.view = scaleAt * self.view 272 | 273 | def wheelEvent(self, event): 274 | if event.pixelDelta().manhattanLength() > 0 and event.source()==Qt.MouseEventSource.MouseEventSynthesizedBySystem: 275 | # Actually touchpad pan or 2D scroll 276 | if event.modifiers() & Qt.AltModifier: 277 | # Hold alt to zoom anyway 278 | s = pow( 1.2, event.angleDelta().y()/120 ) 279 | self.handle_scale_at(event.position(), s) 280 | else: 281 | # Actually pan 282 | drag = event.pixelDelta() / self.view.m11() # m11 accounts for view scale 283 | self.view.translate( drag.x(), drag.y() ) 284 | elif event.angleDelta().y() != 0 and event.source()==Qt.MouseEventSource.MouseEventNotSynthesized: 285 | # Actual mouse wheel zoom 286 | s = pow( 1.2, event.angleDelta().y()/120 ) 287 | self.handle_scale_at(event.position(), s) 288 | self.render() 289 | 290 | # Fiddle with some gesture events to make pinch zoom works 291 | def event(self, event): 292 | if event.type() == QEvent.Gesture: 293 | return self.gestureEvent(event) 294 | return super().event(event) 295 | def gestureEvent(self, event): 296 | if pinch := event.gesture(Qt.PinchGesture): 297 | self.handlePinch(pinch) 298 | return True 299 | def handlePinch(self, pinch): 300 | if pinch.state() == Qt.GestureStarted: 301 | pass 302 | elif pinch.state() in (Qt.GestureUpdated, Qt.GestureFinished): 303 | self.handle_scale_at(pinch.centerPoint(), pinch.scaleFactor()) 304 | self.render() 305 | 306 | def open_dialog(self): 307 | file_name, _ = QFileDialog.getOpenFileName(None, 'Open File', '', 'All Files (*)') 308 | if file_name: 309 | if file_name[-8:]==".graphml": 310 | self.network = read_network_from_graphml(file_name) 311 | self.filedata = None 312 | else: 313 | self.network, self.filedata = read_network_from_loom(file_name) 314 | self.network.scale_by_shortest_edge( min_edge_scale ) 315 | self.history_checkpoint( f'Open "{file_name}"' ) 316 | self.zoom_to_network() 317 | 318 | 319 | def history_checkpoint(self, text): 320 | # Log the message 321 | logline( "user\t"+text ) 322 | # Delete the future 323 | self.history = self.history[0:self.history_index+1] 324 | # Add the present 325 | self.history.append(( text, self.network.clone() )) 326 | self.history_index += 1 327 | self.update_history_actions() 328 | 329 | def update_history_actions(self): 330 | # Set the text and availability of the "undo" menu item based on where we are in time now. 331 | if self.history_index<1: 332 | self.undo_action.setEnabled(False) 333 | self.undo_action.setText("Undo") 334 | else: 335 | self.undo_action.setEnabled(True) 336 | self.undo_action.setText( "Undo " + self.history[self.history_index][0] ) 337 | 338 | if self.history_index==len(self.history)-1: 339 | self.redo_action.setEnabled(False) 340 | self.redo_action.setText("Redo") 341 | else: 342 | self.redo_action.setEnabled(True) 343 | self.redo_action.setText( "Redo " + self.history[self.history_index+1][0] ) 344 | 345 | def undo(self): 346 | # Assumes we don't undo to before the start of time 347 | logline("user\t"+"Undo") 348 | self.history_index -= 1 349 | self.fetch_history() 350 | self.update_history_actions() 351 | self.render() 352 | def redo(self): 353 | # Assumes the future exists 354 | logline("user\t"+"Redo") 355 | self.history_index += 1 356 | self.fetch_history() 357 | self.update_history_actions() 358 | self.render() 359 | def fetch_history(self): 360 | self.network = self.history[self.history_index][1].clone() 361 | 362 | def drawing_is_completely_oob(canvas): 363 | # Is any node on the canvas based on the viewport? (Ignores edges.) 364 | rect = canvas.rect() 365 | for v in canvas.network.nodes.values(): 366 | p = canvas.view.map(v.pos).toPoint() 367 | if rect.contains(p): return False 368 | return True 369 | 370 | 371 | class MainWindow(QMainWindow): 372 | def __init__(self): 373 | super().__init__() 374 | self.setWindowTitle("Mooey") 375 | self.setMinimumSize(1280, 720) 376 | central_widget = QWidget() 377 | self.setCentralWidget(central_widget) 378 | layout = QHBoxLayout(central_widget) 379 | button_layout = QVBoxLayout() 380 | button_layout.setAlignment(Qt.AlignTop) 381 | layout.addLayout(button_layout) 382 | self.canvas = Canvas() 383 | construct_sidebar(self,button_layout) 384 | construct_menubar(self) 385 | layout.addWidget(self.canvas) 386 | 387 | self.canvas.history_checkpoint( "Initial drawing" ) 388 | self.canvas.update_history_actions() 389 | 390 | def construct_menubar(window): 391 | menu_bar = window.menuBar() 392 | # File menu 393 | file_menu = menu_bar.addMenu("File") 394 | open_action = QAction("Open...", window) 395 | open_action.setShortcut(QKeySequence('Ctrl+O')) 396 | open_action.triggered.connect(window.canvas.open_dialog) 397 | file_menu.addAction(open_action) 398 | exit_action = QAction("Exit", window) 399 | exit_action.setShortcut(QKeySequence('Ctrl+Q')) 400 | file_menu.addAction(exit_action) 401 | exit_action.triggered.connect(window.close) 402 | # Edit menu 403 | edit_menu = menu_bar.addMenu("Edit") 404 | window.canvas.undo_action = QAction("Undo", window) 405 | edit_menu.addAction(window.canvas.undo_action) 406 | window.canvas.undo_action.setShortcut(QKeySequence('Ctrl+Z')) 407 | window.canvas.undo_action.triggered.connect(window.canvas.undo) 408 | window.canvas.redo_action = QAction("Undo", window) 409 | edit_menu.addAction(window.canvas.redo_action) 410 | window.canvas.redo_action.setShortcut(QKeySequence('Ctrl+Shift+Z')) 411 | window.canvas.redo_action.triggered.connect(window.canvas.redo) 412 | 413 | 414 | def do_zoom_to_fit(window): 415 | window.canvas.zoom_to_network() 416 | window.canvas.render() 417 | 418 | def do_assign_reset(window): 419 | window.canvas.network.evict_all_edges() 420 | window.canvas.history_checkpoint("Evict all") 421 | window.canvas.render() 422 | 423 | def do_assign_round(window): 424 | assign_by_rounding(window.canvas.network) 425 | update_layout_if_auto(window) 426 | window.canvas.history_checkpoint("Assign ports by rounding") 427 | window.canvas.render() 428 | 429 | def do_assign_matching(window): 430 | assign_by_local_matching(window.canvas.network) 431 | update_layout_if_auto(window) 432 | window.canvas.history_checkpoint("Assign ports by matching") 433 | window.canvas.render() 434 | 435 | def do_assign_ilp(window): 436 | bend_cost = 1 437 | dialog = BendPenaltyDialog() 438 | if dialog.exec() == QDialog.Accepted: 439 | bend_cost = dialog.get_value() 440 | assign_by_ilp(window.canvas.network,bend_cost) 441 | update_layout_if_auto(window) 442 | window.canvas.history_checkpoint(f"Assign ports globally (bend cost {bend_cost})") 443 | window.canvas.render() 444 | 445 | def do_layout(window): 446 | if layout_lp(window.canvas.network) is False: 447 | logline( "user\t"+"Failed to realize layout.") 448 | m = QMessageBox() 449 | m.setText("Failed to realize layout.") 450 | m.setIcon(QMessageBox.Warning) 451 | m.setStandardButtons(QMessageBox.Ok) 452 | m.exec() 453 | window.canvas.history_checkpoint("Automated layout") 454 | if drawing_is_completely_oob(window.canvas): 455 | window.canvas.zoom_to_network() 456 | window.canvas.render() 457 | 458 | def update_layout_if_auto(window): 459 | if window.canvas.auto_update.isChecked(): 460 | do_layout(window) 461 | 462 | def do_reset_layout(window): 463 | for v in window.canvas.network.nodes.values(): 464 | v.pos = v.geo_pos 465 | for e in window.canvas.network.edges: 466 | e.bend = None 467 | window.canvas.zoom_to_network() 468 | window.canvas.history_checkpoint("Reset layout") 469 | window.canvas.render() 470 | 471 | def do_render(window,tag=None): 472 | if window.canvas.filedata is None: 473 | m = QMessageBox() 474 | m.setText("Cannot render: opened file was not from Loom.") 475 | m.setIcon(QMessageBox.Warning) 476 | m.setStandardButtons(QMessageBox.Ok) 477 | m.exec() 478 | else: 479 | export_loom( window.canvas.network, window.canvas.filedata ) 480 | if tag is None: filename = "render.svg" 481 | else: filename = f"{timestring()}-render-{tag}.svg" 482 | render_loom( "render.json", filename ) 483 | 484 | def construct_sidebar(window,layout): 485 | group_separator(layout) 486 | layout.addWidget(QLabel("View")) 487 | sidebar_button(layout, "Zoom to fit", lambda:do_zoom_to_fit(window) ) 488 | window.canvas.show_labels = QCheckBox("Show station IDs") 489 | window.canvas.show_labels.setChecked(False) 490 | window.canvas.show_labels.clicked.connect(lambda:window.canvas.render()) 491 | layout.addWidget(window.canvas.show_labels) 492 | 493 | group_separator(layout) 494 | layout.addWidget(QLabel("Port assignment")) 495 | sidebar_button(layout, "Evict all", lambda:do_assign_reset(window)) 496 | sidebar_button(layout, "Rounding", lambda:do_assign_round(window)) 497 | sidebar_button(layout, "Matching", lambda:do_assign_matching(window)) 498 | sidebar_button(layout, "Global...", lambda:do_assign_ilp(window)) 499 | 500 | group_separator(layout) 501 | layout.addWidget(QLabel("Layout")) 502 | sidebar_button(layout, "Update layout", lambda:do_layout(window)) 503 | window.canvas.auto_update = QCheckBox("Auto-update") 504 | window.canvas.auto_update.setChecked(False) 505 | layout.addWidget(window.canvas.auto_update) 506 | sidebar_button(layout, "Reset", lambda:do_reset_layout(window)) 507 | 508 | group_separator(layout) 509 | layout.addWidget(QLabel("Rendering")) 510 | sidebar_button(layout, "Render using Loom", lambda:do_render(window)) 511 | window.canvas.auto_render = QCheckBox("Auto-render") 512 | window.canvas.auto_render.setChecked(False) 513 | layout.addWidget(window.canvas.auto_render) 514 | 515 | if False: 516 | # Checkpoint buttons for user studies 517 | group_separator(layout) 518 | layout.addWidget(QLabel("Log timing")) 519 | sidebar_button( layout, "Start exploration", lambda:(QApplication.beep(),logline("check\tCheckpoint: Start exploration"))) 520 | sidebar_button( layout, "End exploration", lambda:(QApplication.beep(),do_render(window,"task-explore"),logline("check\tCheckpoint: End exploration"))) 521 | sidebar_button( layout, "Start task 1", lambda:(QApplication.beep(),logline("check\tCheckpoint: Start task 1"))) 522 | sidebar_button( layout, "End task 1", lambda:(QApplication.beep(),do_render(window,"task-1"),logline("check\tCheckpoint: End task 1"))) 523 | sidebar_button( layout, "Start task 2", lambda:(QApplication.beep(),logline("check\tCheckpoint: Start task 2"))) 524 | sidebar_button( layout, "End task 2", lambda:(QApplication.beep(),do_render(window,"task-2"),logline("check\tCheckpoint: End task 2"))) 525 | 526 | 527 | def sidebar_button(layout, text, action): 528 | button = QPushButton(text) 529 | button.clicked.connect(action) 530 | layout.addWidget(button) 531 | 532 | def group_separator(layout): 533 | line = QFrame() 534 | line.setFrameShape(QFrame.HLine) 535 | line.setFrameShadow(QFrame.Sunken) 536 | layout.addWidget(line) 537 | 538 | 539 | if __name__ == "__main__": 540 | print("Welcome to Mooey!") 541 | 542 | app = QApplication(sys.argv) 543 | app.setApplicationName("Mooey") 544 | window = MainWindow() 545 | window.show() 546 | window.canvas.zoom_to_network() 547 | window.canvas.render() 548 | app.exec() -------------------------------------------------------------------------------- /freiburg-bug.json: -------------------------------------------------------------------------------- 1 | {"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.013485281374238574, -0.0184142135623731]}, "properties": {"deg": "3", "deg_in": "3", "deg_out": "3", "id": "0xeea7b0"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.006242640687119286, -0.02565685424949239]}, "properties": {"deg": "3", "deg_in": "3", "deg_out": "3", "id": "0xeeadc0", "station_id": "Parent30601", "station_label": "Am Lindenw\u00e4ldle"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.00924264068711929, -0.008171572875253812]}, "properties": {"deg": "1", "deg_in": "1", "deg_out": "1", "id": "0xef5e20", "station_id": "Parent30800", "station_label": "Moosweiher"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.01224264068711929, -0.02565685424949239]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0xef63b0", "station_id": "Parent30604", "station_label": "Scherrerplatz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.016000000000000007]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0xf19570", "station_id": "Parent30204", "station_label": "Hauptstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.012000000000000004]}, "properties": {"deg": "3", "deg_in": "3", "deg_out": "3", "id": "0xf19ce0", "station_id": "Parent30208", "station_label": "Komturplatz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.010656854249492384, -0.015585786437626911]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0xf22bc0", "station_id": "Parent30503", "station_label": "Betzenhauser Torplatz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.020242640687119288, -0.017656854249492385]}, "properties": {"deg": "3", "deg_in": "3", "deg_out": "3", "id": "0xf480c0", "station_id": "Parent30810", "station_label": "Robert-Koch-Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.020242640687119288, -0.012000000000000005]}, "properties": {"deg": "1", "deg_in": "1", "deg_out": "1", "id": "0xf48250", "station_id": "Parent30813", "station_label": "Technische Fakult\u00e4t"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.02448528137423858, -0.021414213562373104]}, "properties": {"deg": "4", "deg_in": "4", "deg_out": "4", "id": "0xf483e0", "station_id": "Parent30106", "station_label": "Stadttheater"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.018242640687119293, -0.02565685424949239]}, "properties": {"deg": "3", "deg_in": "3", "deg_out": "3", "id": "0xf49330", "station_id": "Parent30411", "station_label": "H.-von-Stephan-Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.04531370849898477, -0.024242640687119295]}, "properties": {"deg": "1", "deg_in": "1", "deg_out": "1", "id": "0xf495a0", "station_id": "Parent30300", "station_label": "La\u00dfbergstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.03331370849898477, -0.024242640687119295]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0xf498e0", "station_id": "Parent30306", "station_label": "Maria-Hilf-Kirche"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.0, -0.021414213562373104]}, "properties": {"deg": "1", "deg_in": "1", "deg_out": "1", "id": "0xf4a360", "station_id": "Parent30727", "station_label": "Bollerstaudenstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.020000000000000007]}, "properties": {"deg": "3", "deg_in": "3", "deg_out": "3", "id": "0xf4aad0", "station_id": "Parent30101", "station_label": "Europaplatz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.026485281374238577, -0.021414213562373104]}, "properties": {"deg": "4", "deg_in": "4", "deg_out": "4", "id": "0xf4aeb0", "station_id": "Parent30100", "station_label": "Bertoldsbrunnen"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.02024264068711929, -0.02565685424949239]}, "properties": {"deg": "3", "deg_in": "3", "deg_out": "3", "id": "0xf4b150", "station_id": "Parent30408", "station_label": "Reiterstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.001414213562373095, -0.02848528137423858]}, "properties": {"deg": "1", "deg_in": "1", "deg_out": "1", "id": "0xf4b230", "station_id": "Parent30716", "station_label": "Munzinger Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.016485281374238568, -0.021414213562373104]}, "properties": {"deg": "3", "deg_in": "3", "deg_out": "3", "id": "0xf4b860", "station_id": "Parent30500", "station_label": "Rathaus im St\u00fchlinger"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.0]}, "properties": {"deg": "1", "deg_in": "1", "deg_out": "1", "id": "0xf4c0f0", "station_id": "Parent30430", "station_label": "Gundelfinger Str."}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.0114142135623731, -0.030485281374238577]}, "properties": {"deg": "1", "deg_in": "1", "deg_out": "1", "id": "0xf4c1d0", "station_id": "Parent30424", "station_label": "Innsbrucker Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.03072792206135786, -0.035899494936611674]}, "properties": {"deg": "1", "deg_in": "1", "deg_out": "1", "id": "0xf4c630", "station_id": "Parent30400", "station_label": "Dorfstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.043313708498984776, -0.024242640687119295]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0xf4c930", "station_id": "Parent30301", "station_label": "R\u00f6merhof"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.0228284271247462]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0xf58400", "station_id": "Parent30118", "station_label": "Oberlinden"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.012071067811865479, -0.017000000000000005]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0xf591c0", "station_id": "Parent30502", "station_label": "Am Bischofskreuz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.026485281374238577, -0.02765685424949239]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0xfd0ae0", "station_id": "Parent30405", "station_label": "Lorettostra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.004242640687119285, -0.02565685424949239]}, "properties": {"deg": "3", "deg_in": "3", "deg_out": "3", "id": "0xfd4b30"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.031313708498984766, -0.024242640687119295]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0xfd4d10", "station_id": "Parent30120", "station_label": "Brauerei Ganter"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.026485281374238577, -0.02565685424949239]}, "properties": {"deg": "3", "deg_in": "3", "deg_out": "3", "id": "0xfd5fc0", "station_id": "Parent30104", "station_label": "Johanneskirche"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.02507106781186548, -0.014828427124746196]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1006c70", "station_id": "Parent30206", "station_label": "Rennweg"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.008242640687119288, -0.02565685424949239]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x10071b0", "station_id": "Parent30602", "station_label": "Krozinger Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.026485281374238577, -0.029656854249492392]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1008aa0", "station_id": "Parent30404", "station_label": "Holbeinstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.041313708498984775, -0.024242640687119295]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x100a510", "station_id": "Parent30302", "station_label": "Hasemannstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.00924264068711929, -0.010171572875253814]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x100ef60", "station_id": "Parent30801", "station_label": "Diakoniekrankenhaus"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.02224264068711929, -0.017656854249492385]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x10116c0", "station_id": "Parent30811", "station_label": "Friedrich-Ebert-Platz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.00924264068711929, -0.012171572875253816]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1011a80", "station_id": "Parent30802", "station_label": "Moosgrund"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.0154142135623731, -0.030485281374238577]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1011e00", "station_id": "Parent30651", "station_label": "Paula-Modersohn-Platz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.021656854249492388, -0.024242640687119295]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x10132c0", "station_id": "Parent30123", "station_label": "Mattenstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.026485281374238574, -0.0134142135623731]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x10156c0", "station_id": "Parent30207", "station_label": "Eichstetter Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.029313708498984768, -0.034485281374238584]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1017150", "station_id": "Parent30401", "station_label": "Klosterplatz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.023071067811865485, -0.0228284271247462]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x10172a0", "station_id": "Parent30114", "station_label": "Erbprinzenstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.00924264068711929, -0.014171572875253816]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1017330", "station_id": "Parent30504", "station_label": "Paduaallee"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.03307106781186549]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1018c00", "station_id": "Parent30402", "station_label": "Wiesenweg"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.023656854249492383, -0.01624264068711929]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101a820", "station_id": "Parent30205", "station_label": "Hauptfriedhof"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.016828427124746196, -0.029071067811865484]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101ae50", "station_id": "Parent30131", "station_label": "Peter-Thumb-Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.01624264068711929, -0.02565685424949239]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101b5f0", "station_id": "Parent30616", "station_label": "Pressehaus"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.021818614707571907, -0.021414213562373104]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101c470", "station_id": "Parent30107", "station_label": "Hauptbahnhof"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.011674621202458752, -0.020224873734152923]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101caf0", "station_id": "Parent30505", "station_label": "Bissierstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.018242640687119293, -0.02765685424949239]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101cb80", "station_id": "Parent30415", "station_label": "Weddigenstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.00282842712474619, -0.027071067811865485]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101ce40", "station_id": "Parent30715", "station_label": "VAG-Zentrum"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.00282842712474619, -0.024242640687119295]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101dfb0", "station_id": "Parent30733", "station_label": "Geschwister-Scholl-Platz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.010242640687119288, -0.02565685424949239]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101e270", "station_id": "Parent30603", "station_label": "Dorfbrunnen"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.02589949493661167, -0.020000000000000007]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101f1d0", "station_id": "Parent30105", "station_label": "Fahnenbergplatz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.029313708498984768, -0.024242640687119295]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101f750", "station_id": "Parent30119", "station_label": "Schwabentorbr\u00fccke"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.03531370849898477, -0.024242640687119295]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x101fff0", "station_id": "Parent30305", "station_label": "Alter Messplatz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.017414213562373097, -0.014828427124746196]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x10200b0", "station_id": "Parent30808", "station_label": "Berliner Allee"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.001414213562373095, -0.0228284271247462]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1020760", "station_id": "Parent30734", "station_label": "Maria-von-Rudloff-Platz"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.03931370849898477, -0.024242640687119295]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1020b20", "station_id": "Parent30303", "station_label": "Emil-G\u00f6tt-Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.01424264068711929, -0.02565685424949239]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1021f40", "station_id": "Parent30605", "station_label": "Haslach Bad"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.03731370849898477, -0.024242640687119295]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1022640", "station_id": "Parent30304", "station_label": "Musikhochschule"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.026485281374238577, -0.03165685424949239]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1023070", "station_id": "Parent30403", "station_label": "Wonnhalde"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.01882842712474619, -0.0134142135623731]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1024320", "station_id": "Parent30814", "station_label": "Els\u00e4sser Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.014985281374238572, -0.0199142135623731]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x10245f0", "station_id": "Parent30501", "station_label": "Runzmattenweg"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.026485281374238577, -0.023535533905932746]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x10252a0", "station_id": "Parent30103", "station_label": "Holzmarkt"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.01882842712474619, -0.01624264068711929]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1025e50", "station_id": "Parent30809", "station_label": "Freiburg Killianstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.019151948040905236, -0.021414213562373104]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1026070", "station_id": "Parent30108", "station_label": "Eschholzstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.004]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1027260", "station_id": "Parent30431", "station_label": "Berggasse"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.009863961030678929, -0.022035533905932745]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1028c00", "station_id": "Parent30607", "station_label": "Rohrgraben"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.0134142135623731, -0.030485281374238577]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x10293b0", "station_id": "Parent30423", "station_label": "Vauban-Mitte"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.008]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x1029b30", "station_id": "Parent30201", "station_label": "Tullastra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.010000000000000002]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x102b240", "station_id": "Parent30200", "station_label": "Hornusstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.018000000000000006]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x102bee0", "station_id": "Parent30102", "station_label": "Tennenbacher Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.002]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x102c630", "station_id": "Parent30432", "station_label": "Glottertalstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.006]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x102dc20", "station_id": "Parent30202", "station_label": "Reutebachgasse"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.027899494936611674, -0.014000000000000004]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x102e8d0", "station_id": "Parent30203", "station_label": "Okenstra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [0.008053300858899107, -0.02384619407771257]}, "properties": {"deg": "2", "deg_in": "2", "deg_out": "2", "id": "0x102efc0", "station_id": "Parent30600", "station_label": "Bugginger Stra\u00dfe"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.013485281374238574, -0.0184142135623731], [0.011674621202458752, -0.020224873734152923]]}, "properties": {"dbg_lines": "3", "from": "0xeea7b0", "id": "0x1014f50", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0x101caf0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.013485281374238574, -0.0184142135623731], [0.014985281374238572, -0.0199142135623731]]}, "properties": {"dbg_lines": "1$3", "from": "0xeea7b0", "id": "0x1024680", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}, {"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0x10245f0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.006242640687119286, -0.02565685424949239], [0.004242640687119285, -0.02565685424949239]]}, "properties": {"dbg_lines": "3$5", "from": "0xeeadc0", "id": "0xefe9a0", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}, {"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0xfd4b30"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.00924264068711929, -0.008171572875253812], [0.00924264068711929, -0.010171572875253814]]}, "properties": {"dbg_lines": "1", "from": "0xef5e20", "id": "0x100ee60", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0x100ef60"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.01224264068711929, -0.02565685424949239], [0.010242640687119288, -0.02565685424949239]]}, "properties": {"dbg_lines": "5", "from": "0xef63b0", "id": "0x101f5c0", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0x101e270"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.016000000000000007], [0.027899494936611674, -0.018000000000000006]]}, "properties": {"dbg_lines": "4", "from": "0xf19570", "id": "0xefeab0", "lines": [{"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x102bee0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.012000000000000004], [0.027899494936611674, -0.010000000000000002]]}, "properties": {"dbg_lines": "2$4", "from": "0xf19ce0", "id": "0x1008140", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x102b240"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.012000000000000004], [0.027899494936611674, -0.014000000000000004]]}, "properties": {"dbg_lines": "4", "from": "0xf19ce0", "id": "0x102ea40", "lines": [{"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x102e8d0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.010656854249492384, -0.015585786437626911], [0.012071067811865479, -0.017000000000000005]]}, "properties": {"dbg_lines": "1", "from": "0xf22bc0", "id": "0xfdc7f0", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0xf591c0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.020242640687119288, -0.017656854249492385], [0.016485281374238568, -0.021414213562373104]]}, "properties": {"dbg_lines": "2$4", "from": "0xf480c0", "id": "0xf50e80", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0xf4b860"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.020242640687119288, -0.017656854249492385], [0.02224264068711929, -0.017656854249492385]]}, "properties": {"dbg_lines": "2", "from": "0xf480c0", "id": "0x1014de0", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0x10116c0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.020242640687119288, -0.012000000000000005], [0.01882842712474619, -0.0134142135623731]]}, "properties": {"dbg_lines": "4", "from": "0xf48250", "id": "0x10244a0", "lines": [{"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x1024320"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.02448528137423858, -0.021414213562373104], [0.026485281374238577, -0.021414213562373104]]}, "properties": {"dbg_lines": "1$2$3$4", "from": "0xf483e0", "id": "0xf55310", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}, {"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "f59e00", "id": "0x26b5750", "label": "3"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0xf4aeb0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.02448528137423858, -0.021414213562373104], [0.023071067811865485, -0.0228284271247462]]}, "properties": {"dbg_lines": "5", "from": "0xf483e0", "id": "0x1018770", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0x10172a0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.018242640687119293, -0.02565685424949239], [0.01624264068711929, -0.02565685424949239]]}, "properties": {"dbg_lines": "5", "from": "0xf49330", "id": "0xfcbf60", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0x101b5f0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.03331370849898477, -0.024242640687119295], [0.03531370849898477, -0.024242640687119295]]}, "properties": {"dbg_lines": "1", "from": "0xf498e0", "id": "0x1020bf0", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0x101fff0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.020000000000000007], [0.02589949493661167, -0.020000000000000007]]}, "properties": {"dbg_lines": "5", "from": "0xf4aad0", "id": "0x101f260", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0x101f1d0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.026485281374238577, -0.021414213562373104], [0.027899494936611674, -0.020000000000000007]]}, "properties": {"dbg_lines": "4", "from": "0xf4aeb0", "id": "0xf590f0", "lines": [{"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0xf4aad0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.026485281374238577, -0.021414213562373104], [0.027899494936611674, -0.0228284271247462]]}, "properties": {"dbg_lines": "1", "from": "0xf4aeb0", "id": "0xfe35f0", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0xf58400"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.02024264068711929, -0.02565685424949239], [0.026485281374238577, -0.02565685424949239]]}, "properties": {"dbg_lines": "3", "from": "0xf4b150", "id": "0xf5a390", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0xfd5fc0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.02024264068711929, -0.02565685424949239], [0.018242640687119293, -0.02565685424949239]]}, "properties": {"dbg_lines": "3$5", "from": "0xf4b150", "id": "0xfda630", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}, {"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0xf49330"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.001414213562373095, -0.02848528137423858], [0.00282842712474619, -0.027071067811865485]]}, "properties": {"dbg_lines": "3", "from": "0xf4b230", "id": "0x101cc80", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0x101ce40"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.016485281374238568, -0.021414213562373104], [0.019151948040905236, -0.021414213562373104]]}, "properties": {"dbg_lines": "1$2$3$4", "from": "0xf4b860", "id": "0x10201b0", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}, {"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "f59e00", "id": "0x26b5750", "label": "3"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x1026070"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.0114142135623731, -0.030485281374238577], [0.0134142135623731, -0.030485281374238577]]}, "properties": {"dbg_lines": "3", "from": "0xf4c1d0", "id": "0x1029440", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0x10293b0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.03072792206135786, -0.035899494936611674], [0.029313708498984768, -0.034485281374238584]]}, "properties": {"dbg_lines": "2", "from": "0xf4c630", "id": "0xeeb200", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0x1017150"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.043313708498984776, -0.024242640687119295], [0.04531370849898477, -0.024242640687119295]]}, "properties": {"dbg_lines": "1", "from": "0xf4c930", "id": "0xf3f140", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0xf495a0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.0228284271247462], [0.029313708498984768, -0.024242640687119295]]}, "properties": {"dbg_lines": "1", "from": "0xf58400", "id": "0x101f7e0", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0x101f750"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.012071067811865479, -0.017000000000000005], [0.013485281374238574, -0.0184142135623731]]}, "properties": {"dbg_lines": "1", "from": "0xf591c0", "id": "0x1016f60", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0xeea7b0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.026485281374238577, -0.02765685424949239], [0.026485281374238577, -0.02565685424949239]]}, "properties": {"dbg_lines": "2", "from": "0xfd0ae0", "id": "0xf40940", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0xfd5fc0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.004242640687119285, -0.02565685424949239], [0.00282842712474619, -0.024242640687119295]]}, "properties": {"dbg_lines": "5", "from": "0xfd4b30", "id": "0x101de20", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0x101dfb0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.031313708498984766, -0.024242640687119295], [0.03331370849898477, -0.024242640687119295]]}, "properties": {"dbg_lines": "1", "from": "0xfd4d10", "id": "0xf48d20", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0xf498e0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.026485281374238577, -0.02565685424949239], [0.026485281374238577, -0.023535533905932746]]}, "properties": {"dbg_lines": "2$3", "from": "0xfd5fc0", "id": "0xfcebd0", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0x10252a0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.02507106781186548, -0.014828427124746196], [0.026485281374238574, -0.0134142135623731]]}, "properties": {"dbg_lines": "2", "from": "0x1006c70", "id": "0x1006c00", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0x10156c0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.008242640687119288, -0.02565685424949239], [0.006242640687119286, -0.02565685424949239]]}, "properties": {"dbg_lines": "5", "from": "0x10071b0", "id": "0xee6ee0", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0xeeadc0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.026485281374238577, -0.029656854249492392], [0.026485281374238577, -0.02765685424949239]]}, "properties": {"dbg_lines": "2", "from": "0x1008aa0", "id": "0x1008b40", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0xfd0ae0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.041313708498984775, -0.024242640687119295], [0.043313708498984776, -0.024242640687119295]]}, "properties": {"dbg_lines": "1", "from": "0x100a510", "id": "0xf50fc0", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0xf4c930"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.00924264068711929, -0.010171572875253814], [0.00924264068711929, -0.012171572875253816]]}, "properties": {"dbg_lines": "1", "from": "0x100ef60", "id": "0x100ecf0", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0x1011a80"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.02224264068711929, -0.017656854249492385], [0.023656854249492383, -0.01624264068711929]]}, "properties": {"dbg_lines": "2", "from": "0x10116c0", "id": "0x1022f80", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0x101a820"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.00924264068711929, -0.012171572875253816], [0.00924264068711929, -0.014171572875253816]]}, "properties": {"dbg_lines": "1", "from": "0x1011a80", "id": "0x10170e0", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0x1017330"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.0154142135623731, -0.030485281374238577], [0.016828427124746196, -0.029071067811865484]]}, "properties": {"dbg_lines": "3", "from": "0x1011e00", "id": "0x1018ca0", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0x101ae50"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.021656854249492388, -0.024242640687119295], [0.02024264068711929, -0.02565685424949239]]}, "properties": {"dbg_lines": "5", "from": "0x10132c0", "id": "0x1011a10", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0xf4b150"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.026485281374238574, -0.0134142135623731], [0.027899494936611674, -0.012000000000000004]]}, "properties": {"dbg_lines": "2", "from": "0x10156c0", "id": "0x1011890", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0xf19ce0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.029313708498984768, -0.034485281374238584], [0.027899494936611674, -0.03307106781186549]]}, "properties": {"dbg_lines": "2", "from": "0x1017150", "id": "0x101a5b0", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0x1018c00"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.023071067811865485, -0.0228284271247462], [0.021656854249492388, -0.024242640687119295]]}, "properties": {"dbg_lines": "5", "from": "0x10172a0", "id": "0x1008880", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0x10132c0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.00924264068711929, -0.014171572875253816], [0.010656854249492384, -0.015585786437626911]]}, "properties": {"dbg_lines": "1", "from": "0x1017330", "id": "0x1009d00", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0xf22bc0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.03307106781186549], [0.026485281374238577, -0.03165685424949239]]}, "properties": {"dbg_lines": "2", "from": "0x1018c00", "id": "0xf40aa0", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0x1023070"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.023656854249492383, -0.01624264068711929], [0.02507106781186548, -0.014828427124746196]]}, "properties": {"dbg_lines": "2", "from": "0x101a820", "id": "0xfd6ea0", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0x1006c70"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.016828427124746196, -0.029071067811865484], [0.018242640687119293, -0.02765685424949239]]}, "properties": {"dbg_lines": "3", "from": "0x101ae50", "id": "0xeeac70", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0x101cb80"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.01624264068711929, -0.02565685424949239], [0.01424264068711929, -0.02565685424949239]]}, "properties": {"dbg_lines": "5", "from": "0x101b5f0", "id": "0x10220a0", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0x1021f40"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.021818614707571907, -0.021414213562373104], [0.02448528137423858, -0.021414213562373104]]}, "properties": {"dbg_lines": "1$2$3$4", "from": "0x101c470", "id": "0x1015b40", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}, {"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "f59e00", "id": "0x26b5750", "label": "3"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0xf483e0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.011674621202458752, -0.020224873734152923], [0.009863961030678929, -0.022035533905932745]]}, "properties": {"dbg_lines": "3", "from": "0x101caf0", "id": "0xf517f0", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0x1028c00"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.018242640687119293, -0.02765685424949239], [0.018242640687119293, -0.02565685424949239]]}, "properties": {"dbg_lines": "3", "from": "0x101cb80", "id": "0xf5cba0", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0xf49330"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.00282842712474619, -0.027071067811865485], [0.004242640687119285, -0.02565685424949239]]}, "properties": {"dbg_lines": "3", "from": "0x101ce40", "id": "0xf5a320", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0xfd4b30"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.00282842712474619, -0.024242640687119295], [0.001414213562373095, -0.0228284271247462]]}, "properties": {"dbg_lines": "5", "from": "0x101dfb0", "id": "0xf5c2d0", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0x1020760"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.010242640687119288, -0.02565685424949239], [0.008242640687119288, -0.02565685424949239]]}, "properties": {"dbg_lines": "5", "from": "0x101e270", "id": "0x101e4d0", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0x10071b0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.02589949493661167, -0.020000000000000007], [0.02448528137423858, -0.021414213562373104]]}, "properties": {"dbg_lines": "5", "from": "0x101f1d0", "id": "0x101b720", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0xf483e0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.029313708498984768, -0.024242640687119295], [0.031313708498984766, -0.024242640687119295]]}, "properties": {"dbg_lines": "1", "from": "0x101f750", "id": "0x101e380", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0xfd4d10"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.03531370849898477, -0.024242640687119295], [0.03731370849898477, -0.024242640687119295]]}, "properties": {"dbg_lines": "1", "from": "0x101fff0", "id": "0x1009c40", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0x1022640"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.017414213562373097, -0.014828427124746196], [0.01882842712474619, -0.01624264068711929]]}, "properties": {"dbg_lines": "4", "from": "0x10200b0", "id": "0x1025ee0", "lines": [{"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x1025e50"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.001414213562373095, -0.0228284271247462], [0.0, -0.021414213562373104]]}, "properties": {"dbg_lines": "5", "from": "0x1020760", "id": "0x10206c0", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0xf4a360"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.03931370849898477, -0.024242640687119295], [0.041313708498984775, -0.024242640687119295]]}, "properties": {"dbg_lines": "1", "from": "0x1020b20", "id": "0xfd4ac0", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0x100a510"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.01424264068711929, -0.02565685424949239], [0.01224264068711929, -0.02565685424949239]]}, "properties": {"dbg_lines": "5", "from": "0x1021f40", "id": "0x1022030", "lines": [{"color": "0000ff", "id": "0x26b5690", "label": "5"}], "to": "0xef63b0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.03731370849898477, -0.024242640687119295], [0.03931370849898477, -0.024242640687119295]]}, "properties": {"dbg_lines": "1", "from": "0x1022640", "id": "0x10229a0", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}], "to": "0x1020b20"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.026485281374238577, -0.03165685424949239], [0.026485281374238577, -0.029656854249492392]]}, "properties": {"dbg_lines": "2", "from": "0x1023070", "id": "0x1020ab0", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}], "to": "0x1008aa0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.01882842712474619, -0.0134142135623731], [0.017414213562373097, -0.014828427124746196]]}, "properties": {"dbg_lines": "4", "from": "0x1024320", "id": "0x1007010", "lines": [{"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x10200b0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.014985281374238572, -0.0199142135623731], [0.016485281374238568, -0.021414213562373104]]}, "properties": {"dbg_lines": "1$3", "from": "0x10245f0", "id": "0xfcc700", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}, {"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0xf4b860"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.026485281374238577, -0.023535533905932746], [0.026485281374238577, -0.021414213562373104]]}, "properties": {"dbg_lines": "2$3", "from": "0x10252a0", "id": "0xf54920", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0xf4aeb0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.01882842712474619, -0.01624264068711929], [0.020242640687119288, -0.017656854249492385]]}, "properties": {"dbg_lines": "4", "from": "0x1025e50", "id": "0xf5a420", "lines": [{"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0xf480c0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.019151948040905236, -0.021414213562373104], [0.021818614707571907, -0.021414213562373104]]}, "properties": {"dbg_lines": "1$2$3$4", "from": "0x1026070", "id": "0x101ca80", "lines": [{"color": "e8001b", "id": "0x26648a0", "label": "1"}, {"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "f59e00", "id": "0x26b5750", "label": "3"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x101c470"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.004], [0.027899494936611674, -0.002]]}, "properties": {"dbg_lines": "2$4", "from": "0x1027260", "id": "0x102c1b0", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x102c630"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.009863961030678929, -0.022035533905932745], [0.008053300858899107, -0.02384619407771257]]}, "properties": {"dbg_lines": "3", "from": "0x1028c00", "id": "0x102f1d0", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0x102efc0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.0134142135623731, -0.030485281374238577], [0.0154142135623731, -0.030485281374238577]]}, "properties": {"dbg_lines": "3", "from": "0x10293b0", "id": "0x10294b0", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0x1011e00"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.008], [0.027899494936611674, -0.006]]}, "properties": {"dbg_lines": "2$4", "from": "0x1029b30", "id": "0x10272f0", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x102dc20"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.010000000000000002], [0.027899494936611674, -0.008]]}, "properties": {"dbg_lines": "2$4", "from": "0x102b240", "id": "0x1027080", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x1029b30"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.018000000000000006], [0.027899494936611674, -0.020000000000000007]]}, "properties": {"dbg_lines": "4", "from": "0x102bee0", "id": "0x102c0e0", "lines": [{"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0xf4aad0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.002], [0.027899494936611674, -0.0]]}, "properties": {"dbg_lines": "2$4", "from": "0x102c630", "id": "0x1012880", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0xf4c0f0"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.006], [0.027899494936611674, -0.004]]}, "properties": {"dbg_lines": "2$4", "from": "0x102dc20", "id": "0xf42230", "lines": [{"color": "13a538", "id": "0x26489d0", "label": "2"}, {"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0x1027260"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.027899494936611674, -0.014000000000000004], [0.027899494936611674, -0.016000000000000007]]}, "properties": {"dbg_lines": "4", "from": "0x102e8d0", "id": "0x102e040", "lines": [{"color": "ea5297", "id": "0x26b5810", "label": "4"}], "to": "0xf19570"}}, {"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[0.008053300858899107, -0.02384619407771257], [0.006242640687119286, -0.02565685424949239]]}, "properties": {"dbg_lines": "3", "from": "0x102efc0", "id": "0x102f160", "lines": [{"color": "f59e00", "id": "0x26b5750", "label": "3"}], "to": "0xeeadc0"}}]} -------------------------------------------------------------------------------- /loom-examples/wuerzburg.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "properties": { 4 | }, 5 | "features": [ 6 | { 7 | "type": "Feature", 8 | "geometry": { 9 | "type": "Point", 10 | "coordinates": [9.9285996460, 49.7923584452] 11 | }, 12 | "properties": { 13 | "component": 522, 14 | "deg": "2", 15 | "deg_in": "2", 16 | "deg_out": "2", 17 | "id": "0x55c4ce14d650", 18 | "station_id": "", 19 | "station_label": "Rathaus" 20 | } 21 | }, 22 | { 23 | "type": "Feature", 24 | "geometry": { 25 | "type": "Point", 26 | "coordinates": [9.9605014560, 49.7389327176] 27 | }, 28 | "properties": { 29 | "component": 522, 30 | "deg": "2", 31 | "deg_in": "2", 32 | "deg_out": "2", 33 | "id": "0x55c4ce16d3d0", 34 | "station_id": "", 35 | "station_label": "Madrider Ring" 36 | } 37 | }, 38 | { 39 | "type": "Feature", 40 | "geometry": { 41 | "type": "Point", 42 | "coordinates": [9.9661405445, 49.7311670471] 43 | }, 44 | "properties": { 45 | "component": 522, 46 | "deg": "2", 47 | "deg_in": "2", 48 | "deg_out": "2", 49 | "id": "0x55c4ce1c9b10", 50 | "station_id": "", 51 | "station_label": "Max-Mengeringhausen-Straße" 52 | } 53 | }, 54 | { 55 | "type": "Feature", 56 | "geometry": { 57 | "type": "Point", 58 | "coordinates": [9.9579698162, 49.7462240358] 59 | }, 60 | "properties": { 61 | "component": 522, 62 | "deg": "2", 63 | "deg_in": "2", 64 | "deg_out": "2", 65 | "id": "0x55c4ce1d95f0", 66 | "station_id": "", 67 | "station_label": "Straßburger Ring" 68 | } 69 | }, 70 | { 71 | "type": "Feature", 72 | "geometry": { 73 | "type": "Point", 74 | "coordinates": [9.9543987628, 49.7474030200] 75 | }, 76 | "properties": { 77 | "component": 522, 78 | "deg": "2", 79 | "deg_in": "2", 80 | "deg_out": "2", 81 | "id": "0x55c4ce438cf0", 82 | "station_id": "", 83 | "station_label": "Berner Straße" 84 | } 85 | }, 86 | { 87 | "type": "Feature", 88 | "geometry": { 89 | "type": "Point", 90 | "coordinates": [9.9318185496, 49.7868110914] 91 | }, 92 | "properties": { 93 | "component": 522, 94 | "deg": "2", 95 | "deg_in": "2", 96 | "deg_out": "2", 97 | "id": "0x55c4d9f528d0", 98 | "station_id": "", 99 | "station_label": "Eichendorffstraße" 100 | } 101 | }, 102 | { 103 | "type": "Feature", 104 | "geometry": { 105 | "type": "Point", 106 | "coordinates": [9.9206769016, 49.7979320366] 107 | }, 108 | "properties": { 109 | "component": 522, 110 | "deg": "2", 111 | "deg_in": "2", 112 | "deg_out": "2", 113 | "id": "0x55c4d9f774f0", 114 | "station_id": "", 115 | "station_label": "Talavera" 116 | } 117 | }, 118 | { 119 | "type": "Feature", 120 | "geometry": { 121 | "type": "Point", 122 | "coordinates": [9.9265989660, 49.7791546120] 123 | }, 124 | "properties": { 125 | "component": 522, 126 | "deg": "2", 127 | "deg_in": "2", 128 | "deg_out": "2", 129 | "id": "0x55c4d9ffa470", 130 | "station_id": "", 131 | "station_label": "Judenbühlweg" 132 | } 133 | }, 134 | { 135 | "type": "Feature", 136 | "geometry": { 137 | "type": "Point", 138 | "coordinates": [9.9646536730, 49.7216149049] 139 | }, 140 | "properties": { 141 | "component": 522, 142 | "deg": "1", 143 | "deg_in": "1", 144 | "deg_out": "1", 145 | "id": "0x55c4da107df0", 146 | "station_id": "", 147 | "station_label": "Rottenbauer" 148 | } 149 | }, 150 | { 151 | "type": "Feature", 152 | "geometry": { 153 | "type": "Point", 154 | "coordinates": [9.9038190219, 49.7943163398] 155 | }, 156 | "properties": { 157 | "component": 522, 158 | "deg": "2", 159 | "deg_in": "2", 160 | "deg_out": "2", 161 | "id": "0x55c4da221250", 162 | "station_id": "", 163 | "station_label": "DJK Sportzentrum" 164 | } 165 | }, 166 | { 167 | "type": "Feature", 168 | "geometry": { 169 | "type": "Point", 170 | "coordinates": [9.9253879401, 49.7829035497] 171 | }, 172 | "properties": { 173 | "component": 522, 174 | "deg": "2", 175 | "deg_in": "2", 176 | "deg_out": "2", 177 | "id": "0x55c4da2332b0", 178 | "station_id": "", 179 | "station_label": "Ruderzentrum" 180 | } 181 | }, 182 | { 183 | "type": "Feature", 184 | "geometry": { 185 | "type": "Point", 186 | "coordinates": [9.9414360607, 49.7673511453] 187 | }, 188 | "properties": { 189 | "component": 522, 190 | "deg": "2", 191 | "deg_in": "2", 192 | "deg_out": "2", 193 | "id": "0x55c4da3c7960", 194 | "station_id": "", 195 | "station_label": "Andreas-Grieser-Straße" 196 | } 197 | }, 198 | { 199 | "type": "Feature", 200 | "geometry": { 201 | "type": "Point", 202 | "coordinates": [9.9430358480, 49.7594851445] 203 | }, 204 | "properties": { 205 | "component": 522, 206 | "deg": "2", 207 | "deg_in": "2", 208 | "deg_out": "2", 209 | "id": "0x55c4da479b40", 210 | "station_id": "", 211 | "station_label": "Heriedenweg" 212 | } 213 | }, 214 | { 215 | "type": "Feature", 216 | "geometry": { 217 | "type": "Point", 218 | "coordinates": [9.9285685750, 49.7747634761] 219 | }, 220 | "properties": { 221 | "component": 522, 222 | "deg": "2", 223 | "deg_in": "2", 224 | "deg_out": "2", 225 | "id": "0x55c4da485c90", 226 | "station_id": "", 227 | "station_label": "Steinbachtal" 228 | } 229 | }, 230 | { 231 | "type": "Feature", 232 | "geometry": { 233 | "type": "Point", 234 | "coordinates": [9.9180300928, 49.7970072307] 235 | }, 236 | "properties": { 237 | "component": 522, 238 | "deg": "2", 239 | "deg_in": "2", 240 | "deg_out": "2", 241 | "id": "0x55c4da493250", 242 | "station_id": "", 243 | "station_label": "Nautiland" 244 | } 245 | }, 246 | { 247 | "type": "Feature", 248 | "geometry": { 249 | "type": "Point", 250 | "coordinates": [9.9251725974, 49.7979937189] 251 | }, 252 | "properties": { 253 | "component": 522, 254 | "deg": "2", 255 | "deg_in": "2", 256 | "deg_out": "2", 257 | "id": "0x55c4da4cef40", 258 | "station_id": "", 259 | "station_label": "Congress Centrum" 260 | } 261 | }, 262 | { 263 | "type": "Feature", 264 | "geometry": { 265 | "type": "Point", 266 | "coordinates": [9.9512408538, 49.8016411534] 267 | }, 268 | "properties": { 269 | "component": 522, 270 | "deg": "2", 271 | "deg_in": "2", 272 | "deg_out": "2", 273 | "id": "0x55c4da59b530", 274 | "station_id": "", 275 | "station_label": "Senefelderstraße" 276 | } 277 | }, 278 | { 279 | "type": "Feature", 280 | "geometry": { 281 | "type": "Point", 282 | "coordinates": [9.9539819767, 49.8014995420] 283 | }, 284 | "properties": { 285 | "component": 522, 286 | "deg": "2", 287 | "deg_in": "2", 288 | "deg_out": "2", 289 | "id": "0x55c4da5ffe90", 290 | "station_id": "", 291 | "station_label": "Uni-Klinikum Bereich D" 292 | } 293 | }, 294 | { 295 | "type": "Feature", 296 | "geometry": { 297 | "type": "Point", 298 | "coordinates": [9.9541178912, 49.8021283149] 299 | }, 300 | "properties": { 301 | "component": 522, 302 | "deg": "2", 303 | "deg_in": "2", 304 | "deg_out": "2", 305 | "id": "0x55c4da72bad0", 306 | "station_id": "", 307 | "station_label": "Robert Koch Straße / Uniklinikum Bereich B/C" 308 | } 309 | }, 310 | { 311 | "type": "Feature", 312 | "geometry": { 313 | "type": "Point", 314 | "coordinates": [9.9674158149, 49.7241126364] 315 | }, 316 | "properties": { 317 | "component": 522, 318 | "deg": "2", 319 | "deg_in": "2", 320 | "deg_out": "2", 321 | "id": "0x55c4da7380d0", 322 | "station_id": "", 323 | "station_label": "Brombergweg" 324 | } 325 | }, 326 | { 327 | "type": "Feature", 328 | "geometry": { 329 | "type": "Point", 330 | "coordinates": [9.9416951024, 49.7994220340] 331 | }, 332 | "properties": { 333 | "component": 522, 334 | "deg": "2", 335 | "deg_in": "2", 336 | "deg_out": "2", 337 | "id": "0x55c4da7a87a0", 338 | "station_id": "", 339 | "station_label": "Berliner Platz" 340 | } 341 | }, 342 | { 343 | "type": "Feature", 344 | "geometry": { 345 | "type": "Point", 346 | "coordinates": [9.9373386574, 49.7692038881] 347 | }, 348 | "properties": { 349 | "component": 522, 350 | "deg": "2", 351 | "deg_in": "2", 352 | "deg_out": "2", 353 | "id": "0x55c4da7ce100", 354 | "station_id": "", 355 | "station_label": "Dallenbergbad" 356 | } 357 | }, 358 | { 359 | "type": "Feature", 360 | "geometry": { 361 | "type": "Point", 362 | "coordinates": [9.9483622244, 49.8017992116] 363 | }, 364 | "properties": { 365 | "component": 522, 366 | "deg": "2", 367 | "deg_in": "2", 368 | "deg_out": "2", 369 | "id": "0x55c4da810e20", 370 | "station_id": "", 371 | "station_label": "Felix-Fechenbach-Haus" 372 | } 373 | }, 374 | { 375 | "type": "Feature", 376 | "geometry": { 377 | "type": "Point", 378 | "coordinates": [9.9355598313, 49.8005417588] 379 | }, 380 | "properties": { 381 | "component": 522, 382 | "deg": "2", 383 | "deg_in": "2", 384 | "deg_out": "2", 385 | "id": "0x55c4da816550", 386 | "station_id": "", 387 | "station_label": "Hauptbahnhof Ost" 388 | } 389 | }, 390 | { 391 | "type": "Feature", 392 | "geometry": { 393 | "type": "Point", 394 | "coordinates": [9.9600506650, 49.7383649681] 395 | }, 396 | "properties": { 397 | "component": 522, 398 | "deg": "2", 399 | "deg_in": "2", 400 | "deg_out": "2", 401 | "id": "0x55c4da82e2c0", 402 | "not_serving": ["5"], 403 | "station_id": "", 404 | "station_label": "Athener Ring" 405 | } 406 | }, 407 | { 408 | "type": "Feature", 409 | "geometry": { 410 | "type": "Point", 411 | "coordinates": [9.9284074912, 49.7966660440] 412 | }, 413 | "properties": { 414 | "component": 522, 415 | "deg": "2", 416 | "deg_in": "2", 417 | "deg_out": "2", 418 | "id": "0x55c4da82e460", 419 | "station_id": "", 420 | "station_label": "Ulmer Hof" 421 | } 422 | }, 423 | { 424 | "type": "Feature", 425 | "geometry": { 426 | "type": "Point", 427 | "coordinates": [9.9250565809, 49.7858421057] 428 | }, 429 | "properties": { 430 | "component": 522, 431 | "deg": "2", 432 | "deg_in": "2", 433 | "deg_out": "2", 434 | "id": "0x55c4da831a10", 435 | "station_id": "", 436 | "station_label": "Löwenbrücke" 437 | } 438 | }, 439 | { 440 | "type": "Feature", 441 | "geometry": { 442 | "type": "Point", 443 | "coordinates": [9.9519786719, 49.8023455225] 444 | }, 445 | "properties": { 446 | "component": 522, 447 | "deg": "2", 448 | "deg_in": "2", 449 | "deg_out": "2", 450 | "id": "0x55c4da889d90", 451 | "station_id": "", 452 | "station_label": "Pestalozzistraße" 453 | } 454 | }, 455 | { 456 | "type": "Feature", 457 | "geometry": { 458 | "type": "Point", 459 | "coordinates": [9.9609422536, 49.7418253054] 460 | }, 461 | "properties": { 462 | "component": 522, 463 | "deg": "2", 464 | "deg_in": "2", 465 | "deg_out": "2", 466 | "id": "0x55c4da951eb0", 467 | "station_id": "", 468 | "station_label": "Wiener Ring" 469 | } 470 | }, 471 | { 472 | "type": "Feature", 473 | "geometry": { 474 | "type": "Point", 475 | "coordinates": [9.9446235644, 49.7624138722] 476 | }, 477 | "properties": { 478 | "component": 522, 479 | "deg": "2", 480 | "deg_in": "2", 481 | "deg_out": "2", 482 | "id": "0x55c4da9a4150", 483 | "station_id": "", 484 | "station_label": "Klingenstraße" 485 | } 486 | }, 487 | { 488 | "type": "Feature", 489 | "geometry": { 490 | "type": "Point", 491 | "coordinates": [9.9291390398, 49.7904676977] 492 | }, 493 | "properties": { 494 | "component": 522, 495 | "deg": "2", 496 | "deg_in": "2", 497 | "deg_out": "2", 498 | "id": "0x55c4da9ce560", 499 | "station_id": "", 500 | "station_label": "Neubaustraße" 501 | } 502 | }, 503 | { 504 | "type": "Feature", 505 | "geometry": { 506 | "type": "Point", 507 | "coordinates": [9.8930894153, 49.7953193974] 508 | }, 509 | "properties": { 510 | "component": 522, 511 | "deg": "2", 512 | "deg_in": "2", 513 | "deg_out": "2", 514 | "id": "0x55c4dab02c00", 515 | "station_id": "", 516 | "station_label": "Bürgerbräu" 517 | } 518 | }, 519 | { 520 | "type": "Feature", 521 | "geometry": { 522 | "type": "Point", 523 | "coordinates": [9.9466336521, 49.7659957881] 524 | }, 525 | "properties": { 526 | "component": 522, 527 | "deg": "2", 528 | "deg_in": "2", 529 | "deg_out": "2", 530 | "id": "0x55c4daf009b0", 531 | "station_id": "", 532 | "station_label": "Reuterstraße" 533 | } 534 | }, 535 | { 536 | "type": "Feature", 537 | "geometry": { 538 | "type": "Point", 539 | "coordinates": [9.8951749835, 49.7943597432] 540 | }, 541 | "properties": { 542 | "component": 522, 543 | "deg": "3", 544 | "deg_in": "3", 545 | "deg_out": "3", 546 | "id": "0x55c4daf0eca0" 547 | } 548 | }, 549 | { 550 | "type": "Feature", 551 | "geometry": { 552 | "type": "Point", 553 | "coordinates": [9.9112972568, 49.7943198476] 554 | }, 555 | "properties": { 556 | "component": 522, 557 | "deg": "2", 558 | "deg_in": "2", 559 | "deg_out": "2", 560 | "id": "0x55c4daf1ca80", 561 | "station_id": "", 562 | "station_label": "Hartmannstraße" 563 | } 564 | }, 565 | { 566 | "type": "Feature", 567 | "geometry": { 568 | "type": "Point", 569 | "coordinates": [9.8981830822, 49.7943226898] 570 | }, 571 | "properties": { 572 | "component": 522, 573 | "deg": "2", 574 | "deg_in": "2", 575 | "deg_out": "2", 576 | "id": "0x55c4daf300f0", 577 | "station_id": "", 578 | "station_label": "Sieboldmuseum" 579 | } 580 | }, 581 | { 582 | "type": "Feature", 583 | "geometry": { 584 | "type": "Point", 585 | "coordinates": [9.9440660228, 49.8019009620] 586 | }, 587 | "properties": { 588 | "component": 522, 589 | "deg": "3", 590 | "deg_in": "3", 591 | "deg_out": "3", 592 | "excluded_conn": [ 593 | { 594 | "line": "1", 595 | "node_from": "0x55c4de5d40a0", 596 | "node_to": "0x55c4da810e20" 597 | }, 598 | { 599 | "line": "1", 600 | "node_from": "0x55c4da810e20", 601 | "node_to": "0x55c4de5d40a0" 602 | }, 603 | { 604 | "line": "5", 605 | "node_from": "0x55c4de5d40a0", 606 | "node_to": "0x55c4da810e20" 607 | }, 608 | { 609 | "line": "5", 610 | "node_from": "0x55c4da810e20", 611 | "node_to": "0x55c4de5d40a0" 612 | }], 613 | "id": "0x55c4daf7d5f0", 614 | "station_id": "", 615 | "station_label": "Brücknerstraße" 616 | } 617 | }, 618 | { 619 | "type": "Feature", 620 | "geometry": { 621 | "type": "Point", 622 | "coordinates": [9.9159884710, 49.7943768187] 623 | }, 624 | "properties": { 625 | "component": 522, 626 | "deg": "2", 627 | "deg_in": "2", 628 | "deg_out": "2", 629 | "id": "0x55c4dafe79a0", 630 | "station_id": "", 631 | "station_label": "Wörthstraße" 632 | } 633 | }, 634 | { 635 | "type": "Feature", 636 | "geometry": { 637 | "type": "Point", 638 | "coordinates": [9.8931672258, 49.7945155318] 639 | }, 640 | "properties": { 641 | "component": 522, 642 | "deg": "2", 643 | "deg_in": "2", 644 | "deg_out": "2", 645 | "id": "0x55c4db056850" 646 | } 647 | }, 648 | { 649 | "type": "Feature", 650 | "geometry": { 651 | "type": "Point", 652 | "coordinates": [9.9310194666, 49.7942649170] 653 | }, 654 | "properties": { 655 | "component": 522, 656 | "deg": "2", 657 | "deg_in": "2", 658 | "deg_out": "2", 659 | "id": "0x55c4db08a060", 660 | "station_id": "", 661 | "station_label": "Dom" 662 | } 663 | }, 664 | { 665 | "type": "Feature", 666 | "geometry": { 667 | "type": "Point", 668 | "coordinates": [9.9312558412, 49.7971758167] 669 | }, 670 | "properties": { 671 | "component": 522, 672 | "deg": "3", 673 | "deg_in": "3", 674 | "deg_out": "3", 675 | "id": "0x55c4de27fd80", 676 | "station_id": "", 677 | "station_label": "Juliuspromenade" 678 | } 679 | }, 680 | { 681 | "type": "Feature", 682 | "geometry": { 683 | "type": "Point", 684 | "coordinates": [9.9480523802, 49.8024082753] 685 | }, 686 | "properties": { 687 | "component": 522, 688 | "deg": "2", 689 | "deg_in": "2", 690 | "deg_out": "2", 691 | "id": "0x55c4de5d40a0", 692 | "station_id": "", 693 | "station_label": "Josefskirche" 694 | } 695 | }, 696 | { 697 | "type": "Feature", 698 | "geometry": { 699 | "type": "LineString", 700 | "coordinates": [[9.9285996460, 49.7923584452], [9.9286058794, 49.7917754307], [9.9286814182, 49.7914304772], [9.9288337497, 49.7910568401], [9.9291390398, 49.7904676977]] 701 | }, 702 | "properties": { 703 | "component": 522, 704 | "dbg_lines": "4,5,3,1", 705 | "from": "0x55c4ce14d650", 706 | "id": "0x55c4ee7d40d0", 707 | "lines": [ 708 | { 709 | "color": "a70cf3", 710 | "id": "4", 711 | "label": "4" 712 | }, 713 | { 714 | "color": "7d0cf3", 715 | "id": "5", 716 | "label": "5" 717 | }, 718 | { 719 | "color": "f3850c", 720 | "id": "3", 721 | "label": "3" 722 | }, 723 | { 724 | "color": "3df30c", 725 | "id": "1", 726 | "label": "1" 727 | }], 728 | "to": "0x55c4da9ce560" 729 | } 730 | }, 731 | { 732 | "type": "Feature", 733 | "geometry": { 734 | "type": "LineString", 735 | "coordinates": [[9.9605014560, 49.7389327176], [9.9606959311, 49.7393185840], [9.9607989449, 49.7395893705], [9.9609312240, 49.7400502926], [9.9610234662, 49.7404865523], [9.9610528917, 49.7407429101], [9.9610488429, 49.7410478972], [9.9610230693, 49.7413066438], [9.9609422536, 49.7418253054]] 736 | }, 737 | "properties": { 738 | "component": 522, 739 | "dbg_lines": "3,5", 740 | "from": "0x55c4ce16d3d0", 741 | "id": "0x55c4f9f0ea80", 742 | "lines": [ 743 | { 744 | "color": "f3850c", 745 | "id": "3", 746 | "label": "3" 747 | }, 748 | { 749 | "color": "7d0cf3", 750 | "id": "5", 751 | "label": "5" 752 | }], 753 | "to": "0x55c4da951eb0" 754 | } 755 | }, 756 | { 757 | "type": "Feature", 758 | "geometry": { 759 | "type": "LineString", 760 | "coordinates": [[9.9661405445, 49.7311670471], [9.9666362783, 49.7301602528], [9.9668520958, 49.7296405756], [9.9670098093, 49.7291522880], [9.9671733079, 49.7284168051], [9.9672296504, 49.7280276581], [9.9672685035, 49.7275833762], [9.9673944311, 49.7256580723], [9.9674359836, 49.7246048057], [9.9674158149, 49.7241126364]] 761 | }, 762 | "properties": { 763 | "component": 522, 764 | "dbg_lines": "5", 765 | "from": "0x55c4ce1c9b10", 766 | "id": "0x55c4fee00d60", 767 | "lines": [ 768 | { 769 | "color": "7d0cf3", 770 | "id": "5", 771 | "label": "5" 772 | }], 773 | "to": "0x55c4da7380d0" 774 | } 775 | }, 776 | { 777 | "type": "Feature", 778 | "geometry": { 779 | "type": "LineString", 780 | "coordinates": [[9.9579698162, 49.7462240358], [9.9574197553, 49.7464500557], [9.9568469669, 49.7466455080], [9.9543987628, 49.7474030200]] 781 | }, 782 | "properties": { 783 | "component": 522, 784 | "dbg_lines": "3,5", 785 | "from": "0x55c4ce1d95f0", 786 | "id": "0x55c4d734a8d0", 787 | "lines": [ 788 | { 789 | "color": "f3850c", 790 | "id": "3", 791 | "label": "3" 792 | }, 793 | { 794 | "color": "7d0cf3", 795 | "id": "5", 796 | "label": "5" 797 | }], 798 | "to": "0x55c4ce438cf0" 799 | } 800 | }, 801 | { 802 | "type": "Feature", 803 | "geometry": { 804 | "type": "LineString", 805 | "coordinates": [[9.9543987628, 49.7474030200], [9.9528330645, 49.7478560886], [9.9522581224, 49.7480368620], [9.9519468811, 49.7481654033], [9.9516561890, 49.7483287847], [9.9514714529, 49.7484739564], [9.9513308073, 49.7486090094], [9.9511786959, 49.7488189513], [9.9510102829, 49.7491300080], [9.9506585047, 49.7498813898], [9.9506024513, 49.7499836067], [9.9505120555, 49.7501081918], [9.9503643609, 49.7502631509], [9.9501946893, 49.7504005758], [9.9499623326, 49.7505599718], [9.9496834113, 49.7507307586], [9.9495305981, 49.7507978871], [9.9493786620, 49.7508430443], [9.9478278988, 49.7510865472], [9.9475817253, 49.7511418510], [9.9472490320, 49.7512378347], [9.9469805036, 49.7513507503], [9.9466864270, 49.7515173768], [9.9464599971, 49.7516839683], [9.9462606621, 49.7518744392], [9.9460810123, 49.7520852566], [9.9459579776, 49.7522561421], [9.9454905055, 49.7530175854], [9.9453122828, 49.7533885411], [9.9452526928, 49.7536481636], [9.9452363636, 49.7537749315], [9.9452691471, 49.7539847181], [9.9455769372, 49.7552146195], [9.9456490447, 49.7555618439], [9.9456482903, 49.7557910627], [9.9455936702, 49.7559686524], [9.9454462248, 49.7562463838], [9.9453310381, 49.7564001206], [9.9451869166, 49.7565585432], [9.9447502697, 49.7569484768], [9.9440974486, 49.7574786453], [9.9437447806, 49.7577466442], [9.9430301388, 49.7580257741], [9.9429425502, 49.7580697633], [9.9428916095, 49.7581258977], [9.9428460985, 49.7582240271], [9.9428473953, 49.7583259793], [9.9430358480, 49.7594851445]] 806 | }, 807 | "properties": { 808 | "component": 522, 809 | "dbg_lines": "3,5", 810 | "from": "0x55c4ce438cf0", 811 | "id": "0x55c4d823b2c0", 812 | "lines": [ 813 | { 814 | "color": "f3850c", 815 | "id": "3", 816 | "label": "3" 817 | }, 818 | { 819 | "color": "7d0cf3", 820 | "id": "5", 821 | "label": "5" 822 | }], 823 | "to": "0x55c4da479b40" 824 | } 825 | }, 826 | { 827 | "type": "Feature", 828 | "geometry": { 829 | "type": "LineString", 830 | "coordinates": [[9.9206769016, 49.7979320366], [9.9180300928, 49.7970072307]] 831 | }, 832 | "properties": { 833 | "component": 522, 834 | "dbg_lines": "2,4", 835 | "from": "0x55c4d9f774f0", 836 | "id": "0x55c4ffbd1710", 837 | "lines": [ 838 | { 839 | "color": "1f0cf3", 840 | "id": "2", 841 | "label": "2" 842 | }, 843 | { 844 | "color": "a70cf3", 845 | "id": "4", 846 | "label": "4" 847 | }], 848 | "to": "0x55c4da493250" 849 | } 850 | }, 851 | { 852 | "type": "Feature", 853 | "geometry": { 854 | "type": "LineString", 855 | "coordinates": [[9.9265989660, 49.7791546120], [9.9261734871, 49.7804249654], [9.9253879401, 49.7829035497]] 856 | }, 857 | "properties": { 858 | "component": 522, 859 | "dbg_lines": "3,5", 860 | "from": "0x55c4d9ffa470", 861 | "id": "0x55c4ed0fe5b0", 862 | "lines": [ 863 | { 864 | "color": "f3850c", 865 | "id": "3", 866 | "label": "3" 867 | }, 868 | { 869 | "color": "7d0cf3", 870 | "id": "5", 871 | "label": "5" 872 | }], 873 | "to": "0x55c4da2332b0" 874 | } 875 | }, 876 | { 877 | "type": "Feature", 878 | "geometry": { 879 | "type": "LineString", 880 | "coordinates": [[9.9038190219, 49.7943163398], [9.9033141981, 49.7943305302], [9.9026175542, 49.7943358557], [9.8981830822, 49.7943226898]] 881 | }, 882 | "properties": { 883 | "component": 522, 884 | "dbg_lines": "2,4", 885 | "from": "0x55c4da221250", 886 | "id": "0x55c4fd4be9f0", 887 | "lines": [ 888 | { 889 | "color": "1f0cf3", 890 | "id": "2", 891 | "label": "2" 892 | }, 893 | { 894 | "color": "a70cf3", 895 | "id": "4", 896 | "label": "4" 897 | }], 898 | "to": "0x55c4daf300f0" 899 | } 900 | }, 901 | { 902 | "type": "Feature", 903 | "geometry": { 904 | "type": "LineString", 905 | "coordinates": [[9.9253879401, 49.7829035497], [9.9251451505, 49.7840324909], [9.9250646085, 49.7846004410], [9.9250393029, 49.7851174746], [9.9250565809, 49.7858421057]] 906 | }, 907 | "properties": { 908 | "component": 522, 909 | "dbg_lines": "3,5", 910 | "from": "0x55c4da2332b0", 911 | "id": "0x55c4dbc4a5d0", 912 | "lines": [ 913 | { 914 | "color": "f3850c", 915 | "id": "3", 916 | "label": "3" 917 | }, 918 | { 919 | "color": "7d0cf3", 920 | "id": "5", 921 | "label": "5" 922 | }], 923 | "to": "0x55c4da831a10" 924 | } 925 | }, 926 | { 927 | "type": "Feature", 928 | "geometry": { 929 | "type": "LineString", 930 | "coordinates": [[9.9414360607, 49.7673511453], [9.9403525963, 49.7678268587], [9.9398055146, 49.7680464383], [9.9394667551, 49.7681692284], [9.9384459069, 49.7685053195], [9.9381067749, 49.7686990538], [9.9373386574, 49.7692038881]] 931 | }, 932 | "properties": { 933 | "component": 522, 934 | "dbg_lines": "3,5", 935 | "from": "0x55c4da3c7960", 936 | "id": "0x55c4ceaa7b80", 937 | "lines": [ 938 | { 939 | "color": "f3850c", 940 | "id": "3", 941 | "label": "3" 942 | }, 943 | { 944 | "color": "7d0cf3", 945 | "id": "5", 946 | "label": "5" 947 | }], 948 | "to": "0x55c4da7ce100" 949 | } 950 | }, 951 | { 952 | "type": "Feature", 953 | "geometry": { 954 | "type": "LineString", 955 | "coordinates": [[9.9430358480, 49.7594851445], [9.9433862698, 49.7602426199], [9.9436560586, 49.7607561480], [9.9446235644, 49.7624138722]] 956 | }, 957 | "properties": { 958 | "component": 522, 959 | "dbg_lines": "3,5", 960 | "from": "0x55c4da479b40", 961 | "id": "0x55c4d0ffbae0", 962 | "lines": [ 963 | { 964 | "color": "f3850c", 965 | "id": "3", 966 | "label": "3" 967 | }, 968 | { 969 | "color": "7d0cf3", 970 | "id": "5", 971 | "label": "5" 972 | }], 973 | "to": "0x55c4da9a4150" 974 | } 975 | }, 976 | { 977 | "type": "Feature", 978 | "geometry": { 979 | "type": "LineString", 980 | "coordinates": [[9.9285685750, 49.7747634761], [9.9283925765, 49.7750693303], [9.9282649559, 49.7753269783], [9.9279595990, 49.7760796695], [9.9277513158, 49.7765390694], [9.9273040395, 49.7773890243], [9.9270576751, 49.7779047159], [9.9268562256, 49.7784091707], [9.9265989660, 49.7791546120]] 981 | }, 982 | "properties": { 983 | "component": 522, 984 | "dbg_lines": "3,5", 985 | "from": "0x55c4da485c90", 986 | "id": "0x55c5019698a0", 987 | "lines": [ 988 | { 989 | "color": "f3850c", 990 | "id": "3", 991 | "label": "3" 992 | }, 993 | { 994 | "color": "7d0cf3", 995 | "id": "5", 996 | "label": "5" 997 | }], 998 | "to": "0x55c4d9ffa470" 999 | } 1000 | }, 1001 | { 1002 | "type": "Feature", 1003 | "geometry": { 1004 | "type": "LineString", 1005 | "coordinates": [[9.9180300928, 49.7970072307], [9.9175194585, 49.7967918462], [9.9173036052, 49.7966873762], [9.9171544700, 49.7965967586], [9.9169932384, 49.7964502367], [9.9169558960, 49.7963915949], [9.9169274511, 49.7962860561], [9.9167209665, 49.7952390074], [9.9166210605, 49.7949355787], [9.9165626181, 49.7948136931], [9.9164985162, 49.7947116935], [9.9164287547, 49.7946295800], [9.9163533337, 49.7945673528], [9.9159884710, 49.7943768187]] 1006 | }, 1007 | "properties": { 1008 | "component": 522, 1009 | "dbg_lines": "2,4", 1010 | "from": "0x55c4da493250", 1011 | "id": "0x55c4d24f7d50", 1012 | "lines": [ 1013 | { 1014 | "color": "1f0cf3", 1015 | "id": "2", 1016 | "label": "2" 1017 | }, 1018 | { 1019 | "color": "a70cf3", 1020 | "id": "4", 1021 | "label": "4" 1022 | }], 1023 | "to": "0x55c4dafe79a0" 1024 | } 1025 | }, 1026 | { 1027 | "type": "Feature", 1028 | "geometry": { 1029 | "type": "LineString", 1030 | "coordinates": [[9.9251725974, 49.7979937189], [9.9247027696, 49.7986727346], [9.9244409595, 49.7990167654], [9.9243968478, 49.7990541860], [9.9243154680, 49.7990957554], [9.9242583620, 49.7991101605], [9.9241846374, 49.7991149095], [9.9240665311, 49.7990987898], [9.9239384097, 49.7990594657], [9.9206769016, 49.7979320366]] 1031 | }, 1032 | "properties": { 1033 | "component": 522, 1034 | "dbg_lines": "2,4", 1035 | "from": "0x55c4da4cef40", 1036 | "id": "0x55c503ca46e0", 1037 | "lines": [ 1038 | { 1039 | "color": "1f0cf3", 1040 | "id": "2", 1041 | "label": "2" 1042 | }, 1043 | { 1044 | "color": "a70cf3", 1045 | "id": "4", 1046 | "label": "4" 1047 | }], 1048 | "to": "0x55c4d9f774f0" 1049 | } 1050 | }, 1051 | { 1052 | "type": "Feature", 1053 | "geometry": { 1054 | "type": "LineString", 1055 | "coordinates": [[9.9512408538, 49.8016411534], [9.9539819767, 49.8014995420]] 1056 | }, 1057 | "properties": { 1058 | "component": 522, 1059 | "dbg_lines": "1,5", 1060 | "from": "0x55c4da59b530", 1061 | "id": "0x55c4dbf4a190", 1062 | "lines": [ 1063 | { 1064 | "color": "3df30c", 1065 | "id": "1", 1066 | "label": "1" 1067 | }, 1068 | { 1069 | "color": "7d0cf3", 1070 | "id": "5", 1071 | "label": "5" 1072 | }], 1073 | "to": "0x55c4da5ffe90" 1074 | } 1075 | }, 1076 | { 1077 | "type": "Feature", 1078 | "geometry": { 1079 | "type": "LineString", 1080 | "coordinates": [[9.9539819767, 49.8014995420], [9.9545166395, 49.8017180268], [9.9547000300, 49.8018059280], [9.9546549043, 49.8018462249], [9.9545749234, 49.8018935670], [9.9541178912, 49.8021283149]] 1081 | }, 1082 | "properties": { 1083 | "component": 522, 1084 | "dbg_lines": "1,5", 1085 | "from": "0x55c4da5ffe90", 1086 | "id": "0x55c4cf0d5f90", 1087 | "lines": [ 1088 | { 1089 | "color": "3df30c", 1090 | "id": "1", 1091 | "label": "1" 1092 | }, 1093 | { 1094 | "color": "7d0cf3", 1095 | "id": "5", 1096 | "label": "5" 1097 | }], 1098 | "to": "0x55c4da72bad0" 1099 | } 1100 | }, 1101 | { 1102 | "type": "Feature", 1103 | "geometry": { 1104 | "type": "LineString", 1105 | "coordinates": [[9.9541178912, 49.8021283149], [9.9519786719, 49.8023455225]] 1106 | }, 1107 | "properties": { 1108 | "component": 522, 1109 | "dbg_lines": "1,5", 1110 | "from": "0x55c4da72bad0", 1111 | "id": "0x55c4eb24f770", 1112 | "lines": [ 1113 | { 1114 | "color": "3df30c", 1115 | "id": "1", 1116 | "label": "1" 1117 | }, 1118 | { 1119 | "color": "7d0cf3", 1120 | "id": "5", 1121 | "label": "5" 1122 | }], 1123 | "to": "0x55c4da889d90" 1124 | } 1125 | }, 1126 | { 1127 | "type": "Feature", 1128 | "geometry": { 1129 | "type": "LineString", 1130 | "coordinates": [[9.9674158149, 49.7241126364], [9.9670675618, 49.7235585372], [9.9670004314, 49.7234819557], [9.9669020498, 49.7234133163], [9.9667455988, 49.7233292065], [9.9665435485, 49.7232592100], [9.9656464370, 49.7230332920], [9.9655091459, 49.7229710692], [9.9654116534, 49.7229014769], [9.9653712762, 49.7228603729], [9.9653285056, 49.7227728497], [9.9652080621, 49.7223521868], [9.9651462512, 49.7221973294], [9.9646536730, 49.7216149049]] 1131 | }, 1132 | "properties": { 1133 | "component": 522, 1134 | "dbg_lines": "5", 1135 | "from": "0x55c4da7380d0", 1136 | "id": "0x55c4d31c6330", 1137 | "lines": [ 1138 | { 1139 | "color": "7d0cf3", 1140 | "id": "5", 1141 | "label": "5" 1142 | }], 1143 | "to": "0x55c4da107df0" 1144 | } 1145 | }, 1146 | { 1147 | "type": "Feature", 1148 | "geometry": { 1149 | "type": "LineString", 1150 | "coordinates": [[9.9416951024, 49.7994220340], [9.9412145356, 49.7995629717], [9.9405582780, 49.7996962892], [9.9393982007, 49.7998886452], [9.9366878221, 49.8003111853], [9.9360345090, 49.8004299419], [9.9355598313, 49.8005417588]] 1151 | }, 1152 | "properties": { 1153 | "component": 522, 1154 | "dbg_lines": "1,5", 1155 | "from": "0x55c4da7a87a0", 1156 | "id": "0x55c4fd7d6e50", 1157 | "lines": [ 1158 | { 1159 | "color": "3df30c", 1160 | "id": "1", 1161 | "label": "1" 1162 | }, 1163 | { 1164 | "color": "7d0cf3", 1165 | "id": "5", 1166 | "label": "5" 1167 | }], 1168 | "to": "0x55c4da816550" 1169 | } 1170 | }, 1171 | { 1172 | "type": "Feature", 1173 | "geometry": { 1174 | "type": "LineString", 1175 | "coordinates": [[9.9373386574, 49.7692038881], [9.9341602423, 49.7713367912], [9.9323372889, 49.7725299546], [9.9309583206, 49.7733800151], [9.9291649419, 49.7743816890], [9.9289208563, 49.7745295379], [9.9285685750, 49.7747634761]] 1176 | }, 1177 | "properties": { 1178 | "component": 522, 1179 | "dbg_lines": "3,5", 1180 | "from": "0x55c4da7ce100", 1181 | "id": "0x55c4d5d99a90", 1182 | "lines": [ 1183 | { 1184 | "color": "f3850c", 1185 | "id": "3", 1186 | "label": "3" 1187 | }, 1188 | { 1189 | "color": "7d0cf3", 1190 | "id": "5", 1191 | "label": "5" 1192 | }], 1193 | "to": "0x55c4da485c90" 1194 | } 1195 | }, 1196 | { 1197 | "type": "Feature", 1198 | "geometry": { 1199 | "type": "LineString", 1200 | "coordinates": [[9.9483622244, 49.8017992116], [9.9512408538, 49.8016411534]] 1201 | }, 1202 | "properties": { 1203 | "component": 522, 1204 | "dbg_lines": "1,5", 1205 | "from": "0x55c4da810e20", 1206 | "id": "0x55c4d8bc2c00", 1207 | "lines": [ 1208 | { 1209 | "color": "3df30c", 1210 | "id": "1", 1211 | "label": "1" 1212 | }, 1213 | { 1214 | "color": "7d0cf3", 1215 | "id": "5", 1216 | "label": "5" 1217 | }], 1218 | "to": "0x55c4da59b530" 1219 | } 1220 | }, 1221 | { 1222 | "type": "Feature", 1223 | "geometry": { 1224 | "type": "LineString", 1225 | "coordinates": [[9.9600506650, 49.7383649681], [9.9605014560, 49.7389327176]] 1226 | }, 1227 | "properties": { 1228 | "component": 522, 1229 | "dbg_lines": "3,5", 1230 | "from": "0x55c4da82e2c0", 1231 | "id": "0x55c4ecd89590", 1232 | "lines": [ 1233 | { 1234 | "color": "f3850c", 1235 | "id": "3", 1236 | "label": "3" 1237 | }, 1238 | { 1239 | "color": "7d0cf3", 1240 | "id": "5", 1241 | "label": "5" 1242 | }], 1243 | "to": "0x55c4ce16d3d0" 1244 | } 1245 | }, 1246 | { 1247 | "type": "Feature", 1248 | "geometry": { 1249 | "type": "LineString", 1250 | "coordinates": [[9.9600506650, 49.7383649681], [9.9600713161, 49.7380391529], [9.9601000597, 49.7379578117], [9.9601529577, 49.7378675328], [9.9602300101, 49.7377683161], [9.9604565783, 49.7375430693], [9.9611897058, 49.7369669694], [9.9614161152, 49.7367684854], [9.9619118111, 49.7362896492], [9.9624646765, 49.7357016526], [9.9636578913, 49.7343204378], [9.9646596610, 49.7331139343], [9.9656902283, 49.7318082441], [9.9659415522, 49.7314672651], [9.9661405445, 49.7311670471]] 1251 | }, 1252 | "properties": { 1253 | "component": 522, 1254 | "dbg_lines": "5", 1255 | "from": "0x55c4da82e2c0", 1256 | "id": "0x55c4ff43abf0", 1257 | "lines": [ 1258 | { 1259 | "color": "7d0cf3", 1260 | "id": "5", 1261 | "label": "5" 1262 | }], 1263 | "to": "0x55c4ce1c9b10" 1264 | } 1265 | }, 1266 | { 1267 | "type": "Feature", 1268 | "geometry": { 1269 | "type": "LineString", 1270 | "coordinates": [[9.9284074912, 49.7966660440], [9.9274912339, 49.7964715274], [9.9270188422, 49.7963919777], [9.9268801255, 49.7963854280], [9.9267064763, 49.7963963297], [9.9265069336, 49.7964370745], [9.9263606620, 49.7964869262], [9.9261812887, 49.7965791134], [9.9260958757, 49.7966754507], [9.9251725974, 49.7979937189]] 1271 | }, 1272 | "properties": { 1273 | "component": 522, 1274 | "dbg_lines": "2,4", 1275 | "from": "0x55c4da82e460", 1276 | "id": "0x55c50ee199d0", 1277 | "lines": [ 1278 | { 1279 | "color": "1f0cf3", 1280 | "id": "2", 1281 | "label": "2" 1282 | }, 1283 | { 1284 | "color": "a70cf3", 1285 | "id": "4", 1286 | "label": "4" 1287 | }], 1288 | "to": "0x55c4da4cef40" 1289 | } 1290 | }, 1291 | { 1292 | "type": "Feature", 1293 | "geometry": { 1294 | "type": "LineString", 1295 | "coordinates": [[9.9250565809, 49.7858421057], [9.9252952955, 49.7861155555], [9.9254286961, 49.7861716182], [9.9256424496, 49.7862229773], [9.9259365559, 49.7862696326], [9.9263110152, 49.7863115843], [9.9273009925, 49.7863813766], [9.9292333092, 49.7864572517], [9.9302503364, 49.7865123274], [9.9306464356, 49.7865425058], [9.9312138052, 49.7866081436], [9.9313850757, 49.7866436030], [9.9318185496, 49.7868110914]] 1296 | }, 1297 | "properties": { 1298 | "component": 522, 1299 | "dbg_lines": "3,5", 1300 | "from": "0x55c4da831a10", 1301 | "id": "0x55c4f0bc7da0", 1302 | "lines": [ 1303 | { 1304 | "color": "f3850c", 1305 | "id": "3", 1306 | "label": "3" 1307 | }, 1308 | { 1309 | "color": "7d0cf3", 1310 | "id": "5", 1311 | "label": "5" 1312 | }], 1313 | "to": "0x55c4d9f528d0" 1314 | } 1315 | }, 1316 | { 1317 | "type": "Feature", 1318 | "geometry": { 1319 | "type": "LineString", 1320 | "coordinates": [[9.9519786719, 49.8023455225], [9.9501281716, 49.8025323436], [9.9494861435, 49.8025886127], [9.9493237688, 49.8025756308], [9.9488391814, 49.8025024673], [9.9480523802, 49.8024082753]] 1321 | }, 1322 | "properties": { 1323 | "component": 522, 1324 | "dbg_lines": "1,5", 1325 | "from": "0x55c4da889d90", 1326 | "id": "0x55c4feff8080", 1327 | "lines": [ 1328 | { 1329 | "color": "3df30c", 1330 | "id": "1", 1331 | "label": "1" 1332 | }, 1333 | { 1334 | "color": "7d0cf3", 1335 | "id": "5", 1336 | "label": "5" 1337 | }], 1338 | "to": "0x55c4de5d40a0" 1339 | } 1340 | }, 1341 | { 1342 | "type": "Feature", 1343 | "geometry": { 1344 | "type": "LineString", 1345 | "coordinates": [[9.9609422536, 49.7418253054], [9.9608235391, 49.7424685949], [9.9606789972, 49.7429589629], [9.9604599658, 49.7434794583], [9.9603046735, 49.7437632867], [9.9601145380, 49.7440658129], [9.9598895591, 49.7443870365], [9.9595351965, 49.7448521499], [9.9592477477, 49.7452039959], [9.9590272125, 49.7454425769], [9.9588010901, 49.7456444933], [9.9586270987, 49.7457800123], [9.9579698162, 49.7462240358]] 1346 | }, 1347 | "properties": { 1348 | "component": 522, 1349 | "dbg_lines": "3,5", 1350 | "from": "0x55c4da951eb0", 1351 | "id": "0x55c4d46a7c00", 1352 | "lines": [ 1353 | { 1354 | "color": "f3850c", 1355 | "id": "3", 1356 | "label": "3" 1357 | }, 1358 | { 1359 | "color": "7d0cf3", 1360 | "id": "5", 1361 | "label": "5" 1362 | }], 1363 | "to": "0x55c4ce1d95f0" 1364 | } 1365 | }, 1366 | { 1367 | "type": "Feature", 1368 | "geometry": { 1369 | "type": "LineString", 1370 | "coordinates": [[9.9446235644, 49.7624138722], [9.9446855443, 49.7627370088], [9.9446969470, 49.7629453484], [9.9446634740, 49.7631430620], [9.9445597737, 49.7634229605], [9.9445355897, 49.7635510637], [9.9446331397, 49.7638424700], [9.9447488836, 49.7640966314], [9.9448345783, 49.7642191451], [9.9449485190, 49.7643113113], [9.9460553823, 49.7650759641], [9.9462160720, 49.7651999880], [9.9464582390, 49.7654207194], [9.9465397164, 49.7655174269], [9.9465947897, 49.7656050292], [9.9466234589, 49.7656835261], [9.9466336521, 49.7659957881]] 1371 | }, 1372 | "properties": { 1373 | "component": 522, 1374 | "dbg_lines": "3,5", 1375 | "from": "0x55c4da9a4150", 1376 | "id": "0x55c50308e620", 1377 | "lines": [ 1378 | { 1379 | "color": "f3850c", 1380 | "id": "3", 1381 | "label": "3" 1382 | }, 1383 | { 1384 | "color": "7d0cf3", 1385 | "id": "5", 1386 | "label": "5" 1387 | }], 1388 | "to": "0x55c4daf009b0" 1389 | } 1390 | }, 1391 | { 1392 | "type": "Feature", 1393 | "geometry": { 1394 | "type": "LineString", 1395 | "coordinates": [[9.9291390398, 49.7904676977], [9.9295587480, 49.7900979132], [9.9299114953, 49.7898287640], [9.9301263647, 49.7895912966], [9.9304275618, 49.7891994692], [9.9310521245, 49.7883201608], [9.9314082642, 49.7877697905], [9.9316131235, 49.7873598556], [9.9318185496, 49.7868110914]] 1396 | }, 1397 | "properties": { 1398 | "component": 522, 1399 | "dbg_lines": "4,5,3,1", 1400 | "from": "0x55c4da9ce560", 1401 | "id": "0x55c4d5ae4980", 1402 | "lines": [ 1403 | { 1404 | "color": "a70cf3", 1405 | "id": "4", 1406 | "label": "4" 1407 | }, 1408 | { 1409 | "color": "7d0cf3", 1410 | "id": "5", 1411 | "label": "5" 1412 | }, 1413 | { 1414 | "color": "f3850c", 1415 | "id": "3", 1416 | "label": "3" 1417 | }, 1418 | { 1419 | "color": "3df30c", 1420 | "id": "1", 1421 | "label": "1" 1422 | }], 1423 | "to": "0x55c4d9f528d0" 1424 | } 1425 | }, 1426 | { 1427 | "type": "Feature", 1428 | "geometry": { 1429 | "type": "LineString", 1430 | "coordinates": [[9.8930894153, 49.7953193974], [9.8926941353, 49.7950144258], [9.8925980404, 49.7949302937], [9.8925751485, 49.7948941695], [9.8927150759, 49.7947899686], [9.8931672258, 49.7945155318]] 1431 | }, 1432 | "properties": { 1433 | "component": 522, 1434 | "dbg_lines": "2,4", 1435 | "from": "0x55c4dab02c00", 1436 | "id": "0x55c4cf50e250", 1437 | "lines": [ 1438 | { 1439 | "color": "1f0cf3", 1440 | "id": "2", 1441 | "label": "2" 1442 | }, 1443 | { 1444 | "color": "a70cf3", 1445 | "id": "4", 1446 | "label": "4" 1447 | }], 1448 | "to": "0x55c4db056850" 1449 | } 1450 | }, 1451 | { 1452 | "type": "Feature", 1453 | "geometry": { 1454 | "type": "LineString", 1455 | "coordinates": [[9.9466336521, 49.7659957881], [9.9461599815, 49.7661007043], [9.9458077348, 49.7661437272], [9.9454007886, 49.7661463685], [9.9447179586, 49.7660994775], [9.9443510578, 49.7660941912], [9.9439883207, 49.7662173820], [9.9435605808, 49.7663952083], [9.9414360607, 49.7673511453]] 1456 | }, 1457 | "properties": { 1458 | "component": 522, 1459 | "dbg_lines": "3,5", 1460 | "from": "0x55c4daf009b0", 1461 | "id": "0x55c4dbae64e0", 1462 | "lines": [ 1463 | { 1464 | "color": "f3850c", 1465 | "id": "3", 1466 | "label": "3" 1467 | }, 1468 | { 1469 | "color": "7d0cf3", 1470 | "id": "5", 1471 | "label": "5" 1472 | }], 1473 | "to": "0x55c4da3c7960" 1474 | } 1475 | }, 1476 | { 1477 | "type": "Feature", 1478 | "geometry": { 1479 | "type": "LineString", 1480 | "coordinates": [[9.8951749835, 49.7943597432], [9.8950847157, 49.7952410398], [9.8950262623, 49.7956344084], [9.8950002887, 49.7957191790], [9.8949368251, 49.7957836649], [9.8948670955, 49.7958289865], [9.8948095848, 49.7958478168], [9.8947660689, 49.7958509596], [9.8946662312, 49.7958260456], [9.8930894153, 49.7953193974]] 1481 | }, 1482 | "properties": { 1483 | "component": 522, 1484 | "dbg_lines": "2,4", 1485 | "from": "0x55c4daf0eca0", 1486 | "id": "0x55c4ea8cc660", 1487 | "lines": [ 1488 | { 1489 | "color": "1f0cf3", 1490 | "id": "2", 1491 | "label": "2" 1492 | }, 1493 | { 1494 | "color": "a70cf3", 1495 | "id": "4", 1496 | "label": "4" 1497 | }], 1498 | "to": "0x55c4dab02c00" 1499 | } 1500 | }, 1501 | { 1502 | "type": "Feature", 1503 | "geometry": { 1504 | "type": "LineString", 1505 | "coordinates": [[9.9112972568, 49.7943198476], [9.9106013558, 49.7942151246], [9.9100156300, 49.7941575366], [9.9095236528, 49.7941293343], [9.9089504810, 49.7941092837], [9.9078047103, 49.7940936499], [9.9069852453, 49.7941106560], [9.9047956623, 49.7942737837], [9.9038190219, 49.7943163398]] 1506 | }, 1507 | "properties": { 1508 | "component": 522, 1509 | "dbg_lines": "2,4", 1510 | "from": "0x55c4daf1ca80", 1511 | "id": "0x55c4d4dc9040", 1512 | "lines": [ 1513 | { 1514 | "color": "1f0cf3", 1515 | "id": "2", 1516 | "label": "2" 1517 | }, 1518 | { 1519 | "color": "a70cf3", 1520 | "id": "4", 1521 | "label": "4" 1522 | }], 1523 | "to": "0x55c4da221250" 1524 | } 1525 | }, 1526 | { 1527 | "type": "Feature", 1528 | "geometry": { 1529 | "type": "LineString", 1530 | "coordinates": [[9.8981830822, 49.7943226898], [9.8970723398, 49.7943214256], [9.8951749835, 49.7943597432]] 1531 | }, 1532 | "properties": { 1533 | "component": 522, 1534 | "dbg_lines": "2,4", 1535 | "from": "0x55c4daf300f0", 1536 | "id": "0x55c4dc5fcdb0", 1537 | "lines": [ 1538 | { 1539 | "color": "1f0cf3", 1540 | "id": "2", 1541 | "label": "2" 1542 | }, 1543 | { 1544 | "color": "a70cf3", 1545 | "id": "4", 1546 | "label": "4" 1547 | }], 1548 | "to": "0x55c4daf0eca0" 1549 | } 1550 | }, 1551 | { 1552 | "type": "Feature", 1553 | "geometry": { 1554 | "type": "LineString", 1555 | "coordinates": [[9.9440660228, 49.8019009620], [9.9437584649, 49.8014663007], [9.9435646285, 49.8010995882], [9.9433875953, 49.8006174675], [9.9430943998, 49.7995839658], [9.9430591160, 49.7994871416], [9.9430286066, 49.7994480722], [9.9429311176, 49.7993781004], [9.9428317051, 49.7993483376], [9.9416951024, 49.7994220340]] 1556 | }, 1557 | "properties": { 1558 | "component": 522, 1559 | "dbg_lines": "1,5", 1560 | "from": "0x55c4daf7d5f0", 1561 | "id": "0x55c4fb6cb0f0", 1562 | "lines": [ 1563 | { 1564 | "color": "3df30c", 1565 | "id": "1", 1566 | "label": "1" 1567 | }, 1568 | { 1569 | "color": "7d0cf3", 1570 | "id": "5", 1571 | "label": "5" 1572 | }], 1573 | "to": "0x55c4da7a87a0" 1574 | } 1575 | }, 1576 | { 1577 | "type": "Feature", 1578 | "geometry": { 1579 | "type": "LineString", 1580 | "coordinates": [[9.9440660228, 49.8019009620], [9.9445648192, 49.8019531243], [9.9447150800, 49.8019602282], [9.9454023661, 49.8019546133], [9.9464444073, 49.8019086086], [9.9483622244, 49.8017992116]] 1581 | }, 1582 | "properties": { 1583 | "component": 522, 1584 | "dbg_lines": "1,5", 1585 | "from": "0x55c4daf7d5f0", 1586 | "id": "0x55c4f904b540", 1587 | "lines": [ 1588 | { 1589 | "color": "3df30c", 1590 | "id": "1", 1591 | "label": "1" 1592 | }, 1593 | { 1594 | "color": "7d0cf3", 1595 | "id": "5", 1596 | "label": "5" 1597 | }], 1598 | "to": "0x55c4da810e20" 1599 | } 1600 | }, 1601 | { 1602 | "type": "Feature", 1603 | "geometry": { 1604 | "type": "LineString", 1605 | "coordinates": [[9.9159884710, 49.7943768187], [9.9146297478, 49.7945618109], [9.9141985694, 49.7946056223], [9.9140034004, 49.7946152501], [9.9136845071, 49.7946062242], [9.9134130491, 49.7945847257], [9.9112972568, 49.7943198476]] 1606 | }, 1607 | "properties": { 1608 | "component": 522, 1609 | "dbg_lines": "2,4", 1610 | "from": "0x55c4dafe79a0", 1611 | "id": "0x55c4fd582070", 1612 | "lines": [ 1613 | { 1614 | "color": "1f0cf3", 1615 | "id": "2", 1616 | "label": "2" 1617 | }, 1618 | { 1619 | "color": "a70cf3", 1620 | "id": "4", 1621 | "label": "4" 1622 | }], 1623 | "to": "0x55c4daf1ca80" 1624 | } 1625 | }, 1626 | { 1627 | "type": "Feature", 1628 | "geometry": { 1629 | "type": "LineString", 1630 | "coordinates": [[9.8931672258, 49.7945155318], [9.8951749835, 49.7943597432]] 1631 | }, 1632 | "properties": { 1633 | "component": 522, 1634 | "dbg_lines": "2,4", 1635 | "from": "0x55c4db056850", 1636 | "id": "0x55c4ea3ceb50", 1637 | "lines": [ 1638 | { 1639 | "color": "1f0cf3", 1640 | "id": "2", 1641 | "label": "2" 1642 | }, 1643 | { 1644 | "color": "a70cf3", 1645 | "id": "4", 1646 | "label": "4" 1647 | }], 1648 | "to": "0x55c4daf0eca0" 1649 | } 1650 | }, 1651 | { 1652 | "type": "Feature", 1653 | "geometry": { 1654 | "type": "LineString", 1655 | "coordinates": [[9.9310194666, 49.7942649170], [9.9309559202, 49.7934935140], [9.9309304463, 49.7934381201], [9.9308761683, 49.7933974448], [9.9307552077, 49.7933670759], [9.9305933881, 49.7933493736], [9.9288139382, 49.7932039643], [9.9287014512, 49.7931887172], [9.9286352184, 49.7931561790], [9.9285995425, 49.7931195023], [9.9285851834, 49.7930288266], [9.9285996460, 49.7923584452]] 1656 | }, 1657 | "properties": { 1658 | "component": 522, 1659 | "dbg_lines": "4,5,3,1", 1660 | "from": "0x55c4db08a060", 1661 | "id": "0x55c4d31a9170", 1662 | "lines": [ 1663 | { 1664 | "color": "a70cf3", 1665 | "id": "4", 1666 | "label": "4" 1667 | }, 1668 | { 1669 | "color": "7d0cf3", 1670 | "id": "5", 1671 | "label": "5" 1672 | }, 1673 | { 1674 | "color": "f3850c", 1675 | "id": "3", 1676 | "label": "3" 1677 | }, 1678 | { 1679 | "color": "3df30c", 1680 | "id": "1", 1681 | "label": "1" 1682 | }], 1683 | "to": "0x55c4ce14d650" 1684 | } 1685 | }, 1686 | { 1687 | "type": "Feature", 1688 | "geometry": { 1689 | "type": "LineString", 1690 | "coordinates": [[9.9312558412, 49.7971758167], [9.9323891223, 49.7974737036], [9.9327263493, 49.7975494595], [9.9332369815, 49.7976487314], [9.9333845677, 49.7977104890], [9.9344160497, 49.7993675898], [9.9347211335, 49.7998144426], [9.9348549852, 49.7999900394], [9.9349763769, 49.8001337501], [9.9351817800, 49.8003255148], [9.9355598313, 49.8005417588]] 1691 | }, 1692 | "properties": { 1693 | "component": 522, 1694 | "dbg_lines": "1,5,3,2,4", 1695 | "from": "0x55c4de27fd80", 1696 | "id": "0x55c4dc573d60", 1697 | "lines": [ 1698 | { 1699 | "color": "3df30c", 1700 | "id": "1", 1701 | "label": "1" 1702 | }, 1703 | { 1704 | "color": "7d0cf3", 1705 | "id": "5", 1706 | "label": "5" 1707 | }, 1708 | { 1709 | "color": "f3850c", 1710 | "id": "3", 1711 | "label": "3" 1712 | }, 1713 | { 1714 | "color": "1f0cf3", 1715 | "id": "2", 1716 | "label": "2" 1717 | }, 1718 | { 1719 | "color": "a70cf3", 1720 | "id": "4", 1721 | "label": "4" 1722 | }], 1723 | "to": "0x55c4da816550" 1724 | } 1725 | }, 1726 | { 1727 | "type": "Feature", 1728 | "geometry": { 1729 | "type": "LineString", 1730 | "coordinates": [[9.9312558412, 49.7971758167], [9.9311520104, 49.7960777435], [9.9310194666, 49.7942649170]] 1731 | }, 1732 | "properties": { 1733 | "component": 522, 1734 | "dbg_lines": "4,5,3,1", 1735 | "from": "0x55c4de27fd80", 1736 | "id": "0x55c4e21db300", 1737 | "lines": [ 1738 | { 1739 | "color": "a70cf3", 1740 | "id": "4", 1741 | "label": "4" 1742 | }, 1743 | { 1744 | "color": "7d0cf3", 1745 | "id": "5", 1746 | "label": "5" 1747 | }, 1748 | { 1749 | "color": "f3850c", 1750 | "id": "3", 1751 | "label": "3" 1752 | }, 1753 | { 1754 | "color": "3df30c", 1755 | "id": "1", 1756 | "label": "1" 1757 | }], 1758 | "to": "0x55c4db08a060" 1759 | } 1760 | }, 1761 | { 1762 | "type": "Feature", 1763 | "geometry": { 1764 | "type": "LineString", 1765 | "coordinates": [[9.9312558412, 49.7971758167], [9.9305346250, 49.7970993481], [9.9300374029, 49.7970178753], [9.9294296503, 49.7968940743], [9.9284074912, 49.7966660440]] 1766 | }, 1767 | "properties": { 1768 | "component": 522, 1769 | "dbg_lines": "2,4", 1770 | "from": "0x55c4de27fd80", 1771 | "id": "0x55c4ce8b3b70", 1772 | "lines": [ 1773 | { 1774 | "color": "1f0cf3", 1775 | "id": "2", 1776 | "label": "2" 1777 | }, 1778 | { 1779 | "color": "a70cf3", 1780 | "id": "4", 1781 | "label": "4" 1782 | }], 1783 | "to": "0x55c4da82e460" 1784 | } 1785 | }, 1786 | { 1787 | "type": "Feature", 1788 | "geometry": { 1789 | "type": "LineString", 1790 | "coordinates": [[9.9480523802, 49.8024082753], [9.9461807876, 49.8026245402], [9.9453218120, 49.8027112913], [9.9447643483, 49.8027270155], [9.9444412728, 49.8027147117], [9.9442799370, 49.8026990685], [9.9441315916, 49.8026648551], [9.9441071187, 49.8026505844], [9.9440848472, 49.8026105311], [9.9440606889, 49.8024266769], [9.9440660228, 49.8019009620]] 1791 | }, 1792 | "properties": { 1793 | "component": 522, 1794 | "dbg_lines": "1,5", 1795 | "from": "0x55c4de5d40a0", 1796 | "id": "0x55c4cff56930", 1797 | "lines": [ 1798 | { 1799 | "color": "3df30c", 1800 | "id": "1", 1801 | "label": "1" 1802 | }, 1803 | { 1804 | "color": "7d0cf3", 1805 | "id": "5", 1806 | "label": "5" 1807 | }], 1808 | "to": "0x55c4daf7d5f0" 1809 | } 1810 | }] 1811 | } --------------------------------------------------------------------------------