diff --git a/app/mbm/static/js/app.js b/app/mbm/static/js/app.js index 27576e1..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,6 +268,10 @@ export default class App { } else { this.map.spin(true) $.getJSON(this.routeUrl + '?' + $.param({ source, target, enable_v2: enableV2 })).done((data) => { + + 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 } diff --git a/app/mbm/views.py b/app/mbm/views.py index c077a07..9d24ca1 100644 --- a/app/mbm/views.py +++ b/app/mbm/views.py @@ -110,8 +110,11 @@ 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, - mellow.type + 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( 'WITH mellow AS ( SELECT DISTINCT(UNNEST(ways)) AS osm_id, type @@ -152,7 +155,16 @@ 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) @@ -173,7 +185,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 +198,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