From 163e0e76b8caf106a83a82204225009fc908253b Mon Sep 17 00:00:00 2001 From: Kalil Date: Sun, 21 Aug 2022 17:30:56 -0500 Subject: [PATCH 1/4] Add heading to route segments and ensure they're in order --- app/mbm/views.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/mbm/views.py b/app/mbm/views.py index c077a07..371b9bc 100644 --- a/app/mbm/views.py +++ b/app/mbm/views.py @@ -111,7 +111,9 @@ def get_route(self, source_vertex_id, target_vertex_id, enable_v2=False): way.name, way.length_m, ST_AsGeoJSON(way.the_geom) AS geometry, - mellow.type + DEGREES(ST_AZIMUTH(ST_StartPoint(way.the_geom), ST_EndPoint(way.the_geom))) AS heading, + mellow.type, + path.seq FROM pgr_dijkstra( 'WITH mellow AS ( SELECT DISTINCT(UNNEST(ways)) AS osm_id, type @@ -153,6 +155,7 @@ def get_route(self, source_vertex_id, target_vertex_id, enable_v2=False): FROM mbm_mellowroute ) as mellow USING(osm_id) + ORDER BY path.seq """, [source_vertex_id, target_vertex_id]) rows = fetchall(cursor) @@ -173,7 +176,9 @@ def get_route(self, source_vertex_id, target_vertex_id, enable_v2=False): 'geometry': json.loads(row['geometry']), 'properties': { 'name': row['name'], - 'type': row['type'] + 'type': row['type'], + 'distance': row['length_m'], + 'heading': row['heading'], } } for row in rows @@ -184,7 +189,7 @@ def format_distance(self, dist_in_meters): """ Given a distance in meters, return a tuple (distance, time) where `distance` is a string representing a distance in miles and - `time` is a string representing an estimated travelime in minutes. + `time` is a string representing an estimated travel time in minutes. """ meters_per_mi = 1609.344 dist_in_mi = dist_in_meters / meters_per_mi From 61fa26ba6e07cc5865c120be640c60e16d5474ae Mon Sep 17 00:00:00 2001 From: Kalil Date: Sun, 21 Aug 2022 17:32:27 -0500 Subject: [PATCH 2/4] JS sketch of turn by turn directions (not yet using backend calculated headings) --- app/mbm/static/js/app.js | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/app/mbm/static/js/app.js b/app/mbm/static/js/app.js index 27576e1..f014067 100644 --- a/app/mbm/static/js/app.js +++ b/app/mbm/static/js/app.js @@ -267,6 +267,61 @@ export default class App { } else { this.map.spin(true) $.getJSON(this.routeUrl + '?' + $.param({ source, target, enable_v2: enableV2 })).done((data) => { + const angle = ([lnga, lata], [lngb, latb]) => { + return (Math.atan2(Math.cos(latb) * Math.sin(lngb - lnga), Math.cos(lata) * Math.sin(latb) - Math.sin(lata) * Math.cos(latb) * Math.cos(lngb - lnga)) * 180 / Math.PI) + } + + const turnToEnglish = (oldHeading, newHeading) => { + const turn = newHeading - oldHeading + if (turn < 100 && turn > 80) { + return "Turn right" + } + if (turn < -80 && turn > -100) { + return "Turn left" + } + if (turn < 10 && turn > -10) { + return "Continue" + } + if (Math.abs(turn) > 170 && Math.abs(turn) < 190) { + // There are many false u-turns, likely a data problem. Safer to just + // ignore them since this isn't a very common legit direction + // return "Turn around" + return null + } + } + const directionsList = () => { + const directions = [] + let lastHeading, lastName + for (const feature of data.route.features) { + const name = feature.properties.name + const coords = feature.geometry.coordinates + const heading = angle(coords[0], coords[coords.length - 1]) + const turn = lastHeading && turnToEnglish(lastHeading, heading) + console.log(heading) + console.log(lastHeading) + console.log(turn) + const distance = feature.properties.distance + if ((lastName && name !== lastName) || (turn || !lastHeading)) { + directions.push({ name, distance, turn, heading }) + } else { + if (directions.length) { + directions[directions.length - 1].distance += distance + } + if (name && directions.length && !directions[directions.length - 1].name) { + directions[directions.length - 1].name = name + } + } + if (coords.length > 1) { + lastHeading = angle(coords[coords.length - 2], coords[coords.length - 1]) + } else { + lastHeading = heading + } + lastName = name + } + return directions + } + console.log(directionsList()) + if (this.routeLayer) { this.map.removeLayer(this.routeLayer) } From 48dbb3ac18e1c51b868c29718b87645ed33372b4 Mon Sep 17 00:00:00 2001 From: Kalil Date: Wed, 24 Aug 2022 17:49:59 -0500 Subject: [PATCH 3/4] Make sure line segments along a route are all oriented correctly --- app/mbm/views.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/mbm/views.py b/app/mbm/views.py index 371b9bc..9d24ca1 100644 --- a/app/mbm/views.py +++ b/app/mbm/views.py @@ -110,8 +110,9 @@ def get_route(self, source_vertex_id, target_vertex_id, enable_v2=False): SELECT way.name, way.length_m, - ST_AsGeoJSON(way.the_geom) AS geometry, - DEGREES(ST_AZIMUTH(ST_StartPoint(way.the_geom), ST_EndPoint(way.the_geom))) AS heading, + ST_AsGeoJSON(oriented.the_geom) AS geometry, + -- Calculate the angle between each segment of the route so we can generate turn-by-turn directions + DEGREES(ST_AZIMUTH(ST_StartPoint(oriented.the_geom), ST_EndPoint(oriented.the_geom))) AS heading, mellow.type, path.seq FROM pgr_dijkstra( @@ -154,7 +155,15 @@ def get_route(self, source_vertex_id, target_vertex_id, enable_v2=False): SELECT DISTINCT(UNNEST(ways)) AS osm_id, type FROM mbm_mellowroute ) as mellow - USING(osm_id) + USING(osm_id), + -- Make sure each segment of the route is oriented such that the last point of + -- each line segment is the same as the first point in the next line segment + LATERAL ( + SELECT CASE + WHEN path.node = way.source THEN way.the_geom + ELSE ST_Reverse(way.the_geom) + END AS the_geom + ) as oriented ORDER BY path.seq """, [source_vertex_id, target_vertex_id]) rows = fetchall(cursor) From 243972776408eef852c1959e1cefeba86482b3db Mon Sep 17 00:00:00 2001 From: Kalil Date: Wed, 24 Aug 2022 21:34:39 -0500 Subject: [PATCH 4/4] Finish draft of turn by turn directions --- app/mbm/static/js/app.js | 56 ++------------------------ app/mbm/static/js/turnbyturn.js | 71 +++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 53 deletions(-) create mode 100644 app/mbm/static/js/turnbyturn.js diff --git a/app/mbm/static/js/app.js b/app/mbm/static/js/app.js index f014067..c9b7315 100644 --- a/app/mbm/static/js/app.js +++ b/app/mbm/static/js/app.js @@ -2,6 +2,7 @@ import UserLocations from './userlocations.js' import autocomplete from './autocomplete.js' import Geolocation from './geolocation.js' import { getUserPreferences, saveUserPreferences } from './storage.js' +import { serializeDirections, directionsList } from './turnbyturn.js' // The App class holds top level state and map related methods that other modules // need to call, for example to update the position of markers. export default class App { @@ -267,60 +268,9 @@ export default class App { } else { this.map.spin(true) $.getJSON(this.routeUrl + '?' + $.param({ source, target, enable_v2: enableV2 })).done((data) => { - const angle = ([lnga, lata], [lngb, latb]) => { - return (Math.atan2(Math.cos(latb) * Math.sin(lngb - lnga), Math.cos(lata) * Math.sin(latb) - Math.sin(lata) * Math.cos(latb) * Math.cos(lngb - lnga)) * 180 / Math.PI) - } - const turnToEnglish = (oldHeading, newHeading) => { - const turn = newHeading - oldHeading - if (turn < 100 && turn > 80) { - return "Turn right" - } - if (turn < -80 && turn > -100) { - return "Turn left" - } - if (turn < 10 && turn > -10) { - return "Continue" - } - if (Math.abs(turn) > 170 && Math.abs(turn) < 190) { - // There are many false u-turns, likely a data problem. Safer to just - // ignore them since this isn't a very common legit direction - // return "Turn around" - return null - } - } - const directionsList = () => { - const directions = [] - let lastHeading, lastName - for (const feature of data.route.features) { - const name = feature.properties.name - const coords = feature.geometry.coordinates - const heading = angle(coords[0], coords[coords.length - 1]) - const turn = lastHeading && turnToEnglish(lastHeading, heading) - console.log(heading) - console.log(lastHeading) - console.log(turn) - const distance = feature.properties.distance - if ((lastName && name !== lastName) || (turn || !lastHeading)) { - directions.push({ name, distance, turn, heading }) - } else { - if (directions.length) { - directions[directions.length - 1].distance += distance - } - if (name && directions.length && !directions[directions.length - 1].name) { - directions[directions.length - 1].name = name - } - } - if (coords.length > 1) { - lastHeading = angle(coords[coords.length - 2], coords[coords.length - 1]) - } else { - lastHeading = heading - } - lastName = name - } - return directions - } - console.log(directionsList()) + const directions = serializeDirections(directionsList(data.route.features)) + console.log(directions.join("\n")) if (this.routeLayer) { this.map.removeLayer(this.routeLayer) diff --git a/app/mbm/static/js/turnbyturn.js b/app/mbm/static/js/turnbyturn.js new file mode 100644 index 0000000..1243d12 --- /dev/null +++ b/app/mbm/static/js/turnbyturn.js @@ -0,0 +1,71 @@ +// This should probably all be moved to python but is a first draft of +// what turn-by-turn directions will look like + +const headingToEnglishManeuver = (heading, previousHeading) => { + const maneuvers = { + 0: { maneuver: "Continue", cardinal: "North" }, + 45: { maneuver: "Turn slightly to the right", cardinal: "Northeast" }, + 90: { maneuver: "Turn right", cardinal: "East" }, + 135: { maneuver: "Take a sharp right turn", cardinal: "Southeast" }, + 180: { maneuver: "Turn around", cardinal: "South" }, + 225: { maneuver: "Take a sharp left turn", cardinal: "Southwest" }, + 270: { maneuver: "Turn left", cardinal: "West" }, + 315: { maneuver: "Turn slightly to the left", cardinal: "Northwest" }, + } + + const nearest45 = (x) => (Math.round(x / 45) * 45) % 360 + + const angle = nearest45(((heading - previousHeading) + 360) % 360) + + return { maneuver: maneuvers[angle]?.maneuver, cardinal: maneuvers[nearest45(heading)].cardinal } +} + +const directionsList = (features) => { + const directions = [] + let previousHeading, previousName + for (const feature of features) { + const name = feature.properties.name + const heading = feature.properties.heading + const { maneuver, cardinal } = headingToEnglishManeuver(heading, previousHeading) + const distance = feature.properties.distance + const direction = { name, distance, maneuver, heading, cardinal } + console.log({previousHeading, previousName, ...direction}) + + // If the street name changed or there's a turn to be made, add a new direction to the list + const streetNameChanged = previousName && name !== previousName + const turnRequired = (maneuver !== 'Continue' || !previousHeading) // "Continue" + if (streetNameChanged || turnRequired) { + directions.push(direction) + } + // Otherwise this is just a quirk of our data and the line segments should be combined + else { + if (directions.length) { + directions[directions.length - 1].distance += distance + } + // Sometimes only some segments of a street are named, so check if the + // previous segment is named and backfill the name if not + if (name && directions.length && !directions[directions.length - 1].name) { + directions[directions.length - 1].name = name + } + } + + previousHeading = heading + previousName = name + } + return directions +} + +const serializeDirections = (directions) => { + const lines = [] + const first = directions.shift() + lines.push(`Head ${first.cardinal} on ${first.name || 'an unknown street'} for ${Math.round(first.distance)} meters`) + for (const direction of directions) { + lines.push(`${direction.maneuver} onto ${direction.name || 'an unknown street'} and head ${direction.cardinal} for ${Math.round(direction.distance)} meters`) + } + // TODO: add which side of the street it's on + lines[lines.length - 1] += " until you reach your destination" + + return lines +} + +export { serializeDirections, directionsList }