-
Notifications
You must be signed in to change notification settings - Fork 5
Feat/amenity routing #133
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
base: main
Are you sure you want to change the base?
Feat/amenity routing #133
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Add amenities to your route | ||
|
|
||
| I need coffee! The kids need a bathroom break! Feed me or I'll end it all! Common complaints heard whilst driving. With amenity-based-routing, your caffeine/bathroom/food/other needs can be catered too, all whilst you charge your vehicle. | ||
|
|
||
| ## Requirements | ||
|
|
||
| - [Chargetrip API key](https://account.chargetrip.com) - to plot routes outside this region | ||
| - [Mapbox API key](https://www.mapbox.com) - to display the map | ||
| - [URQL](https://formidable.com/open-source/urql/) - a lightweight graphQL client | ||
|
|
||
| ## Steps to take | ||
|
|
||
| 1. Plotting a route starts by executing the `newRoute` mutation. This mutation requires information about the car, origin and destination. After the mutation is finished executing a route `id` will be returned. Here the amenity preferences for the route should be added. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets change car to vehicle
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the newRoute mutation you can also add additional requirements such as a scheduled amenity stop. |
||
| 2. This `id` can be used to request route updates through the `routeUpdatedById` subscription. This subscription receives dynamic updates. | ||
| 3. After the subscription returns done as status, data can be rendered onto the screen. The `polyline` and the `legs` object will be used to display charge stations on the map. Total distance, duration of a trip, consumption are displayed on the side. | ||
| 4. The amenities are also displayed underneath the relevant station for the end user to see. | ||
| 5. Using the `route.leg.type` property, it's possible to check if a leg ends at an amenity. | ||
|
|
||
| ## Next steps | ||
|
|
||
| This example shows how routing can be supplemented by adding amenities during certain charging stops. Next, let's move on from routing and dive in to the tile service. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import { createClient, createRequest, defaultExchanges, subscriptionExchange } from '@urql/core'; | ||
| import { pipe, subscribe } from 'wonka'; | ||
| import { SubscriptionClient } from 'subscriptions-transport-ws'; | ||
| import { createRouteQuery, routeUpdateSubscription, getAmenityListQuery } from './queries.js'; | ||
|
|
||
| /** | ||
| * Example application of how to build a route with the Chargetrip API. | ||
| * Please have a look to Readme file in this repo for more details. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how to build a route with a scheduled amen city stop |
||
| * | ||
| * For the purpose of this example we use urgl - lightweights GraphQL client. | ||
| * To establish a connection with Chargetrip GraphQL API you need to have an API key. | ||
| * The key in this example is a public one and gives access only to a part of our extensive database. | ||
| * You need a registered `x-client-id` to access the full database. | ||
| * Read more about an authorisation in our documentation (https://docs.chargetrip.com/#authorisation). | ||
| */ | ||
| const headers = { | ||
| //Replace this x-client-id and app-id with your own to get access to more cars | ||
| 'x-client-id': '5ed1175bad06853b3aa1e492', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. vehicles and the complete station database |
||
| 'x-app-id': '623998b2c35130073829b2d2', | ||
| }; | ||
|
|
||
| const subscriptionClient = new SubscriptionClient('wss://api.chargetrip.io/graphql', { | ||
| reconnect: true, | ||
| connectionParams: headers, | ||
| }); | ||
|
|
||
| const client = createClient({ | ||
| url: 'https://api.chargetrip.io/graphql', | ||
| fetchOptions: { | ||
| method: 'POST', | ||
| headers, | ||
| }, | ||
| exchanges: [ | ||
| ...defaultExchanges, | ||
| subscriptionExchange({ | ||
| forwardSubscription(operation) { | ||
| return subscriptionClient.request(operation); | ||
| }, | ||
| }), | ||
| ], | ||
| }); | ||
|
|
||
| /** | ||
| * To create a route you need: | ||
| * | ||
| * 1. Create a new route and receive back its ID; | ||
| * 2. Subscribe to route updates in order to receive its details. | ||
| */ | ||
| export const getRoute = callback => { | ||
| client | ||
| .mutation(createRouteQuery()) | ||
| .toPromise() | ||
| .then(response => { | ||
| const routeId = response.data.newRoute; | ||
| if (!routeId) return Promise.reject('Could not retrieve Route ID. The response is not valid.'); | ||
|
|
||
| const { unsubscribe } = pipe( | ||
| client.executeSubscription(createRequest(routeUpdateSubscription, { id: routeId })), | ||
| subscribe(result => { | ||
| const { status, route } = result.data.routeUpdatedById; | ||
|
|
||
| // you can keep listening to the route changes to update route information | ||
| // for this example we want to only draw the initial route | ||
| if (status === 'done' && route) { | ||
| unsubscribe(); | ||
| callback(route); | ||
| } | ||
| }), | ||
| ); | ||
| }) | ||
| .catch(error => console.log(error)); | ||
| }; | ||
|
|
||
| /** | ||
| * Fetch the detail data of a specific station | ||
| * @param { string } id - the id of the station | ||
| */ | ||
| export const getAmenityData = id => { | ||
| return client | ||
| .query(getAmenityListQuery, { | ||
| stationId: id, | ||
| }) | ||
| .toPromise() | ||
| .then(response => { | ||
| return response.data; | ||
| }) | ||
| .catch(error => console.log(error)); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { getRoute, getAmenityData } from './client'; | ||
| import { renderRouteData, renderAmenityData } from './interface'; | ||
| import { drawRoutePolyline } from './map'; | ||
|
|
||
| /** | ||
| * This project shows you how to fetch a car list and render the car details | ||
| * The project structure contains; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This project shows you how create a route with a scheduled amenity stop |
||
| * | ||
| * - client.js - All networking requests | ||
| * - interface.js - All interface rendering | ||
| * - map.js - All map rendering (including routes and waypoints) | ||
| * - queries.js - The GraphQL queries used in the networking requests | ||
| */ | ||
|
|
||
| getRoute(route => { | ||
| drawRoutePolyline(route); | ||
| renderRouteData(route); | ||
| // Check if the leg ends at an amenity | ||
| if (route && route.legs[0].type === 'stationAmenity') { | ||
| getAmenityData(route.legs[0].stationId).then(data => { | ||
| renderAmenityData(data.amenityList[0]); | ||
| }); | ||
| } | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { getDurationString } from '../../../utils'; | ||
|
|
||
| /** | ||
| * Render journey overview. | ||
| * | ||
| * @param data {object} route specification | ||
| */ | ||
| export const renderRouteData = data => { | ||
| // Show the side card once the route information has loaded | ||
| const sideCard = document.getElementById('side-card'); | ||
| sideCard.style.display = 'block'; | ||
|
|
||
| // the total duration of the journey (including charge time), in seconds | ||
| document.getElementById('duration').innerHTML = `${getDurationString(data.duration ?? 0)}`; | ||
|
|
||
| // the total distance of the route, in meters converted to km | ||
| const routeDistance = data.distance ? `${(data.distance / 1000).toFixed(0)} km` : 'Unknown'; | ||
|
|
||
| // the total energy used of the route, in kWh | ||
| const routeEnergy = data.consumption ? `${data.consumption.toFixed(2)} kWh` : 'Unknown'; | ||
|
|
||
| // the amount of stops in this route | ||
| const routeStops = `${data.charges ?? 0} stops`; | ||
|
|
||
| // A combined field containing several of the route meta data | ||
| document.getElementById('route-metadata').innerHTML = `${routeDistance} / ${routeStops} / ${routeEnergy}`; | ||
|
|
||
| const numberOfLegs = data.legs.length; | ||
| // Populate journey overview | ||
| if (data && data.legs) { | ||
| // Show first distance | ||
| document.getElementById('first-leg-distance').innerHTML = `Drive for ${(data.legs[0].distance / 1000).toFixed( | ||
| 1, | ||
| )} km`; | ||
| document.getElementById('first-leg-duration').innerHTML = `Approx ${getDurationString(data.legs[0].duration)}`; | ||
| document.getElementById('charging-time').innerHTML = `Charge for ${getDurationString(data.legs[0].chargeTime)}utes`; | ||
| document.getElementById('last-leg-distance').innerHTML = `Drive for ${( | ||
| data.legs[numberOfLegs - 1].distance / 1000 | ||
| ).toFixed(1)} km`; | ||
| document.getElementById('last-leg-duration').innerHTML = `Approx ${getDurationString( | ||
| data.legs[numberOfLegs - 1].duration, | ||
| )}`; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Render a horizontal list of amenity icons | ||
| * @param { Object } amenities - an object that contains all amenities and their details | ||
| */ | ||
| export const renderAmenityData = amenityData => { | ||
| let amenityName = document.getElementById('restaurant-name'); | ||
| let amenityAddress = document.getElementById('restaurant-address'); | ||
| let amenityDistance = document.getElementById('restaurant-distance'); | ||
| amenityName.innerHTML = amenityData.name; | ||
| amenityAddress.innerHTML = `${amenityData.address.formattedAddress[0]} | ${amenityData.address.country}`; | ||
| amenityDistance.innerHTML = `${amenityData.distance} meters from charging station`; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| import mapboxgl from 'mapbox-gl'; | ||
| import { getDurationString } from '../../../utils'; | ||
| import * as mapboxPolyline from '@mapbox/polyline'; | ||
|
|
||
| mapboxgl.accessToken = 'pk.eyJ1IjoiY2hhcmdldHJpcCIsImEiOiJjazhpaG8ydTIwNWNpM21ud29xeXc2amhlIn0.rGKgR3JfG9Z5dCWjUI_oGA'; | ||
|
|
||
| // Keep track of the charging time popups | ||
| let chargingTimePopUps = []; | ||
|
|
||
| const map = new mapboxgl.Map({ | ||
| cooperativeGestures: true, | ||
| container: 'map', | ||
| style: 'mapbox://styles/chargetrip/ckgcbf3kz0h8819qki8uwhe0k', | ||
| zoom: 7, | ||
| center: [10, 55.0758916], | ||
| }); | ||
|
|
||
| /** | ||
| * Draw a route on a map. | ||
| * | ||
| * Route object contains `polyline` data - the polyline encoded route (series of coordinates as a single string). | ||
| * We need to decode this information first. We use Mapbox polyline tool (https://www.npmjs.com/package/@mapbox/polyline) for this. | ||
| * As a result of decoding we get pairs [latitude, longitude]. | ||
| * To draw a route on a map we use Mapbox GL JS. This tool uses the format [longitude,latitude], | ||
| * so we have to reverse every pair. | ||
| * | ||
| * @param data {object} route specification | ||
| */ | ||
| export const drawRoutePolyline = data => { | ||
| const decodedData = mapboxPolyline.decode(data.polyline); | ||
| const reversed = decodedData.map(item => item.reverse()); | ||
|
|
||
| drawRoute(reversed, data.legs); | ||
| }; | ||
|
|
||
| /** | ||
| * Draw route polyline and show charging stations on the map. | ||
| * | ||
| * @param coordinates {array} Array of coordinates | ||
| * @param legs {array} route legs (stops) - each leg represents either a charging station, or via point or final point | ||
| */ | ||
| const drawRoute = (coordinates, legs) => { | ||
| if (map.loaded()) { | ||
| drawPolyline(coordinates); | ||
| drawChargingTimes(legs); | ||
| showLegs(legs); | ||
| } else { | ||
| map.on('load', () => { | ||
| drawPolyline(coordinates); | ||
| drawChargingTimes(legs); | ||
| showLegs(legs); | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Render the charging times at each station directly on top of it's marker. | ||
| * @param {array} legs - each leg represents either a charging station, or a via point or final point | ||
| */ | ||
| const drawChargingTimes = legs => { | ||
| legs.forEach((leg, idx) => { | ||
| if (idx == legs.length - 1) { | ||
| return; | ||
| } | ||
|
|
||
| const chargeTime = leg.chargeTime; | ||
| const hrs = ~~(chargeTime / 3600); | ||
| const mins = ~~((chargeTime % 3600) / 60); | ||
|
|
||
| const popup = new mapboxgl.Popup({ closeOnClick: false }) | ||
| .setLngLat(leg.destination.geometry.coordinates) | ||
| .setHTML(`<small>${hrs}:${mins}</small>`) | ||
| .addTo(map); | ||
|
|
||
| // Add the popup to the chargingTimePopUps array | ||
| chargingTimePopUps.push(popup); | ||
| }); | ||
| }; | ||
|
|
||
| /** | ||
| * Draw route polyline on a map. | ||
| * | ||
| * @param coordinates {array} polyline coordinates | ||
| */ | ||
| const drawPolyline = coordinates => { | ||
| const geojson = { | ||
| type: 'FeatureCollection', | ||
| features: [ | ||
| { | ||
| type: 'Feature', | ||
| geometry: { | ||
| type: 'LineString', | ||
| properties: {}, | ||
| coordinates, | ||
| }, | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
| map.addSource('polyline-source', { | ||
| type: 'geojson', | ||
| lineMetrics: true, | ||
| data: geojson, | ||
| }); | ||
|
|
||
| map.addLayer({ | ||
| id: 'polyline', | ||
| type: 'line', | ||
| options: 'beforeLayer', | ||
| source: 'polyline-source', | ||
| layout: { | ||
| 'line-join': 'round', | ||
| 'line-cap': 'round', | ||
| }, | ||
| paint: { | ||
| 'line-color': '#0078FF', | ||
| 'line-width': 3, | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| /** | ||
| * Show the charging station, origin and destination on the map. | ||
| * | ||
| * Last leg of the route is a destination point. | ||
| * All other legs are either charging stations or via points (if route has stops). | ||
| * | ||
| * @param legs {array} route legs | ||
| */ | ||
| const showLegs = legs => { | ||
| if (legs.length === 0) return; | ||
|
|
||
| let points = []; | ||
|
|
||
| // we want to show origin point on the map | ||
| // to do that we use the origin of the first leg | ||
| points.push({ | ||
| type: 'Feature', | ||
| properties: { | ||
| icon: 'location_big', | ||
| }, | ||
| geometry: legs[0].origin?.geometry, | ||
| }); | ||
|
|
||
| legs.map((leg, index) => { | ||
| // add charging stations | ||
| if (index !== legs.length - 1) { | ||
| points.push({ | ||
| type: 'Feature', | ||
| properties: { | ||
| description: `${getDurationString(leg.chargeTime)}`, | ||
| icon: 'restaurant', | ||
| }, | ||
| geometry: leg.destination?.geometry, | ||
| }); | ||
| } else { | ||
| // add destination point (last leg) | ||
| points.push({ | ||
| type: 'Feature', | ||
| properties: { | ||
| icon: 'arrival', | ||
| }, | ||
| geometry: leg.destination?.geometry, | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| // draw route points on a map | ||
| map.addLayer({ | ||
| id: 'legs', | ||
| type: 'symbol', | ||
| layout: { | ||
| 'icon-image': '{icon}', | ||
| 'icon-allow-overlap': true, | ||
| 'icon-offset': ['case', ['==', ['get', 'icon'], 'location_big'], ['literal', [0, 0]], ['literal', [0, -15]]], | ||
| }, | ||
| source: { | ||
| type: 'geojson', | ||
| data: { | ||
| type: 'FeatureCollection', | ||
| features: points, | ||
| }, | ||
| }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I loved this but I would make this a bit less extreme and more focus on a lunch stop.