updates to summary and api usage, added real time routing

* the summary now respects unit preferences from the watch default
settings (read from the health measurement system display)
* added more specific fields for public transit api
* added real time routing (in progress)
* added setting to disable real time routing
* added api attribution and feedback link in the settings page
This commit is contained in:
dyedgreen
2016-06-30 20:42:45 +02:00
parent 9d2ea422e3
commit 0d31e7c728
7 changed files with 328 additions and 127 deletions
+2 -1
View File
@@ -35,7 +35,8 @@
"DISTANCE", "DISTANCE",
"TIME", "TIME",
"INSTRUCTION_ICONS", "INSTRUCTION_ICONS",
"INSTRUCTION_LIST" "INSTRUCTION_LIST",
"CURRENT"
], ],
"resources": { "resources": {
"media": [ "media": [
+16 -6
View File
@@ -42,13 +42,23 @@ void directions_draw_summary(GContext *ctx, GRect bounds, GColor color, int dist
// Get the strings // Get the strings
char string_distance[16]; char string_distance[16];
char string_time[16]; char string_time[16];
if (distance < 1000) { // Disance string
// Use meters switch (health_service_get_measurement_system_for_display(HealthMetricWalkedDistanceMeters)) {
snprintf(string_distance, sizeof(string_distance), "%i m", distance); // Imperial (miles)
} else { case MeasurementSystemImperial:
// Convert to kilometeres with one place after the point snprintf(string_distance, sizeof(string_distance), "%i.%i mi", distance / 1609, ((distance % 1609) * 10) / 1609);
snprintf(string_distance, sizeof(string_distance), "%i.%i km", distance / 1000, (distance % 1000) / 100); break;
// Metric (meters & kilometers)
default:
if (distance < 1000) {
// Use meters
snprintf(string_distance, sizeof(string_distance), "%i m", distance);
} else {
// Convert to kilometeres with one place after the point
snprintf(string_distance, sizeof(string_distance), "%i.%i km", distance / 1000, (distance % 1000) / 100);
}
} }
// Time required string
if (time < 60) { if (time < 60) {
// Use minutes // Use minutes
snprintf(string_time, sizeof(string_time), "%i min", time); snprintf(string_time, sizeof(string_time), "%i min", time);
+31
View File
@@ -16,6 +16,9 @@
// The RouteData struct // The RouteData struct
struct RouteData { struct RouteData {
// Meta fields
bool complete;
int current;
// Data fields // Data fields
int distance; int distance;
int time; int time;
@@ -56,6 +59,7 @@ int message_number = -1;
static void app_message_send_search_data(); static void app_message_send_search_data();
void window_display_error(enum ErrorType err); void window_display_error(enum ErrorType err);
static void window_update_data(); static void window_update_data();
static void window_update_step();
// ****************************** // ******************************
@@ -168,6 +172,7 @@ static void app_message_inbox_recived_callback(DictionaryIterator *iter, void *c
switch (success_response) { switch (success_response) {
// Success // Success
case 0: case 0:
route_data->complete = true;
window_update_data(); window_update_data();
break; break;
// Route not found / api error // Route not found / api error
@@ -221,6 +226,17 @@ static void app_message_inbox_recived_callback(DictionaryIterator *iter, void *c
// Increment the number of recived steps // Increment the number of recived steps
route_data->count += 1; route_data->count += 1;
} }
// Test if the recived message is for key CURRENT
message = dict_find(iter, MESSAGE_KEY_CURRENT);
if (message) {
// Set the current and update the ui if the data is already complete
int new_step = (int)message->value->int32;
if (route_data->complete && new_step < route_data->count) {
route_data->current = new_step;
window_update_step();
}
}
} }
// Respond with error, if any data is lost // Respond with error, if any data is lost
@@ -272,6 +288,9 @@ static void app_message_start() {
// Set up the data // Set up the data
route_data = malloc(sizeof(struct RouteData)); route_data = malloc(sizeof(struct RouteData));
// Set meta values
route_data->complete = false;
route_data->current = 0;
// Set initial values // Set initial values
route_data->distance = 0; route_data->distance = 0;
route_data->time = 0; route_data->time = 0;
@@ -323,6 +342,18 @@ static void window_update_data() {
progress_layer_remove(progress_layer); progress_layer_remove(progress_layer);
} }
// Update the window display to reflrect the new current step and vibrate
static void window_update_step() {
// Update the position of the step list
#ifdef PBL_ROUND
menu_layer_set_selected_index(directions_list, (MenuIndex){ .section = 0, .row = route_data->current + 1 }, MenuRowAlignCenter, true);
#else
menu_layer_set_selected_index(directions_list, (MenuIndex){ .section = 0, .row = route_data->current + 1 }, MenuRowAlignTop, true);
#endif
// Play a short vibration
vibes_long_pulse();
}
// Window unload handler // Window unload handler
static void window_unload() { static void window_unload() {
// Destroy the menu layer // Destroy the menu layer
+68
View File
@@ -27,6 +27,11 @@
* ––– * –––
* Chars map to the following icons: type: 'a', forward: 'b', right: 'c', left: 'd', uRight: 'e', uLeft: 'f', attr: 'g', final: 'h' * Chars map to the following icons: type: 'a', forward: 'b', right: 'c', left: 'd', uRight: 'e', uLeft: 'f', attr: 'g', final: 'h'
* *
* About the automatic navigation
* –––
* The automatic navigation can be toggled in the settings. If enabled, the JS part will listen to location updates and send the
* current step-index to the watch in case it changes.
*
* About the config of 'named addresses' * About the config of 'named addresses'
* ––– * –––
* The config stores an array of these name / address pairs, every time a search request hits the phone the * The config stores an array of these name / address pairs, every time a search request hits the phone the
@@ -45,6 +50,12 @@ var messagePadding = 10;
var locationService = require('./location.js'); var locationService = require('./location.js');
// Configuration // Configuration
var config = require('./config.js'); var config = require('./config.js');
// Route data
var routeData = {
stepPositionList: [],
currentStep: 0,
watchId: null,
};
// App Message functions // App Message functions
@@ -116,6 +127,55 @@ function sendRoute(success, distance, time, stepList, stepIconsString, messageNu
} }
} }
// Send the new current index
function sendCurrentStep(index, shouldRetry) {
// Build the message
var keyCurrent = keys.CURRENT;
var dict = {};
dict[keyCurrent] = +index;
// Transmit
Pebble.sendAppMessage(dict, function() {
// Success!
console.log('Current step send:', index);
}, function() {
// Error, retry if allowed
if (shouldRetry === true) {
sendCurrentStep(index, false);
} else {
console.log('Transmission of current step failed');
}
});
}
// Start sending current step information
function startCurrentStepUpdates(stepPositionList) {
// Store the current route data
routeData.stepPositionList = stepPositionList;
routeData.currentStep = 0;
// Register the location updates
routeData.watchId = navigator.geolocation.watchPosition(function(pos) {
// New position was recived
var lat = pos.coords.latitude;
var lon = pos.coords.longitude;
var newStep = locationService.getCurrentStepIndex(routeData.stepPositionList, lat, lon, routeData.currentStep);
if (newStep != routeData.currentStep) {
sendCurrentStep(newStep, true);
routeData.currentStep = newStep;
}
},
function() {
// Error while reciving location update
console.log('Error while reciving location update');
}, { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 });
}
// Stop sending current step information
function stopCurrentStepUpdates() {
// Clear the watch and stop receiving updates
navigator.geolocation.clearWatch(routeData.watchId);
}
// Fetch a route from the here api and send it to the pebble // Fetch a route from the here api and send it to the pebble
function fetchAndSendRoute(routeType, searchText, messageNumber) { function fetchAndSendRoute(routeType, searchText, messageNumber) {
// Log the recived data // Log the recived data
@@ -130,8 +190,14 @@ function fetchAndSendRoute(routeType, searchText, messageNumber) {
}); });
// Load a route from here api. Data format: { distance, time, stepList[string], stepIconsString } // Load a route from here api. Data format: { distance, time, stepList[string], stepIconsString }
locationService.createRoute(routeType, searchText, function(success, data) { locationService.createRoute(routeType, searchText, function(success, data) {
// Log the route data
console.log('Will send:', success, data.distance, data.time, data.stepList.length, data.stepIconsString, messageNumber); console.log('Will send:', success, data.distance, data.time, data.stepList.length, data.stepIconsString, messageNumber);
// Send the route data to the watch
sendRoute(success, data.distance, data.time, data.stepList, data.stepIconsString, messageNumber); sendRoute(success, data.distance, data.time, data.stepList, data.stepIconsString, messageNumber);
// If the loading was successfull, start watching the position (if enabled in the config)
if (success && config.getNavigationSettings().auto) {
startCurrentStepUpdates(data.stepPositionList);
}
}); });
} }
@@ -147,5 +213,7 @@ Pebble.addEventListener('appmessage', function(e) {
currentMessageNumber ++; currentMessageNumber ++;
console.log('Message number send:', messageNumber); console.log('Message number send:', messageNumber);
fetchAndSendRoute(dict['SEARCH'].substr(0, 1), dict['SEARCH'].substr(1), messageNumber); fetchAndSendRoute(dict['SEARCH'].substr(0, 1), dict['SEARCH'].substr(1), messageNumber);
// Stop watching the position for automatic step updates if a new search is recived
stopCurrentStepUpdates();
} }
}); });
+124 -117
View File
@@ -1,5 +1,25 @@
// The config page (without the already stored name / addess pairs) // The config page (without the already stored name / addess pairs)
module.exports = [ module.exports = [
// Navigation settings
{
type: 'section',
items: [
// Description
{
type: 'heading',
defaultValue: 'Navigation settings',
},
// Enable / disable real time navigation
{
type: 'toggle',
messageKey: 'navigationAutoEnable',
label: 'Enable automatic navigation',
defaultValue: true,
description: 'Automatic navigation tells you when to take a turn in real time based on the GPS of your phone. This feature does not require an active internet connection, once the route has been loaded. Disable this feature if you want to reduce battery usage on your phone.',
},
],
},
// The named addessses section // The named addessses section
{ {
type: 'section', type: 'section',
@@ -17,143 +37,130 @@ module.exports = [
// Address #1 // Address #1
{ {
type: 'section', type: 'input',
items: [ messageKey: 'namedAddressName_0',
{ label: 'Name #1',
type: 'heading', description: 'Use a name that makes sense to you, such as \'home\', \'my college\' or \'mom\'.',
defaultValue: 'Address 1', attributes: {
}, placeholder: 'e.g. home',
{ },
type: 'input', },
messageKey: 'namedAddressName_0', {
label: 'Name', type: 'input',
description: 'Use a name that makes sense to you, such as \'home\', \'my college\' or \'mom\'.', messageKey: 'namedAddressAddress_0',
attributes: { label: 'Address #1',
placeholder: 'e.g. home', description: 'The address should be as specific as possible. Something like \'Schleusenufer Kreuzberg, 10997 Berlin\'.',
}, attributes: {
}, placeholder: 'e.g. Schleusenufer',
{ },
type: 'input',
messageKey: 'namedAddressAddress_0',
label: 'Address',
description: 'The address should be as specific as possible. Something like \'Schleusenufer Kreuzberg, 10997 Berlin\'.',
attributes: {
placeholder: 'e.g. Schleusenufer',
},
},
]
}, },
// Address #2 // Address #2
{ {
type: 'section', type: 'input',
items: [ messageKey: 'namedAddressName_1',
{ label: 'Name #2',
type: 'heading', attributes: {
defaultValue: 'Address 2', placeholder: 'e.g. home',
}, },
{ },
type: 'input', {
messageKey: 'namedAddressName_1', type: 'input',
label: 'Name', messageKey: 'namedAddressAddress_1',
attributes: { label: 'Address #2',
placeholder: 'e.g. home', attributes: {
}, placeholder: 'e.g. Schleusenufer',
}, },
{
type: 'input',
messageKey: 'namedAddressAddress_1',
label: 'Address',
attributes: {
placeholder: 'e.g. Schleusenufer',
},
},
]
}, },
// Address #3 // Address #3
{ {
type: 'section', type: 'input',
items: [ messageKey: 'namedAddressName_2',
{ label: 'Name #3',
type: 'heading', attributes: {
defaultValue: 'Address 3', placeholder: 'e.g. home',
}, },
{ },
type: 'input', {
messageKey: 'namedAddressName_2', type: 'input',
label: 'Name', messageKey: 'namedAddressAddress_2',
attributes: { label: 'Address #3',
placeholder: 'e.g. home', attributes: {
}, placeholder: 'e.g. Schleusenufer',
}, },
{
type: 'input',
messageKey: 'namedAddressAddress_2',
label: 'Address',
attributes: {
placeholder: 'e.g. Schleusenufer',
},
},
]
}, },
// Address #4 // Address #4
{ {
type: 'section', type: 'input',
items: [ messageKey: 'namedAddressName_3',
{ label: 'Name #4',
type: 'heading', attributes: {
defaultValue: 'Address 4', placeholder: 'e.g. home',
}, },
{ },
type: 'input', {
messageKey: 'namedAddressName_3', type: 'input',
label: 'Name', messageKey: 'namedAddressAddress_3',
attributes: { label: 'Address #4',
placeholder: 'e.g. home', attributes: {
}, placeholder: 'e.g. Schleusenufer',
}, },
{
type: 'input',
messageKey: 'namedAddressAddress_3',
label: 'Address',
attributes: {
placeholder: 'e.g. Schleusenufer',
},
},
]
}, },
// Address #4 // Address #4
{ {
type: 'section', type: 'input',
items: [ messageKey: 'namedAddressName_4',
{ label: 'Name #5',
type: 'heading', attributes: {
defaultValue: 'Address 5', placeholder: 'e.g. home',
}, },
{ },
type: 'input', {
messageKey: 'namedAddressName_4', type: 'input',
label: 'Name', messageKey: 'namedAddressAddress_4',
attributes: { label: 'Address #5',
placeholder: 'e.g. home', attributes: {
}, placeholder: 'e.g. Schleusenufer',
}, },
{ },
type: 'input', ],
messageKey: 'namedAddressAddress_4', },
label: 'Address',
attributes: { // Support / feedback stuff
placeholder: 'e.g. Schleusenufer', {
}, type: 'section',
}, items: [
] // Description
{
type: 'heading',
defaultValue: 'Feedback',
},
{
type: 'text',
defaultValue: 'To submit your feedback, please use this form: <a href="http://goo.gl/forms/KeN3q0oVqg9h4rWf1">FEEDBACK FORM</a> (save your preferences, before clicking the link).',
},
],
},
// Api attribution stuff
{
type: 'section',
items: [
// Description
{
type: 'heading',
defaultValue: 'Navigation data',
},
{
type: 'text',
defaultValue: 'The data is provided by the HERE API (https://developer.here.com).',
}, },
], ],
}, },
+44 -1
View File
@@ -1,4 +1,6 @@
// Functions to handle storing / retriving of the values // Functions to handle storing / retriving of the values
// Store the named addresses data
function storeNamedAddresses(configDict) { function storeNamedAddresses(configDict) {
// This array will hold the raw named addesses // This array will hold the raw named addesses
var namedAddressesNamesRaw = []; var namedAddressesNamesRaw = [];
@@ -37,9 +39,10 @@ function storeNamedAddresses(configDict) {
// Store the named addresses // Store the named addresses
try { try {
localStorage.setItem('namedAddresses', JSON.stringify(namedAddresses)); localStorage.setItem('namedAddresses', JSON.stringify(namedAddresses));
} catch(e) {} } catch (e) {}
} }
// Get the named addresses data (exposed)
function getNamedAddresses() { function getNamedAddresses() {
// Load the stored data // Load the stored data
data = localStorage.getItem('namedAddresses'); data = localStorage.getItem('namedAddresses');
@@ -72,6 +75,44 @@ function getNamedAddresses() {
return []; return [];
} }
// Store the navigation settings
function storeNavigationSettings(configDict) {
// Settings object with default values
var navigationSettings = {
auto: true,
};
// Automatic turn by turn navigation
if (configDict.hasOwnProperty('navigationAutoEnable')) {
navigationSettings.auto = !!configDict.navigationAutoEnable.value;
}
// Store the settings
try {
localStorage.setItem('navigationSettings', JSON.stringify(navigationSettings));
} catch (e) {}
}
// Get the navigation settings (exposed)
function getNavigationSettings() {
// Settings object with default values
var navigationSettings = {
auto: true,
};
// Load the settings
try {
var navigationSettingsData = JSON.parse(localStorage.getItem('navigationSettings'));
// Automatic turn by turn navigation
if (navigationSettingsData.hasOwnProperty('navigationAutoEnable')) {
navigationSettings.auto = !!navigationSettingsData.navigationAutoEnable;
}
} catch (e) {}
// Return the settings
return navigationSettings;
}
// Clay things // Clay things
var Clay = require('pebble-clay'); var Clay = require('pebble-clay');
@@ -94,8 +135,10 @@ Pebble.addEventListener('webviewclosed', function(e) {
// Store the addresses returned by the config page // Store the addresses returned by the config page
var configDict = clay.getSettings(e.response, false); var configDict = clay.getSettings(e.response, false);
storeNamedAddresses(configDict); storeNamedAddresses(configDict);
storeNavigationSettings(configDict);
}); });
// Exports for use in the app.js // Exports for use in the app.js
module.exports.getNamedAddresses = getNamedAddresses; module.exports.getNamedAddresses = getNamedAddresses;
module.exports.getNavigationSettings = getNavigationSettings;
+43 -2
View File
@@ -111,8 +111,14 @@ function loadRouteData(routeType, fromLat, fromLon, toLat, toLon, callback) {
url = url.concat('&waypoint0=geo!').concat(fromLat).concat(',').concat(fromLon); url = url.concat('&waypoint0=geo!').concat(fromLat).concat(',').concat(fromLon);
url = url.concat('&waypoint1=geo!').concat(toLat).concat(',').concat(toLon); url = url.concat('&waypoint1=geo!').concat(toLat).concat(',').concat(toLon);
url = url.concat('&mode=fastest;').concat(modes[routeType]); /* selectes the transit method, e.g. teleportation / car / ... */ url = url.concat('&mode=fastest;').concat(modes[routeType]); /* selectes the transit method, e.g. teleportation / car / ... */
if (modes[routeType] == 'publicTransport') {
// Special url params for public transport routes
url = url.concat('&departure=now&combineChange=true');
}
// Format the response // Format the response
url = url.concat('&routeattributes=none,summary,legs&routelegattributes=none,maneuvers&maneuverattributes=none,direction&instructionformat=text'); url = url.concat('&routeattributes=none,summary,legs&routelegattributes=none,maneuvers&maneuverattributes=none,direction,position&instructionformat=text');
// Log the final url (for rare use)
//console.log(url);
// Perform the request // Perform the request
makeJsonHttpGetRequest(url, function(success, res) { makeJsonHttpGetRequest(url, function(success, res) {
if (success) { if (success) {
@@ -125,10 +131,16 @@ function loadRouteData(routeType, fromLat, fromLon, toLat, toLon, callback) {
routeData.time = Math.ceil(res.response.route[0].summary.travelTime / 60); /* in minutes */ routeData.time = Math.ceil(res.response.route[0].summary.travelTime / 60); /* in minutes */
// Get the steps // Get the steps
routeData.stepList = []; routeData.stepList = [];
routeData.stepPositionList = [];
routeData.stepIconsString = ''; routeData.stepIconsString = '';
res.response.route[0].leg[0].maneuver.forEach(function(step, index) { res.response.route[0].leg[0].maneuver.forEach(function(step, index) {
// Add the text // Add the text
routeData.stepList[index] = step.instruction; routeData.stepList[index] = step.instruction;
// Add the position
routeData.stepPositionList[index] = {
lat: step.position.latitude,
lon: step.position.longitude,
};
// Add the icon // Add the icon
if (res.response.route[0].leg[0].maneuver.length == index + 1) { if (res.response.route[0].leg[0].maneuver.length == index + 1) {
// This is the last step, add the finished icon // This is the last step, add the finished icon
@@ -187,9 +199,38 @@ function createRoute(routeType, searchText, callback) {
} }
// Helper function to define createRoute error callback all in one place // Helper function to define createRoute error callback all in one place
function routeErrorCallback(callback) { function routeErrorCallback(callback) {
callback(false, { distance: 0, time: 0, stepList: [], stepIconsString: '' }); callback(false, { distance: 0, time: 0, stepList: [], stepPositionList: [], stepIconsString: '' });
}
// Calculates the distance of two sets of coordinates (in meters)
function getApproxDistance(fromLat, fromLon, toLat, toLon) {
var p = 0.017453292519943295; // Math.PI / 180
var c = Math.cos;
var a = 0.5 - c((fromLat - toLat) * p)/2 +
c(toLat * p) * c(fromLat * p) *
(1 - c((fromLon - toLon) * p))/2;
return 12742000 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371000 m
}
// Determine the current waypoint index, based on a list of waypoint coords [{lat,lon},...], the current position, and the current index
function getCurrentStepIndex(steps, lat, lon, currentIndex) {
if (steps instanceof Array && typeof lat === 'number' && typeof lon === 'number' && typeof currentIndex === 'number') {
// Test is the distance to the next waypoint is smaller or equal to 10 m
if (steps.length > currentIndex + 1 && getApproxDistance(lat, lon, steps[currentIndex + 1].lat, steps[currentIndex + 1].lon) <= 10) {
// Move on to the next waypoint
return currentIndex + 1;
} else {
// The current waypoint is either the last one or the next one is too far away
return currentIndex;
}
}
// In case of an error, return 0 or the current index if it is a number
return typeof currentIndex === 'number' ? currentIndex : 0;
} }
// Exports // Exports
module.exports.createRoute = createRoute; module.exports.createRoute = createRoute;
module.exports.getCurrentStepIndex = getCurrentStepIndex;