summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--algorithms.py65
-rw-r--r--lib.py77
-rw-r--r--server.py37
-rw-r--r--store.py82
-rw-r--r--templates/index.js10
5 files changed, 225 insertions, 46 deletions
diff --git a/algorithms.py b/algorithms.py
index 6e01287..9f3fd78 100644
--- a/algorithms.py
+++ b/algorithms.py
@@ -10,22 +10,75 @@ def length_haversine(p1, p2):
lat1, lng1, lat2, lng2 = map(math.radians, [lat1, lng1, lat2, lng2])
dlat = lat2 - lat1
dlng = lng2 - lng1
- a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlng / 2) ** 2
+ a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(
+ dlng / 2) ** 2
c = 2 * math.asin(math.sqrt(a))
return 6372797.560856 * c # return the distance in meters
-def get_closest_node_id(nodes, source_node):
- """ Search through all nodes and return the id of the node
- that is closest to 'source_node'. """
+def grid_search(grid, source_node):
+ """
+ Finds closest node to source node by comparing distance to nodes within
+ nearest tiles in the grid.
+ """
+ closest_nodes = []
+
+ source_key = (int(round(source_node.lat, 3) * 1000), int(round(
+ source_node.lng, 3) * 1000))
+
+ closest_tiles = find_nodes(0, grid, source_key)
+
+ for tile_nodes in closest_tiles:
+ closest_nodes.append(get_closest_node(tile_nodes, source_node))
+
+ closest_node_id = get_closest_node(closest_nodes, source_node).id
+
+ return closest_node_id
+
+
+def find_nodes(offset, grid, source_key):
+ """
+ Searches a grid in an outward "spiral" from source node fetching all tiles
+ which contain nodes.
+ """
+ tiles = None
+
+ while not tiles:
+ tiles = look_for_nodes(offset, grid, source_key)
+ offset += 1
+
+ return tiles + look_for_nodes(offset + 1, grid, source_key)
+
+
+def look_for_nodes(offset, grid, source_key):
+ """Search for nearest tile containing nodes in an outward spiral."""
+ tiles = []
+ for i in range(-offset, offset + 1):
+ for j in range(-offset, offset + 1):
+
+ if i in (-offset, offset) or j in (-offset, offset):
+
+ key = (source_key[0] + j, source_key[1] + i)
+
+ if key in grid.keys():
+ tiles.append(grid[key])
+
+ return tiles
+
+
+def get_closest_node(nodes, source_node):
+ """
+ Searches through all nodes in a specified grid and return node
+ closes to source node.
+ """
min_node = None
min_value = None
- for node_id, node in nodes.items():
+ for node in nodes:
length = length_haversine(source_node, node)
if min_node is None or length < min_value:
- min_node = node_id
+ min_node = node
min_value = length
return min_node
diff --git a/lib.py b/lib.py
index 3b19daa..8a31276 100644
--- a/lib.py
+++ b/lib.py
@@ -1,31 +1,57 @@
import os
import re
+import sys
+import traceback
from http.server import HTTPServer, BaseHTTPRequestHandler
+try:
+ from pygments import highlight
+ from pygments.lexers import PythonLexer
+ from pygments.formatters import HtmlFormatter
+ have_pygment = True
+except ModuleNotFoundError:
+ have_pygment = False
+
posts = {}
gets = {}
+def format_py(code):
+ if have_pygment:
+ formatter = HtmlFormatter()
+ formatter.noclasses = True # inline styles
+ return highlight(code, PythonLexer(), formatter)
+ else:
+ return "<code>{}</code>".format(code).replace("\n", "\n<br>") \
+ .replace(" ", "&nbsp;")
+
+
def find_get(path):
""" [Internal] - Used by the Handler class to try and
find the correct handler for a GET request.
"""
+ if path not in gets:
+ return 404, ""
try:
- return gets[path]()
- except Exception as e:
- print(type(e), e)
- return "404"
+ return 200, gets[path]()
+ except Exception:
+ tb = traceback.format_exc()
+ print(tb, file=sys.stderr)
+ return 500, format_py(tb)
def find_post(path, request):
""" [Internal] - Used by the Handler class to try and
find the correct handler for a POST request.
"""
+ if path not in posts:
+ return 404, ""
try:
- return posts[path](request)
- except Exception as e:
- print(type(e), e)
- return "404"
+ return 200, posts[path](request)
+ except Exception:
+ tb = traceback.format_exc()
+ print(tb, file=sys.stderr)
+ return 500, ""
def get_relative_path():
@@ -35,7 +61,7 @@ def get_relative_path():
def read_html(relative_file_path):
""" Reads the html file on the file location provided
and injects any external files (marked with {{file_path}} )
- that may be present. Returns the html content as a string.
+ that may be present. Returns the html content as a string.
Example:
>>> html_string = read_html('templates/index.html')
@@ -49,8 +75,8 @@ def read_html(relative_file_path):
def inject_external_files(html_content):
- """ [Internal] - Replaces {{ 'file_path' }} with the file content of that file
- path. Useful for seperation of javascript and css files.
+ """ [Internal] - Replaces {{ 'file_path' }} with the file content of that file
+ pat. Useful for seperation of javascript and css files.
Uses regex to capture the pattern. Here is a link to see how it
works: https://regex101.com/r/v917NK/2
"""
@@ -76,8 +102,8 @@ def inject_external_files(html_content):
def post(route='/'):
""" A decorator that takes in the route path as argument, and then
- puts the post handler in the dictionary, with the route as a key.
- The handler will receive one `str` argument called 'body',
+ puts the post handler in the dictionary, with the route as a key.
+ The handler will receive one `str` argument called 'body',
where the body of the post request will exist. """
def decorator(handler_function):
@@ -106,21 +132,14 @@ class Handler(BaseHTTPRequestHandler):
different requests made to our server.
"""
- def _set_headers(self):
- """ Sets the headers for a response. This is to tell the browser
- the status of the request that was sent. '200' means that everything
- went fine.
- """
- self.send_response(200)
- self.send_header("Content-type", "text/html")
- self.end_headers()
-
def do_GET(self):
""" Tries to find the handler for this GET request. The handler can
- return either `bytes` or `str`.
+ return either `bytes` or `str`.
"""
- self._set_headers()
- res = find_get(self.path)
+ status, res = find_get(self.path)
+ self.send_response(status)
+ self.send_header("Content-type", "text/html")
+ self.end_headers()
if hasattr(res, 'encode'):
self.wfile.write(res.encode())
else:
@@ -131,11 +150,11 @@ class Handler(BaseHTTPRequestHandler):
will get the 'body' of the request as the argument. The body
is a `str`, formatted in JSON.
"""
- self.send_response(200)
- self.end_headers()
content_length = int(self.headers['Content-Length'])
body = self.rfile.read(content_length)
- res = find_post(self.path, body.decode('utf-8'))
+ status, res = find_post(self.path, body.decode('utf-8'))
+ self.send_response(status)
+ self.end_headers()
self.wfile.write(res.encode())
@@ -143,4 +162,4 @@ def run_server(port=8314):
server_address = ('127.0.0.1', port) # Should make the port a command line argument
server = HTTPServer(server_address, Handler)
print('Starting server on http://{}:{}.'.format(*server_address))
- server.serve_forever()
+ server.serve_forever() \ No newline at end of file
diff --git a/server.py b/server.py
index 5bf2a49..90e135e 100644
--- a/server.py
+++ b/server.py
@@ -4,30 +4,55 @@ import algorithms
import store
from lib import run_server, get, post, read_html
-
+grid = None
nodes = None
-
+unconnected_nodes = None
@get('/')
def index():
global nodes
- nodes = store.extract_osm_nodes("linkoping.osm")
+ global grid
+ global unconnected_nodes
+
+ nodes, grid, unconnected_nodes = store.extract_osm_nodes("linkoping.osm")
+
return read_html('templates/index.html')
@get('/show-area')
def show_area():
rect = dict()
- for (k, node) in enumerate(store.select_nodes_in_rectangle(nodes, 58.3984, 58.3990, 15.5733, 15.576)):
+ for (k, node) in enumerate(
+ store.select_nodes_in_rectangle(nodes, 58.3984, 58.3990, 15.5733,
+ 15.576)):
rect[node.id] = node.coord_tuple()
return json.dumps(rect)
+@get('/favicon.ico')
+def favicon():
+ with open("data/favicon.ico", "rb") as image:
+ read_image = image.read()
+ image_bytes = bytearray(read_image)
+
+ return image_bytes
+
+
+@get('/show-unconnected-nodes')
+def show_unconnected_nodes():
+ print(f"Showing {len(unconnected_nodes)} unconnected nodes")
+ return json.dumps({node.id: node.coord_tuple() for node in unconnected_nodes})
+
+
@post('/shortest-path')
def shortest_path(body):
body = json.loads(body)
- source_id = algorithms.get_closest_node_id(nodes, store.Node(-1, body['lat1'], body['lng1']))
- target_id = algorithms.get_closest_node_id(nodes, store.Node(-1, body['lat2'], body['lng2']))
+ source_id = algorithms.grid_search(grid,
+ store.Node(-1, body['lat1'],
+ body['lng1']))
+ target_id = algorithms.grid_search(grid,
+ store.Node(-1, body['lat2'],
+ body['lng2']))
path = algorithms.find_shortest_path(nodes, source_id, target_id)
response = {"path": [(nodes[node].lat, nodes[node].lng) for node in path]}
diff --git a/store.py b/store.py
index daae28f..87cd940 100644
--- a/store.py
+++ b/store.py
@@ -1,4 +1,5 @@
from osm_parser import get_default_parser
+from collections import defaultdict
class Node:
@@ -8,6 +9,34 @@ class Node:
self.lng = float(lng)
self.neighbours = []
+ self.parent = None
+ self.size = 1
+
+ def find_root(self):
+ """ Recursively find the root of this node. """
+ if not self.parent:
+ return self
+ else:
+ return self.parent.find_root()
+
+ def union(self, other):
+ """ Mark this node and some other node as part of the same tree. """
+ this = self.find_root()
+ other = other.find_root()
+
+ # If we share the same root we are already part of the same tree
+ if this == other:
+ return
+
+ # Minimize tree size. This ensures optimal complexity.
+ # We could also store the rank instead.
+ if this.size < other.size:
+ this, other = other, this
+
+ # This is safe since other is a root node and currently doesn't have a
+ # parent.
+ other.parent = this
+ this.size += other.size
def coord_tuple(self):
return self.lat, self.lng
@@ -26,6 +55,8 @@ def add_neighbours(nodes, parser):
nodes[node1].neighbours.append(nodes[node2])
nodes[node2].neighbours.append(nodes[node1])
+ # These two are neighbours and should be part of the same tree.
+ nodes[node1].union(nodes[node2])
return nodes
@@ -33,18 +64,63 @@ def add_neighbours(nodes, parser):
def extract_osm_nodes(f_name):
parser = get_default_parser(f_name)
nodes = dict()
+ grid = defaultdict()
for node in parser.iter_nodes():
- nodes[node['id']] = Node(node['id'], node['lat'], node['lon'])
+ new_node = Node(node['id'], node['lat'], node['lon'])
+ nodes[node['id']] = new_node
add_neighbours(nodes, parser)
- # remove nodes without neighbours
+ # Remove nodes without neighbours
for node_id, node in nodes.copy().items():
if not node.neighbours:
del nodes[node_id]
- return nodes
+ # Construct a forest of disjoint trees.
+ # The forest is a { root: tree }-dict. If we construct a relation where
+ # two nodes relate to each other if they are a part of the same tree,
+ # this can be shown to be an equivalence relation where the equivalence
+ # class is some node in the tree. In our case, the equivalence class
+ # becomes the root of each node since it will be the same for all nodes
+ # in the same tree due our use of union-find.
+ forest = defaultdict(dict)
+ for node_id, node in nodes.items():
+ forest[node.find_root().id][node_id] = node
+
+ # Find the largest disjoint tree.
+ # We store the root so we can easily test if a node is part of this
+ # tree or not.
+ best_size = 0
+ best_tree = None
+ best_root = None
+ for root in forest:
+ tree = forest[root]
+ size = len(tree)
+ if size > best_size:
+ best_size = size
+ best_tree = tree
+ best_root = root
+
+ unconnected_nodes = []
+ for _, node in nodes.items():
+ if node.find_root().id != best_root:
+ unconnected_nodes.append(node)
+
+ nodes = best_tree
+ # create a "grid" by grouping nearby nodes.
+ for node in nodes.copy().values():
+
+ key = (int(round(node.lat, 3) * 1000), int(round(node.lng, 3)
+ * 1000))
+
+ if key in grid.keys():
+ grid[key].append(node)
+
+ else:
+ grid[key] = [node]
+
+ return nodes, grid, unconnected_nodes
def select_nodes_in_rectangle(nodes, min_lat, max_lat, min_long, max_long):
diff --git a/templates/index.js b/templates/index.js
index b7b3b58..01e3880 100644
--- a/templates/index.js
+++ b/templates/index.js
@@ -49,7 +49,6 @@ map.on('click', handleMapClick)
function showMarker(){
var marker = L.marker([58.3984, 15.5733]).addTo(map)
}
-
showMarker()
async function showMarkers() {
@@ -59,5 +58,12 @@ async function showMarkers() {
var marker = L.marker(res[key]).addTo(map)
}
}
-
showMarkers()
+
+async function showUnconnectedMarkers() {
+ const res = await fetch('/show-unconnected-nodes').then(res => res.json());
+ for (let key in res) {
+ var marker = L.marker(res[key]).addTo(map)
+ }
+}
+showUnconnectedMarkers()