Browse Source

Update app to use GraphHopper API

Phyks (Lucas Verney) 2 years ago
parent
commit
eeb1b929ef
3 changed files with 196 additions and 52 deletions
  1. 184
    47
      app.py
  2. 10
    5
      index.html
  3. 2
    0
      requirements.txt

+ 184
- 47
app.py View File

@@ -1,3 +1,7 @@
1
+#!/usr/bin/env python
2
+# coding: utf-8
3
+import subprocess
4
+
1 5
 import bottle
2 6
 import polyline
3 7
 
@@ -18,71 +22,204 @@ def enable_cors():
18 22
     )
19 23
 
20 24
 
21
-@bottle.route("/:service/:version/:profile/:coordinates", ["GET", "OPTIONS"])
22
-def index(service, version, profile, coordinates):
25
+def _build_rv2_input(points, **kwargs):
26
+    """
27
+    Build a ``get_route`` input from arguments.
28
+
29
+    ..note::
30
+        See https://gitlab.crans.org/leger/rv2/blob/master/doc/using_get_route.md
31
+        for more infos about available arguments.
32
+
33
+    :param points: A list of (lng, lat) points.
34
+    :return: The input to pass to ``get_route`` program.
35
+    """
36
+    rv_input = []
37
+    for k, v in kwargs.items():
38
+        rv_input.append("%s %f" % (k, v))
39
+    rv_input.append("points")
40
+    for point in points:
41
+        rv_input.append("%f %f" % (point[0], point[1]))
42
+    rv_input.append("route")
43
+    return "\n".join(rv_input)
44
+
45
+
46
+def _parse_rv2_route(pcroute_lines):
47
+    """
48
+    Parse a ``rv2`` route.
49
+
50
+    :param pcroute_lines: The ``rv2`` route (list of lines).
51
+    :return: The parsed route.
52
+    """
23 53
     points = []
