Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add draft of turn by turn directions #48

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/mbm/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
71 changes: 71 additions & 0 deletions app/mbm/static/js/turnbyturn.js
Original file line number Diff line number Diff line change
@@ -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 }
24 changes: 19 additions & 5 deletions app/mbm/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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
Expand Down