def create_places(graph, bounds, viewport_size, limit): """Convert a graph of rooms to a graph of 'places'. A 'place' is a set of one or more rooms. The position of a place is the average of the positions of its rooms. The places are created such that no two places are closer than limit to each other. Each place node has a property 'rooms' (available as placenode.properties['rooms']) which is a list of the room nodes it is based on. Arguments: graph -- a Graph object. It is destructively modified. bounds -- a dictionary with keys (minLon, maxLon, minLat, maxLat) describing the bounds of the interesting region. viewport_size -- a dictionary with keys (width, height), the width and height of the user's viewport for the map in pixels. limit -- the minimum distance (in pixels) there may be between two points without them being collapsed to one. """ # TODO: # # -- This may give division by zero with bogus input (should check # for zeros -- what should we do then?) # # -- Should take into account that longitudes wrap around. Is # there any way to detect whether we have a map wider than the # earth, or do we need an extra parameter? width = bounds['maxLon'] - bounds['minLon'] height = bounds['maxLat'] - bounds['minLat'] lon_scale = float(viewport_size['width']) / width lat_scale = float(viewport_size['height']) / height def square(var): """Square a number""" return var * var def distance(node1, node2): """Calculate distance from node1 to node2""" return sqrt(square((node1.lon - node2.lon) * lon_scale) + square((node1.lat - node2.lat) * lat_scale)) places = [] for node in graph.nodes.values(): for place in places: if distance(node, place['position']) < limit: place['rooms'].append(node) place['position'].lon = avg([n.lon for n in place['rooms']]) place['position'].lat = avg([n.lat for n in place['rooms']]) break else: places.append({'position': Node(None, node.lon, node.lat, None), 'rooms': [node]}) collapse_nodes(graph, [place['rooms'] for place in places], AGGREGATE_PROPERTIES_PLACE)
def collapse_nodes(graph, node_sets, property_aggregators): """Collapse sets of nodes to single nodes. Replaces each set of nodes in node_sets by a single (new) node and redirects the edges correspondingly. Edges which would end up having both endpoints in the same node are removed. Each new node is positioned at the average of the positions of the node set it represents. It also gets a property containing the original nodes; the name of this property is given by subnode_list_name. Properties from the original nodes may be combined to form aggregate values in the new node. The property_aggregators argument determines how (and whether) this is done. Some useful aggregator functions are sum and avg (for numbers) and lambda lst: ', '.join(map(str, lst)). Arguments: graph -- a Graph object. It is destructively modified. node_sets -- a list of lists of nodes in graph. Each node should occur in exactly one of the lists. subnode_list_name -- name for the property containing the original nodes a newly created node represents. property_aggregators -- describes how to create aggregate properties. Dictionary with names of properties as keys and aggregator functions as corresponding values. Each aggregator function should take a single argument, a list. """ if property_aggregators is None: property_aggregators = {} graph.nodes = {} nodehash = {} for node_set in node_sets: properties = aggregate_properties([x.properties for x in node_set], property_aggregators) new_node = Node( 'cn[%s]' % combine_ids(node_set), avg([n.lon for n in node_set]), avg([n.lat for n in node_set]), properties, ) for node in node_set: nodehash[node.id] = new_node graph.add_node(new_node) # Now nodehash maps original node ids to new node objects. Use it # to redirect the edges to the new nodes: for edge in graph.edges.values(): edge.source = nodehash[edge.source.id] edge.target = nodehash[edge.target.id] graph.edges = filter_dict(lambda e: e.source != e.target, graph.edges)
def collapse_nodes(graph, node_sets, property_aggregators): """Collapse sets of nodes to single nodes. Replaces each set of nodes in node_sets by a single (new) node and redirects the edges correspondingly. Edges which would end up having both endpoints in the same node are removed. Each new node is positioned at the average of the positions of the node set it represents. It also gets a property containing the original nodes; the name of this property is given by subnode_list_name. Properties from the original nodes may be combined to form aggregate values in the new node. The property_aggregators argument determines how (and whether) this is done. Some useful aggregator functions are sum and avg (for numbers) and lambda lst: ', '.join(map(str, lst)). Arguments: graph -- a Graph object. It is destructively modified. node_sets -- a list of lists of nodes in graph. Each node should occur in exactly one of the lists. subnode_list_name -- name for the property containing the original nodes a newly created node represents. property_aggregators -- describes how to create aggregate properties. Dictionary with names of properties as keys and aggregator functions as corresponding values. Each aggregator function should take a single argument, a list. """ if property_aggregators is None: property_aggregators = {} graph.nodes = {} nodehash = {} for node_set in node_sets: properties = aggregate_properties( [x.properties for x in node_set], property_aggregators) new_node = Node('cn[%s]' % combine_ids(node_set), avg([n.lon for n in node_set]), avg([n.lat for n in node_set]), properties) for node in node_set: nodehash[node.id] = new_node graph.add_node(new_node) # Now nodehash maps original node ids to new node objects. Use it # to redirect the edges to the new nodes: for edge in graph.edges.values(): edge.source = nodehash[edge.source.id] edge.target = nodehash[edge.target.id] graph.edges = filter_dict(lambda e: e.source != e.target, graph.edges)