24
-    with open("pcroute.txt", "r") as fh:
25
-        for line in fh:
26
-            if not line.startswith("nid:"):
27
-                continue
28
-            line = line.split()
29
-            lat = next(
30
-                x for x in line if x.startswith("lat")
31
-            )
32
-            lng = next(
33
-                x for x in line if x.startswith("lon")
34
-            )
35
-            elevation = next(
36
-                x for x in line if x.startswith("elevation")
37
-            )
38
-            distance = next(
39
-                x for x in line if x.startswith("distance")
40
-            )
41
-            duration = next(
42
-                x for x in line if x.startswith("time")
43
-            )
44
-            energy = next(
45
-                x for x in line if x.startswith("energy")
46
-            )
47
-            points.append({
48
-                "lat": float(lat.replace("lat:", "")),
49
-                "lng": float(lng.replace("lon:", "")),
50
-                "elevation": float(elevation.replace("elevation:", "")),
51
-                "distance": float(distance.replace("distance:", "")),
52
-                "duration": float(duration.replace("time:", "")),
53
-                "energy": float(energy.replace("energy:", ""))
54
-            })
55
-    geometry = polyline.encode([
56
-        (point["lat"], point["lng"]) for point in points
54
+    for line in pcroute_lines:
55
+        line = line.strip()
56
+        if not line.startswith("nid:"):
57
+            continue
58
+        point_dict = dict(
59
+            field.split(":")[:2]
60
+            for field in line.split()
61
+        )
62
+        point_dict["nid"] = int(point_dict["nid"])
63
+        for k in ["lon", "lat", "elevation", "velocity", "distance",
64
+                  "energy", "time", "power"]:
65
+            point_dict[k] = float(point_dict[k])
66
+        points.append(point_dict)
67
+    return points
68
+
69
+
70
+def _call_rv2(rv_input, rv_bin="get_route"):
71
+    """
72
+    Call rv2 ``get_route`` program and return the parsed route.
73
+
74
+    :param rv_input: Input for the ``get_route`` program.
75
+    :param rv_bin: Path to the binary ``get_route``. Defaults to having
76
+        ``get_route`` in your path.
77
+    :return: The fetched route.
78
+    """
79
+    process = subprocess.Popen(
80
+        [rv_bin],
81
+        stdin=subprocess.PIPE,
82
+        stderr=subprocess.PIPE,
83
+        stdout=subprocess.PIPE
84
+    )
85
+    rv_output, _ = process.communicate(rv_input)
86
+    return _parse_rv2_route(rv_output.splitlines())
87
+
88
+
89
+def _load_pcroute(filename="pcroute.txt"):
90
+    """
91
+    Load a ``pcroute.txt`` file as returned by rv2.
92
+
93
+    :param filename: Filename of the file to load. Defaults to ``pcroute.txt``.
94
+    :return: The loaded route.
95
+    """
96
+    with open(filename, "r") as fh:
97
+        return _parse_rv2_route(fh)
98
+
99
+
100
+def _compute_geometry(points):
101
+    """
102
+    Compute an encoded geometry according to description of points.
103
+
104
+    :param points: An array of points from rv2.
105
+    :returns: A string representing the polyline.
106
+    """
107
+    return polyline.encode([
108
+        (float(point["lat"]), float(point["lon"])) for point in points
57 109
     ], 5)
110
+
111
+
112
+@bottle.route("/api/1/route", ["GET", "OPTIONS"])
113
+def to_graphhopper():
114
+    """
115
+    Expose a GraphHopper-like API with rv2 backend.
116
+    """
117
+    query_points = bottle.request.query.getall('point')
118
+    if len(query_points) < 2:
119
+        return bottle.abort(400, "Invalid coordinates.")
120
+    query_points = [
121
+        (float(x.split(",")[0]), float(x.split(",")[1]))
122
+        for x in query_points
123
+    ]
124
+
125
+    points = _call_rv2(_build_rv2_input(query_points))
126
+    geometry = _compute_geometry(points)
127
+
128
+    min_lat = min(point["lat"] for point in points)
129
+    max_lat = max(point["lat"] for point in points)
130
+    min_lng = min(point["lon"] for point in points)
131
+    max_lng = max(point["lon"] for point in points)
132
+
133
+    return {
134
+        "paths": [{
135
+            "bbox": [
136
+                min_lng, min_lat, max_lng, max_lat
137
+            ],
138
+            "distance": points[-1]["distance"],
139
+            "instructions": [
140
+                {
141
+                    "distance": point["distance"] - points[i-1]["distance"],
142
+                    "interval": [i-1, i],
143
+                    # TODO: Better encoding of instructions
144
+                    "sign": 0,
145
+                    "text": "",
146
+                    "time": point["time"] - points[i-1]["time"]
147
+                }
148
+                for i, point in enumerate(points)
149
+                if i >= 1
150
+            ],
151
+            "points": geometry,
152
+            "points_encoded": True,
153
+            "time": points[-1]["time"] * 1000
154
+        }]
155
+    }
156
+
157
+
158
+@bottle.route("/:service/:version/:profile/:coordinates", ["GET", "OPTIONS"])
159
+def to_osrm(service, version, profile, coordinates):
160
+    """
161
+    Expose as an API in same format as OSRM API
162
+
163
+    ..note::
164
+        OSRM API is very complete and not all data is available in current rv2
165
+        version, so this is not fully working.
166
+    """
167
+    points = _load_pcroute()
168
+    geometry = _compute_geometry(points)
169
+
58 170
     return {
59 171
         "routes": [
60 172
             {
173
+                "distance": points[-1]["distance"],
174
+                "duration": points[-1]["time"],
175
+                "weight": points[-1]["energy"],
176
+                "weight_name": "energy",
61 177
                 "geometry": geometry,
62 178
                 "legs": [
63 179
                     {
64
-                        "summary": "",
180
+                        "summary": "",  # TODO
181
+                        "distance": points[-1]["distance"],
182
+                        "duration": points[-1]["time"],
65 183
                         "weight": points[-1]["energy"],
66
-                        "duration": points[-1]["duration"],
67 184
                         "steps": [
185
+                            {
186
+                                "distance": (
187
+                                    point["distance"] - points[i-1]["distance"]
188
+                                ),
189
+                                "duration": (
190
+                                    point["time"] - points[i-1]["time"]
191
+                                ),
192
+                                "geometry": "",
193
+                                "weight": (
194
+                                    point["energy"] - points[i-1]["energy"]
195
+                                ),
196
+                                "name": "",
197
+                                "mode": "",
198
+                                "maneuver": {
199
+                                    "location": (None, None),
200
+                                    "bearing_before": 0,
201
+                                    "bearing_after": 0,
202
+                                    "type": "turn"
203
+                                },
204
+                                "intersections": [{
205
+                                }]
206
+                            }
207
+                            for i, point in enumerate(points)
208
+                            if i >= 1
68 209
                         ],
69
-                        "distance": points[-1]["distance"]
210
+                        "annotations": {}  # TODO
70 211
                     }
71
-                ],
72
-                "weight_name": "routability",
73
-                "weight": points[-1]["energy"],
74
-                "duration": points[-1]["duration"],
75
-                "distance": points[-1]["distance"]
212
+                ]
76 213
             }
77 214
         ],
78 215
         "waypoints": [
79 216
             {
80
-                "name": "Start",
81
-                "location": [points[0]["lng"], points[0]["lat"]]
217
+                "name": points[0]["nid"],
218
+                "location": [points[0]["lon"], points[0]["lat"]]
82 219
             },
83 220
             {
84
-                "name": "End",
85
-                "location": [points[-1]["lng"], points[-1]["lat"]]
221
+                "name": points[-1]["nid"],
222
+                "location": [points[-1]["lon"], points[-1]["lat"]]
86 223
             }
87 224
         ],
88 225
         "code": "Ok"

+ 10
- 5
index.html View File

@@ -17,6 +17,7 @@
17 17
     <div id="map" class="map"></div>
18 18
     <script src="https://unpkg.com/leaflet@1.2.0/dist/leaflet.js"></script>
19 19
     <script src="https://unpkg.com/leaflet-routing-machine@latest/dist/leaflet-routing-machine.js"></script>
20
+    <script src="node_modules/lrm-graphhopper/dist/lrm-graphhopper.js"></script>
20 21
     <script type="text/javascript">
21 22
 var map = L.map('map');
22 23
 
@@ -26,13 +27,17 @@ L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}{r}.png', {
26 27
 
27 28
 L.Routing.control({
28 29
     waypoints: [
29
-        L.latLng(57.74, 11.94),
30
-        L.latLng(57.6792, 11.949)
30
+        L.latLng(48.818783, 2.319883),
31
+        L.latLng(48.841132, 2.384841)
31 32
     ],
32 33
     routeWhileDragging: true,
33
-    router: new L.Routing.OSRMv1({
34
-        serviceUrl: "http://127.0.0.1:8081/route/v1"
35
-    })
34
+    router:
35
+        new L.Routing.GraphHopper('', {
36
+            serviceUrl: "http://127.0.0.1:8081/api/1/route"
37
+        })
38
+        /* new L.Routing.OSRMv1({
39
+            serviceUrl: "http://127.0.0.1:8081/route/v1"
40
+        } */
36 41
 }).addTo(map);
37 42
 </script>
38 43
 </body>

+ 2
- 0
requirements.txt View File

@@ -0,0 +1,2 @@
1
+bottle
2
+polyline