diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 870ad0fdb7..09637df1ba 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -98,3 +98,12 @@ * New setting : power screen off between points to save battery * Color change for lost direction (now purple) * Adaptive screen powersaving + +0.21: + * Jit is back for display functions (10% speed increase) + * Store, parse and display elevation data + * Removed 'lost' indicator (we now change position to purple when lost) + * Powersaving fix : don't powersave when lost + * Bugfix for negative remaining distance when going backwards + * New settings for powersaving + * Adjustments to powersaving algorithm diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 03ca977538..8539167e1c 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -28,6 +28,7 @@ It provides the following features : - toilets - artwork - bakeries +- display elevation data if available in the trace ## Usage @@ -41,7 +42,7 @@ also a nice open source option. Note that *mapstogpx* has a super nice feature in its advanced settings. You can turn on 'next turn info' and be warned by the watch when you need to turn. -Once you have your gpx file you need to convert it to *gpc* which is my custom file format. +Once you have your gpx file you need to convert it to *gps* which is my custom file format. They are smaller than gpx and reduce the number of computations left to be done on the watch. Just click the disk icon and select your gpx file. @@ -78,34 +79,75 @@ On your screen you can see: * green: artwork - a *turn* indicator on the top right when you reach a turning point - a *gps* indicator (blinking) on the top right if you lose gps signal -- a *lost* indicator on the top right if you stray too far away from path ### Lost -If you stray away from path we will rescale the display to continue displaying nearby segments and -display the direction to follow as a purple segment. +If you stray away from path we will display the direction to follow as a purple segment. Your main position will also turn to purple. Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you are. On path it just needed to scan a few points ahead and behind. -The distance to next point displayed corresponds to the length of the black segment. +The distance to next point displayed corresponds to the length of the purple segment. ### Menu If you click the button you'll reach a menu where you can currently zoom out to see more of the map (with a slower refresh rate), reverse the path direction and disable power saving (keeping backlight on). +### Elevation + +If you touch the screen you will switch between display modes. +The first one displays the map, the second one the nearby elevation and the last one the elevation +for the whole path. + +![Screenshot](heights.png) + +Colors correspond to slopes. +Above 15% will be red, above 8% orange, above 3% yellow, between 3% and -3% is green and shades of blue +are for descents. + +You should note that the precision is not very good. The input data is not very precise and you only get the +slopes between path points. Don't expect to see small bumps on the road. + ### Settings Few settings for now (feel free to suggest me more) : -- lost distance : at which distance from path are you considered to be lost ? - buzz on turns : should the watch buzz when reaching a waypoint ? - disable bluetooth : turn bluetooth off completely to try to save some power. +- lost distance : at which distance from path are you considered to be lost ? +- wake-up speed : if you drive below this speed powersaving will disable itself +- active-time : how long (in seconds) the screen should be turned on if activated before going back to sleep. - brightness : how bright should screen be ? (by default 0.5, again saving power) - power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points, when you touch the screen and when speed is below 13km/h. +### Powersaving + +Starting with release 0.20 we experiment with power saving. + +There are now two display modes : + +- active : the screen is lit back (default at 50% light but can be configured with the *brightness* setting) +- inactive : by default the screen is not lit but you can also power it off completely (with the *power lcd off* setting) + +The algorithm works in the following ways : + +- some events will *activate* : the display will turn *active* +- if no activation event occur for at least 10 seconds (or *active-time* setting) we switch back to *inactive* + +Activation events are the following : + +- you are near (< 100m) the next point on path +- you are slow (< *wake-up speed* setting (13 km/h by default)) +- you press the button / touch the screen + + +Power saving has been tested on a very long trip with several benefits + +- longer battery life +- waking up near path points will attract your attention more easily when needed + ### Caveats It is good to use but you should know : diff --git a/apps/gipy/TODO b/apps/gipy/TODO index b2a3c7ae1d..8c767c4639 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -36,10 +36,16 @@ my conclusion is that: ************************** +JIT: array declaration in jit is buggy +(especially several declarations) + +************************** + ++ try disabling gps for more powersaving + + when you walk the direction still has a tendency to shift + put back foot only ways -+ try fiddling with jit + put back street names + put back shortest paths but with points cache this time and jit + how to display paths from shortest path ? diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 071ef82835..46e29c3597 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -7,24 +7,33 @@ let powersaving = true; let status; let interests_colors = [ - 0xffff, // Waypoints, white - 0xf800, // Bakery, red - 0x001f, // DrinkingWater, blue - 0x07ff, // Toilets, cyan - 0x07e0, // Artwork, green + 0xffff, // Waypoints, white + 0xf800, // Bakery, red + 0x001f, // DrinkingWater, blue + 0x07ff, // Toilets, cyan + 0x07e0, // Artwork, green ]; let Y_OFFSET = 20; + +// some constants for screen types +let MAP = 0; +let HEIGHTS_ZOOMED_IN = 1; +let HEIGHTS_FULL = 2; + let s = require("Storage"); -var settings = Object.assign({ - lost_distance: 50, - brightness: 0.5, - buzz_on_turns: false, - disable_bluetooth: true, - power_lcd_off: false, - }, - s.readJSON("gipy.json", true) || {} +var settings = Object.assign( + { + lost_distance: 50, + wake_up_speed: 13, + active_time: 10, + brightness: 0.5, + buzz_on_turns: false, + disable_bluetooth: true, + power_lcd_off: false, + }, + s.readJSON("gipy.json", true) || {} ); // let profile_start_times = []; @@ -41,25 +50,25 @@ var settings = Object.assign({ // return the index of the largest element of the array which is <= x function binary_search(array, x) { - let start = 0, - end = array.length; - - while (end - start >= 0) { - let mid = Math.floor((start + end) / 2); - if (array[mid] == x) { - return mid; - } else if (array[mid] < x) { - if (array[mid + 1] > x) { - return mid; - } - start = mid + 1; - } else end = mid - 1; - } - if (array[start] > x) { - return null; - } else { - return start; - } + let start = 0, + end = array.length; + + while (end - start >= 0) { + let mid = Math.floor((start + end) / 2); + if (array[mid] == x) { + return mid; + } else if (array[mid] < x) { + if (array[mid + 1] > x) { + return mid; + } + start = mid + 1; + } else end = mid - 1; + } + if (array[start] > x) { + return null; + } else { + return start; + } } // return a string containing estimated time of arrival. @@ -67,1513 +76,1735 @@ function binary_search(array, x) { // remaining distance in km // hour, minutes is current time function compute_eta(hour, minutes, approximate_speed, remaining_distance) { - if (isNaN(approximate_speed) || approximate_speed < 0.1) { - return ""; - } - let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes - let eta_in_minutes = Math.round(hour * 60 + minutes + time_needed); - let eta_minutes = eta_in_minutes % 60; - let eta_hour = ((eta_in_minutes - eta_minutes) / 60) % 24; - if (eta_minutes < 10) { - return eta_hour.toString() + ":0" + eta_minutes; - } else { - return eta_hour.toString() + ":" + eta_minutes; - } + if (isNaN(approximate_speed) || approximate_speed < 0.1) { + return ""; + } + let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes + let eta_in_minutes = Math.round(hour * 60 + minutes + time_needed); + let eta_minutes = eta_in_minutes % 60; + let eta_hour = ((eta_in_minutes - eta_minutes) / 60) % 24; + if (eta_minutes < 10) { + return eta_hour.toString() + ":0" + eta_minutes; + } else { + return eta_hour.toString() + ":" + eta_minutes; + } } class TilesOffsets { - constructor(buffer, offset) { - let type_size = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - this.entry_size = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - let non_empty_tiles_number = Uint16Array(buffer, offset, 1)[0]; - offset += 2; - this.non_empty_tiles = Uint16Array(buffer, offset, non_empty_tiles_number); - offset += 2 * non_empty_tiles_number; - if (type_size == 24) { - this.non_empty_tiles_ends = Uint24Array( - buffer, - offset, - non_empty_tiles_number - ); - offset += 3 * non_empty_tiles_number; - } else if (type_size == 16) { - this.non_empty_tiles_ends = Uint16Array( - buffer, - offset, - non_empty_tiles_number - ); - offset += 2 * non_empty_tiles_number; - } else { - throw "unknown size"; - } - return [this, offset]; + constructor(buffer, offset) { + let type_size = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + this.entry_size = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + let non_empty_tiles_number = Uint16Array(buffer, offset, 1)[0]; + offset += 2; + this.non_empty_tiles = Uint16Array(buffer, offset, non_empty_tiles_number); + offset += 2 * non_empty_tiles_number; + if (type_size == 24) { + this.non_empty_tiles_ends = Uint24Array( + buffer, + offset, + non_empty_tiles_number + ); + offset += 3 * non_empty_tiles_number; + } else if (type_size == 16) { + this.non_empty_tiles_ends = Uint16Array( + buffer, + offset, + non_empty_tiles_number + ); + offset += 2 * non_empty_tiles_number; + } else { + throw "unknown size"; } - tile_start_offset(tile_index) { - if (tile_index <= this.non_empty_tiles[0]) { - return 0; - } else { - return this.tile_end_offset(tile_index - 1); - } + return [this, offset]; + } + tile_start_offset(tile_index) { + if (tile_index <= this.non_empty_tiles[0]) { + return 0; + } else { + return this.tile_end_offset(tile_index - 1); } - tile_end_offset(tile_index) { - let me_or_before = binary_search(this.non_empty_tiles, tile_index); - if (me_or_before === null) { - return 0; - } - if (me_or_before >= this.non_empty_tiles_ends.length) { - return ( - this.non_empty_tiles_ends[this.non_empty_tiles.length - 1] * - this.entry_size - ); - } else { - return this.non_empty_tiles_ends[me_or_before] * this.entry_size; - } + } + tile_end_offset(tile_index) { + let me_or_before = binary_search(this.non_empty_tiles, tile_index); + if (me_or_before === null) { + return 0; } - end_offset() { - return ( - this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] * - this.entry_size - ); + if (me_or_before >= this.non_empty_tiles_ends.length) { + return ( + this.non_empty_tiles_ends[this.non_empty_tiles.length - 1] * + this.entry_size + ); + } else { + return this.non_empty_tiles_ends[me_or_before] * this.entry_size; } + } + end_offset() { + return ( + this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] * + this.entry_size + ); + } +} + +// this function is not inlined to avoid array declaration in jit +function center_points(points, scaled_current_x, scaled_current_y) { + return g.transformVertices(points, [ + 1, + 0, + 0, + 1, + -scaled_current_x, + -scaled_current_y, + ]); +} + +// this function is not inlined to avoid array declaration in jit +function rotate_points(points, c, s) { + let center_x = g.getWidth() / 2; + let center_y = g.getHeight() / 2 + Y_OFFSET; + + return g.transformVertices(points, [-c, s, s, c, center_x, center_y]); } class Map { - constructor(buffer, offset, filename) { - this.points_cache = []; // don't refetch points all the time - // header - let color_array = Uint8Array(buffer, offset, 3); - this.color = [ - color_array[0] / 255, - color_array[1] / 255, - color_array[2] / 255, - ]; - offset += 3; - this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile - offset += 2 * 4; - this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height - offset += 2 * 4; - this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates - offset += 2 * 8; - let side_array = Float64Array(buffer, offset, 1); // side of a tile - this.side = side_array[0]; - offset += 8; - - // tiles offsets - let res = new TilesOffsets(buffer, offset); - this.tiles_offsets = res[0]; - offset = res[1]; - - // now, do binary ways - // since the file is so big we'll go line by line - let binary_lines = []; - for (let y = 0; y < this.grid_size[1]; y++) { - let first_tile_start = this.tiles_offsets.tile_start_offset( - y * this.grid_size[0] + constructor(buffer, offset, filename) { + this.points_cache = []; // don't refetch points all the time + // header + let color_array = Uint8Array(buffer, offset, 3); + this.color = [ + color_array[0] / 255, + color_array[1] / 255, + color_array[2] / 255, + ]; + offset += 3; + this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile + offset += 2 * 4; + this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height + offset += 2 * 4; + this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates + offset += 2 * 8; + let side_array = Float64Array(buffer, offset, 1); // side of a tile + this.side = side_array[0]; + offset += 8; + + // tiles offsets + let res = new TilesOffsets(buffer, offset); + this.tiles_offsets = res[0]; + offset = res[1]; + + // now, do binary ways + // since the file is so big we'll go line by line + let binary_lines = []; + for (let y = 0; y < this.grid_size[1]; y++) { + let first_tile_start = this.tiles_offsets.tile_start_offset( + y * this.grid_size[0] + ); + let last_tile_end = this.tiles_offsets.tile_start_offset( + (y + 1) * this.grid_size[0] + ); + let size = last_tile_end - first_tile_start; + let string = s.read(filename, offset + first_tile_start, size); + let array = Uint8Array(E.toArrayBuffer(string)); + binary_lines.push(array); + } + this.binary_lines = binary_lines; + offset += this.tiles_offsets.end_offset(); + + return [this, offset]; + + // now do streets data header + // let streets_header = E.toArrayBuffer(s.read(filename, offset, 8)); + // let streets_header_offset = 0; + // let full_streets_size = Uint32Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 4; + // let blocks_number = Uint16Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 2; + // let labels_string_size = Uint16Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 2; + // offset += streets_header_offset; + + // // continue with main streets labels + // main_streets_labels = s.read(filename, offset, labels_string_size); + // // this.main_streets_labels = main_streets_labels.split(/\r?\n/); + // this.main_streets_labels = main_streets_labels.split(/\n/); + // offset += labels_string_size; + + // // continue with blocks start offsets + // this.blocks_offsets = Uint32Array( + // E.toArrayBuffer(s.read(filename, offset, blocks_number * 4)) + // ); + // offset += blocks_number * 4; + + // // continue with compressed street blocks + // let encoded_blocks_size = + // full_streets_size - 4 - 2 - 2 - labels_string_size - blocks_number * 4; + // this.compressed_streets = Uint8Array( + // E.toArrayBuffer(s.read(filename, offset, encoded_blocks_size)) + // ); + // offset += encoded_blocks_size; + } + + display( + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + g.setColor(this.color[0], this.color[1], this.color[2]); + let local_x = displayed_x - this.start_coordinates[0]; + let local_y = displayed_y - this.start_coordinates[1]; + let tile_x = Math.floor(local_x / this.side); + let tile_y = Math.floor(local_y / this.side); + + let limit = 1; + if (!zoomed) { + limit = 2; + } + for (let y = tile_y - limit; y <= tile_y + limit; y++) { + if (y < 0 || y >= this.grid_size[1]) { + continue; + } + for (let x = tile_x - limit; x <= tile_x + limit; x++) { + if (x < 0 || x >= this.grid_size[0]) { + continue; + } + if ( + this.tile_is_on_screen( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction + ) + ) { + // let colors = [ + // [0, 0, 0], + // [0, 0, 1], + // [0, 1, 0], + // [0, 1, 1], + // [1, 0, 0], + // [1, 0, 1], + // [1, 1, 0], + // [1, 1, 0.5], + // [0.5, 0, 0.5], + // [0, 0.5, 0.5], + // ]; + if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) { + this.display_thick_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction ); - let last_tile_end = this.tiles_offsets.tile_start_offset( - (y + 1) * this.grid_size[0] + } else { + this.display_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction ); - let size = last_tile_end - first_tile_start; - let string = s.read(filename, offset + first_tile_start, size); - let array = Uint8Array(E.toArrayBuffer(string)); - binary_lines.push(array); - } - this.binary_lines = binary_lines; - offset += this.tiles_offsets.end_offset(); - - return [this, offset]; - - // now do streets data header - // let streets_header = E.toArrayBuffer(s.read(filename, offset, 8)); - // let streets_header_offset = 0; - // let full_streets_size = Uint32Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 4; - // let blocks_number = Uint16Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 2; - // let labels_string_size = Uint16Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 2; - // offset += streets_header_offset; - - // // continue with main streets labels - // main_streets_labels = s.read(filename, offset, labels_string_size); - // // this.main_streets_labels = main_streets_labels.split(/\r?\n/); - // this.main_streets_labels = main_streets_labels.split(/\n/); - // offset += labels_string_size; - - // // continue with blocks start offsets - // this.blocks_offsets = Uint32Array( - // E.toArrayBuffer(s.read(filename, offset, blocks_number * 4)) - // ); - // offset += blocks_number * 4; - - // // continue with compressed street blocks - // let encoded_blocks_size = - // full_streets_size - 4 - 2 - 2 - labels_string_size - blocks_number * 4; - // this.compressed_streets = Uint8Array( - // E.toArrayBuffer(s.read(filename, offset, encoded_blocks_size)) - // ); - // offset += encoded_blocks_size; - } - - display( - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - g.setColor(this.color[0], this.color[1], this.color[2]); - let local_x = displayed_x - this.start_coordinates[0]; - let local_y = displayed_y - this.start_coordinates[1]; - let tile_x = Math.floor(local_x / this.side); - let tile_y = Math.floor(local_y / this.side); - - let limit = 1; - if (!zoomed) { - limit = 2; - } - for (let y = tile_y - limit; y <= tile_y + limit; y++) { - if (y < 0 || y >= this.grid_size[1]) { - continue; - } - for (let x = tile_x - limit; x <= tile_x + limit; x++) { - if (x < 0 || x >= this.grid_size[0]) { - continue; - } - if ( - this.tile_is_on_screen( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ) - ) { - // let colors = [ - // [0, 0, 0], - // [0, 0, 1], - // [0, 1, 0], - // [0, 1, 1], - // [1, 0, 0], - // [1, 0, 1], - // [1, 1, 0], - // [1, 1, 0.5], - // [0.5, 0, 0.5], - // [0, 0.5, 0.5], - // ]; - if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) { - this.display_thick_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ); - } else { - this.display_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ); - } - } - } + } } + } } - - tile_is_on_screen( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - let width = g.getWidth(); - let height = g.getHeight(); - let center_x = width / 2; - let center_y = height / 2 + Y_OFFSET; - let side = this.side; - let tile_center_x = (tile_x + 0.5) * side; - let tile_center_y = (tile_y + 0.5) * side; - let scaled_center_x = (tile_center_x - current_x) * scale_factor; - let scaled_center_y = (tile_center_y - current_y) * scale_factor; - let rotated_center_x = scaled_center_x * cos_direction - scaled_center_y * sin_direction; - let rotated_center_y = scaled_center_x * sin_direction + scaled_center_y * cos_direction; - let on_screen_center_x = center_x - rotated_center_x; - let on_screen_center_y = center_y + rotated_center_y; - - let scaled_side = side * scale_factor * Math.sqrt(1 / 2); - - if (on_screen_center_x + scaled_side <= 0) { - return false; - } - if (on_screen_center_x - scaled_side >= width) { - return false; - } - if (on_screen_center_y + scaled_side <= 0) { - return false; - } - if (on_screen_center_y - scaled_side >= height) { - return false; - } - return true; + } + + tile_is_on_screen( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + let width = g.getWidth(); + let height = g.getHeight(); + let center_x = width / 2; + let center_y = height / 2 + Y_OFFSET; + let side = this.side; + let tile_center_x = (tile_x + 0.5) * side; + let tile_center_y = (tile_y + 0.5) * side; + let scaled_center_x = (tile_center_x - current_x) * scale_factor; + let scaled_center_y = (tile_center_y - current_y) * scale_factor; + let rotated_center_x = + scaled_center_x * cos_direction - scaled_center_y * sin_direction; + let rotated_center_y = + scaled_center_x * sin_direction + scaled_center_y * cos_direction; + let on_screen_center_x = center_x - rotated_center_x; + let on_screen_center_y = center_y + rotated_center_y; + + let scaled_side = side * scale_factor * Math.sqrt(1 / 2); + + if (on_screen_center_x + scaled_side <= 0) { + return false; } - - tile_points(tile_num, tile_x, tile_y, scaled_side) { - let line_start_offset = this.tiles_offsets.tile_start_offset( - tile_y * this.grid_size[0] - ); - let offset = - this.tiles_offsets.tile_start_offset(tile_num) - line_start_offset; - let upper_limit = - this.tiles_offsets.tile_end_offset(tile_num) - line_start_offset; - - let line = this.binary_lines[tile_y]; - // we need to copy both for correct results and for performances - // let's precompute also. - let cached_tile = new Float64Array(upper_limit - offset); - for (let i = offset; i < upper_limit; i += 2) { - let x = (tile_x + line.buffer[i] / 255) * scaled_side; - let y = (tile_y + line.buffer[i + 1] / 255) * scaled_side; - cached_tile[i - offset] = x; - cached_tile[i + 1 - offset] = y; - } - return cached_tile; + if (on_screen_center_x - scaled_side >= width) { + return false; } - - invalidate_caches() { - this.points_cache = []; + if (on_screen_center_y + scaled_side <= 0) { + return false; } + if (on_screen_center_y - scaled_side >= height) { + return false; + } + return true; + } - fetch_points(tile_x, tile_y, scaled_side) { - let tile_num = tile_x + tile_y * this.grid_size[0]; - for (let i = 0; i < this.points_cache.length; i++) { - if (this.points_cache[i][0] == tile_num) { - return this.points_cache[i][1]; - } - } - if (this.points_cache.length > 40) { - this.points_cache.shift(); - } - let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); - this.points_cache.push([tile_num, points]); - return points; + tile_points(tile_num, tile_x, tile_y, scaled_side) { + let line_start_offset = this.tiles_offsets.tile_start_offset( + tile_y * this.grid_size[0] + ); + let offset = + this.tiles_offsets.tile_start_offset(tile_num) - line_start_offset; + let upper_limit = + this.tiles_offsets.tile_end_offset(tile_num) - line_start_offset; + + let line = this.binary_lines[tile_y]; + // we need to copy both for correct results and for performances + // let's precompute also. + let cached_tile = new Float64Array(upper_limit - offset); + for (let i = offset; i < upper_limit; i += 2) { + let x = (tile_x + line.buffer[i] / 255) * scaled_side; + let y = (tile_y + line.buffer[i + 1] / 255) * scaled_side; + cached_tile[i - offset] = x; + cached_tile[i + 1 - offset] = y; + } + return cached_tile; + } + + invalidate_caches() { + this.points_cache = []; + } + + fetch_points(tile_x, tile_y, scaled_side) { + let tile_num = tile_x + tile_y * this.grid_size[0]; + for (let i = 0; i < this.points_cache.length; i++) { + if (this.points_cache[i][0] == tile_num) { + return this.points_cache[i][1]; + } + } + if (this.points_cache.length > 40) { + this.points_cache.shift(); } + let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); + this.points_cache.push([tile_num, points]); + return points; + } + + display_tile( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + "jit"; + let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + let scaled_current_x = current_x * scale_factor; + let scaled_current_y = current_y * scale_factor; + let recentered_points = center_points( + points, + scaled_current_x, + scaled_current_y + ); + let screen_points = rotate_points( + recentered_points, + cos_direction, + sin_direction + ); - display_tile( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - // "jit"; - let center_x = g.getWidth() / 2; - let center_y = g.getHeight() / 2 + Y_OFFSET; - - let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - let scaled_current_x = current_x * scale_factor; - let scaled_current_y = current_y * scale_factor; - let recentered_points = g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]); - let c = cos_direction; - let s = sin_direction; - let screen_points = g.transformVertices(recentered_points, [-c, s, s, c, center_x, center_y]); - - for (let i = 0; i < screen_points.length; i += 4) { - g.drawLine(screen_points[i], screen_points[i + 1], screen_points[i + 2], screen_points[i + 3]); - } + for (let i = 0; i < screen_points.length; i += 4) { + g.drawLine( + screen_points[i], + screen_points[i + 1], + screen_points[i + 2], + screen_points[i + 3] + ); } + } + + display_thick_tile( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + "jit"; + + let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + let scaled_current_x = current_x * scale_factor; + let scaled_current_y = current_y * scale_factor; + let recentered_points = center_points( + points, + scaled_current_x, + scaled_current_y + ); + let screen_points = rotate_points( + recentered_points, + cos_direction, + sin_direction + ); - display_thick_tile( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - // "jit"; - let center_x = g.getWidth() / 2; - let center_y = g.getHeight() / 2 + Y_OFFSET; - - let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - let scaled_current_x = current_x * scale_factor; - let scaled_current_y = current_y * scale_factor; - let recentered_points = g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]); - let c = cos_direction; - let s = sin_direction; - let screen_points = g.transformVertices(recentered_points, [-c, s, s, c, center_x, center_y]); - - for (let i = 0; i < screen_points.length; i += 4) { - let final_x = screen_points[i]; - let final_y = screen_points[i + 1]; - let new_final_x = screen_points[i + 2]; - let new_final_y = screen_points[i + 3]; - - let xdiff = new_final_x - final_x; - let ydiff = new_final_y - final_y; - let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff); - let ox = (-ydiff / d) * 3; - let oy = (xdiff / d) * 3; - g.fillPoly([ - final_x + ox, - final_y + oy, - new_final_x + ox, - new_final_y + oy, - new_final_x - ox, - new_final_y - oy, - final_x - ox, - final_y - oy, - ]); - } + for (let i = 0; i < screen_points.length; i += 4) { + let final_x = screen_points[i]; + let final_y = screen_points[i + 1]; + let new_final_x = screen_points[i + 2]; + let new_final_y = screen_points[i + 3]; + + let xdiff = new_final_x - final_x; + let ydiff = new_final_y - final_y; + let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff); + let ox = (-ydiff / d) * 3; + let oy = (xdiff / d) * 3; + g.fillPoly([ + final_x + ox, + final_y + oy, + new_final_x + ox, + new_final_y + oy, + new_final_x - ox, + new_final_y - oy, + final_x - ox, + final_y - oy, + ]); } + } } class Interests { - constructor(buffer, offset) { - this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile - offset += 2 * 4; - this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height - offset += 2 * 4; - this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates - offset += 2 * 8; - let side_array = Float64Array(buffer, offset, 1); // side of a tile - this.side = side_array[0]; - offset += 8; - - let res = new TilesOffsets(buffer, offset); - offset = res[1]; - this.offsets = res[0]; - let end = this.offsets.end_offset(); - this.binary_interests = new Uint8Array(end); - let binary_interests = Uint8Array(buffer, offset, end); - for (let i = 0; i < end; i++) { - this.binary_interests[i] = binary_interests[i]; - } - offset += end; - this.points_cache = []; - return [this, offset]; + constructor(buffer, offset) { + this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile + offset += 2 * 4; + this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height + offset += 2 * 4; + this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates + offset += 2 * 8; + let side_array = Float64Array(buffer, offset, 1); // side of a tile + this.side = side_array[0]; + offset += 8; + + let res = new TilesOffsets(buffer, offset); + offset = res[1]; + this.offsets = res[0]; + let end = this.offsets.end_offset(); + this.binary_interests = new Uint8Array(end); + let binary_interests = Uint8Array(buffer, offset, end); + for (let i = 0; i < end; i++) { + this.binary_interests[i] = binary_interests[i]; } - - display( - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - let local_x = displayed_x - this.start_coordinates[0]; - let local_y = displayed_y - this.start_coordinates[1]; - let tile_x = Math.floor(local_x / this.side); - let tile_y = Math.floor(local_y / this.side); - for (let y = tile_y - 1; y <= tile_y + 1; y++) { - if (y < 0 || y >= this.grid_size[1]) { - continue; - } - for (let x = tile_x - 1; x <= tile_x + 1; x++) { - if (x < 0 || x >= this.grid_size[0]) { - continue; - } - this.display_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ); - } + offset += end; + this.points_cache = []; + return [this, offset]; + } + + display( + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + let local_x = displayed_x - this.start_coordinates[0]; + let local_y = displayed_y - this.start_coordinates[1]; + let tile_x = Math.floor(local_x / this.side); + let tile_y = Math.floor(local_y / this.side); + for (let y = tile_y - 1; y <= tile_y + 1; y++) { + if (y < 0 || y >= this.grid_size[1]) { + continue; + } + for (let x = tile_x - 1; x <= tile_x + 1; x++) { + if (x < 0 || x >= this.grid_size[0]) { + continue; } + this.display_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction + ); + } } - - tile_points(tile_num, tile_x, tile_y, scaled_side) { - let offset = this.offsets.tile_start_offset(tile_num); - let upper_limit = this.offsets.tile_end_offset(tile_num); - - let tile_interests = []; - for (let i = offset; i < upper_limit; i += 3) { - let interest = this.binary_interests[i]; - let x = (tile_x + this.binary_interests[i + 1] / 255) * scaled_side; - let y = (tile_y + this.binary_interests[i + 2] / 255) * scaled_side; - if (interest >= interests_colors.length) { - throw "bad interest" + interest + "at" + tile_num + "offset" + i; - } - tile_interests.push(interest); - tile_interests.push(x); - tile_interests.push(y); - } - return tile_interests; - } - fetch_points(tile_x, tile_y, scaled_side) { - //TODO: factorize with map ? - let tile_num = tile_x + tile_y * this.grid_size[0]; - for (let i = 0; i < this.points_cache.length; i++) { - if (this.points_cache[i][0] == tile_num) { - return this.points_cache[i][1]; - } - } - if (this.points_cache.length > 40) { - this.points_cache.shift(); - } - let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); - this.points_cache.push([tile_num, points]); - return points; - } - invalidate_caches() { - this.points_cache = []; - } - display_tile( - tile_x, - tile_y, - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - let width = g.getWidth(); - let half_width = width / 2; - let half_height = g.getHeight() / 2 + Y_OFFSET; - let interests = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - - let scaled_current_x = displayed_x * scale_factor; - let scaled_current_y = displayed_y * scale_factor; - - for (let i = 0; i < interests.length; i += 3) { - let type = interests[i]; - let x = interests[i + 1]; - let y = interests[i + 2]; - - let scaled_x = x - scaled_current_x; - let scaled_y = y - scaled_current_y; - let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let final_x = half_width - rotated_x; - let final_y = half_height + rotated_y; - - let color = interests_colors[type]; - if (type == 0) { - g.setColor(0, 0, 0).fillCircle(final_x, final_y, 6); - } - g.setColor(color).fillCircle(final_x, final_y, 5); - } + } + + tile_points(tile_num, tile_x, tile_y, scaled_side) { + let offset = this.offsets.tile_start_offset(tile_num); + let upper_limit = this.offsets.tile_end_offset(tile_num); + + let tile_interests = []; + for (let i = offset; i < upper_limit; i += 3) { + let interest = this.binary_interests[i]; + let x = (tile_x + this.binary_interests[i + 1] / 255) * scaled_side; + let y = (tile_y + this.binary_interests[i + 2] / 255) * scaled_side; + if (interest >= interests_colors.length) { + throw "bad interest" + interest + "at" + tile_num + "offset" + i; + } + tile_interests.push(interest); + tile_interests.push(x); + tile_interests.push(y); + } + return tile_interests; + } + fetch_points(tile_x, tile_y, scaled_side) { + //TODO: factorize with map ? + let tile_num = tile_x + tile_y * this.grid_size[0]; + for (let i = 0; i < this.points_cache.length; i++) { + if (this.points_cache[i][0] == tile_num) { + return this.points_cache[i][1]; + } + } + if (this.points_cache.length > 40) { + this.points_cache.shift(); } + let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); + this.points_cache.push([tile_num, points]); + return points; + } + invalidate_caches() { + this.points_cache = []; + } + display_tile( + tile_x, + tile_y, + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + let width = g.getWidth(); + let half_width = width / 2; + let half_height = g.getHeight() / 2 + Y_OFFSET; + let interests = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + + let scaled_current_x = displayed_x * scale_factor; + let scaled_current_y = displayed_y * scale_factor; + + for (let i = 0; i < interests.length; i += 3) { + let type = interests[i]; + let x = interests[i + 1]; + let y = interests[i + 2]; + + let scaled_x = x - scaled_current_x; + let scaled_y = y - scaled_current_y; + let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; + let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; + let final_x = half_width - rotated_x; + let final_y = half_height + rotated_y; + + let color = interests_colors[type]; + if (type == 0) { + g.setColor(0, 0, 0).fillCircle(final_x, final_y, 6); + } + g.setColor(color).fillCircle(final_x, final_y, 5); + } + } } class Status { - constructor(path, maps, interests) { - this.path = path; - this.default_options = true; // do we still have default options ? - this.active = false; // should we have screen on - this.last_activity = getTime(); - this.maps = maps; - this.interests = interests; - let half_screen_width = g.getWidth() / 2; - let half_screen_height = g.getHeight() / 2; - let half_screen_diagonal = Math.sqrt( - half_screen_width * half_screen_width + - half_screen_height * half_screen_height - ); - this.scale_factor = half_screen_diagonal / maps[0].side; // multiply geo coordinates by this to get pixels coordinates - this.on_path = true; // are we on the path or lost ? - this.position = null; // where we are - this.adjusted_cos_direction = 1; // cos of where we look at - this.adjusted_sin_direction = 0; // sin of where we look at - this.current_segment = null; // which segment is closest - this.reaching = null; // which waypoint are we reaching ? - this.distance_to_next_point = null; // how far are we from next point ? - this.projected_point = null; - - if (this.path !== null) { - let r = [0]; - // let's do a reversed prefix computations on all distances: - // loop on all segments in reversed order - let previous_point = null; - for (let i = this.path.len - 1; i >= 0; i--) { - let point = this.path.point(i); - if (previous_point !== null) { - r.unshift(r[0] + point.distance(previous_point)); - } - previous_point = point; - } - this.remaining_distances = r; // how much distance remains at start of each segment - } - this.starting_time = null; // time we start - this.advanced_distance = 0.0; - this.gps_coordinates_counter = 0; // how many coordinates did we receive - this.old_points = []; // record previous points but only when enough distance between them - this.old_times = []; // the corresponding times - } - activate() { - this.last_activity = getTime(); - if (this.active) { - return; - } else { - this.active = true; - Bangle.setLCDBrightness(settings.brightness); - Bangle.setLocked(false); - if (settings.power_lcd_off) { - Bangle.setLCDPower(true); - } + constructor(path, maps, interests, heights) { + this.path = path; + this.default_options = true; // do we still have default options ? + this.active = false; // should we have screen on + this.last_activity = getTime(); + this.maps = maps; + this.interests = interests; + this.heights = heights; + this.screen = MAP; + let half_screen_width = g.getWidth() / 2; + let half_screen_height = g.getHeight() / 2; + let half_screen_diagonal = Math.sqrt( + half_screen_width * half_screen_width + + half_screen_height * half_screen_height + ); + this.scale_factor = half_screen_diagonal / maps[0].side; // multiply geo coordinates by this to get pixels coordinates + this.on_path = true; // are we on the path or lost ? + this.position = null; // where we are + this.adjusted_cos_direction = 1; // cos of where we look at + this.adjusted_sin_direction = 0; // sin of where we look at + this.current_segment = null; // which segment is closest + this.reaching = null; // which waypoint are we reaching ? + this.distance_to_next_point = null; // how far are we from next point ? + this.projected_point = null; + + if (this.path !== null) { + let r = [0]; + // let's do a reversed prefix computations on all distances: + // loop on all segments in reversed order + let previous_point = null; + for (let i = this.path.len - 1; i >= 0; i--) { + let point = this.path.point(i); + if (previous_point !== null) { + r.unshift(r[0] + point.distance(previous_point)); } + previous_point = point; + } + this.remaining_distances = r; // how much distance remains at start of each segment } - check_activity() { - if (!this.active || !powersaving) { - return; - } - if (getTime() - this.last_activity > 30) { - this.active = false; - Bangle.setLCDBrightness(0); - if (settings.power_lcd_off) { - Bangle.setLCDPower(false); - } - } + this.starting_time = null; // time we start + this.advanced_distance = 0.0; + this.gps_coordinates_counter = 0; // how many coordinates did we receive + this.old_points = []; // record previous points but only when enough distance between them + this.old_times = []; // the corresponding times + } + activate() { + this.last_activity = getTime(); + if (this.active) { + return; + } else { + this.active = true; + Bangle.setLCDBrightness(settings.brightness); + Bangle.setLocked(false); + if (settings.power_lcd_off) { + Bangle.setLCDPower(true); + } } - invalidate_caches() { - for (let i = 0; i < this.maps.length; i++) { - this.maps[i].invalidate_caches(); - } - if (this.interests !== null) { - this.interests.invalidate_caches(); - } + } + check_activity() { + if (!this.active || !powersaving) { + return; } - new_position_reached(position) { - // we try to figure out direction by looking at previous points - // instead of the gps course which is not very nice. - - let now = getTime(); - - if (this.old_points.length == 0) { - this.gps_coordinates_counter += 1; - this.old_points.push(position); - this.old_times.push(now); - return null; - } else { - let previous_point = this.old_points[this.old_points.length - 1]; - let distance_to_previous = previous_point.distance(position); - // gps signal is noisy but rarely above 5 meters - if (distance_to_previous < 5) { - // update instant speed and return - let oldest_point = this.old_points[0]; - let distance_to_oldest = oldest_point.distance(position); - this.instant_speed = distance_to_oldest / (now - this.old_times[0]); - return null; - } - } - this.gps_coordinates_counter += 1; - this.old_points.push(position); - this.old_times.push(now); - + if (getTime() - this.last_activity > settings.active_time) { + this.active = false; + Bangle.setLCDBrightness(0); + if (settings.power_lcd_off) { + Bangle.setLCDPower(false); + } + } + } + invalidate_caches() { + for (let i = 0; i < this.maps.length; i++) { + this.maps[i].invalidate_caches(); + } + if (this.interests !== null) { + this.interests.invalidate_caches(); + } + } + new_position_reached(position) { + // we try to figure out direction by looking at previous points + // instead of the gps course which is not very nice. + + let now = getTime(); + + if (this.old_points.length == 0) { + this.gps_coordinates_counter += 1; + this.old_points.push(position); + this.old_times.push(now); + return null; + } else { + let previous_point = this.old_points[this.old_points.length - 1]; + let distance_to_previous = previous_point.distance(position); + // gps signal is noisy but rarely above 5 meters + if (distance_to_previous < 5) { + // update instant speed and return let oldest_point = this.old_points[0]; let distance_to_oldest = oldest_point.distance(position); - - // every 3 points we count the distance - if (this.gps_coordinates_counter % 3 == 0) { - if (distance_to_oldest < 150.0) { - // to avoid gps glitches - this.advanced_distance += distance_to_oldest; - } - } - this.instant_speed = distance_to_oldest / (now - this.old_times[0]); + return null; + } + } + this.gps_coordinates_counter += 1; + this.old_points.push(position); + this.old_times.push(now); + + let oldest_point = this.old_points[0]; + let distance_to_oldest = oldest_point.distance(position); + + // every 3 points we count the distance + if (this.gps_coordinates_counter % 3 == 0) { + if (distance_to_oldest < 150.0) { + // to avoid gps glitches + this.advanced_distance += distance_to_oldest; + } + } - if (this.old_points.length == 4) { - this.old_points.shift(); - this.old_times.shift(); - } - // let's just take angle of segment between newest point and a point a bit before - let previous_index = this.old_points.length - 3; - if (previous_index < 0) { - previous_index = 0; - } - let diff = position.minus(this.old_points[previous_index]); - let angle = Math.atan2(diff.lat, diff.lon); - return angle; - } - update_position(new_position) { - let direction = this.new_position_reached(new_position); - if (direction === null) { - if (this.old_points.length > 1) { - this.display(); // re-display because speed has changed - } - return; - } - if (in_menu) { - return; - } - if (this.instant_speed * 3.6 < 13) { - this.activate(); // if we go too slow turn on, we might be looking for the direction to follow - if (!this.default_options) { - this.default_options = true; - - Bangle.setOptions({ - lockTimeout: 10000, - backlightTimeout: 10000, - wakeOnTwist: true, - powerSave: true, - }); - } - } else { - if (this.default_options) { - this.default_options = false; - - Bangle.setOptions({ - lockTimeout: 0, - backlightTimeout: 0, - lcdPowerTimeout: 0, - hrmSportMode: 2, - wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. - wakeOnFaceUp: false, - wakeOnTouch: true, - powerSave: false, - }); - Bangle.setPollInterval(2000); // disable accelerometer as much as we can (a value of 4000 seem to cause hard reboot crashes (segfaults ?) so keep 2000) - } - - } - this.check_activity(); // if we don't move or are in menu we should stay on - - this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); - this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); - this.angle = direction; - let cos_direction = Math.cos(direction); - let sin_direction = Math.sin(direction); - this.position = new_position; - - // we will display position of where we'll be at in a few seconds - // and not where we currently are. - // this is because the display has more than 1sec duration. - this.displayed_position = new Point( - new_position.lon + cos_direction * this.instant_speed * 0.00001, - new_position.lat + sin_direction * this.instant_speed * 0.00001 - ); - - if (this.path !== null) { - // detect segment we are on now - let res = this.path.nearest_segment( - this.displayed_position, - Math.max(0, this.current_segment - 1), - Math.min(this.current_segment + 2, this.path.len - 1), - cos_direction, - sin_direction - ); - let orientation = res[0]; - let next_segment = res[1]; - - if (this.is_lost(next_segment)) { - // start_profiling(); - // it did not work, try anywhere - res = this.path.nearest_segment( - this.displayed_position, - 0, - this.path.len - 1, - cos_direction, - sin_direction - ); - orientation = res[0]; - next_segment = res[1]; - // end_profiling("repositioning"); - } - // now check if we strayed away from path or back to it - let lost = this.is_lost(next_segment); - if (this.on_path == lost) { - this.activate(); - // if status changes - if (lost) { - Bangle.buzz(); // we lost path - setTimeout(() => Bangle.buzz(), 500); - setTimeout(() => Bangle.buzz(), 1000); - setTimeout(() => Bangle.buzz(), 1500); - } - this.on_path = !lost; - } + this.instant_speed = distance_to_oldest / (now - this.old_times[0]); - this.current_segment = next_segment; + if (this.old_points.length == 4) { + this.old_points.shift(); + this.old_times.shift(); + } + // let's just take angle of segment between newest point and a point a bit before + let previous_index = this.old_points.length - 3; + if (previous_index < 0) { + previous_index = 0; + } + let diff = position.minus(this.old_points[previous_index]); + let angle = Math.atan2(diff.lat, diff.lon); + return angle; + } + update_position(new_position) { + let direction = this.new_position_reached(new_position); + if (direction === null) { + if (this.old_points.length > 1) { + this.display(); // re-display because speed has changed + } + return; + } + if (in_menu) { + return; + } + if (this.instant_speed * 3.6 < settings.wake_up_speed) { + this.activate(); // if we go too slow turn on, we might be looking for the direction to follow + if (!this.default_options) { + this.default_options = true; + + Bangle.setOptions({ + lockTimeout: 0, + backlightTimeout: 10000, + wakeOnTwist: true, + powerSave: true, + }); + } + } else { + if (this.default_options) { + this.default_options = false; + + Bangle.setOptions({ + lockTimeout: 0, + backlightTimeout: 0, + lcdPowerTimeout: 0, + hrmSportMode: 2, + wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. + wakeOnFaceUp: false, + wakeOnTouch: true, + powerSave: false, + }); + } + } + this.check_activity(); // if we don't move or are in menu we should stay on + + this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); + this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); + this.angle = direction; + let cos_direction = Math.cos(direction); + let sin_direction = Math.sin(direction); + this.position = new_position; + + // we will display position of where we'll be at in a few seconds + // and not where we currently are. + // this is because the display has more than 1sec duration. + this.displayed_position = new Point( + new_position.lon + cos_direction * this.instant_speed * 0.00001, + new_position.lat + sin_direction * this.instant_speed * 0.00001 + ); - // check if we are nearing the next point on our path and alert the user - let next_point = this.current_segment + (1 - orientation); - this.distance_to_next_point = Math.ceil( - this.position.distance(this.path.point(next_point)) - ); + if (this.path !== null) { + // detect segment we are on now + let res = this.path.nearest_segment( + this.displayed_position, + Math.max(0, this.current_segment - 1), + Math.min(this.current_segment + 2, this.path.len - 1), + cos_direction, + sin_direction + ); + let orientation = res[0]; + let next_segment = res[1]; - // disable gps when far from next point and locked - // if (Bangle.isLocked() && !settings.keep_gps_alive) { - // let time_to_next_point = - // (this.distance_to_next_point * 3.6) / settings.max_speed; - // if (time_to_next_point > 60) { - // Bangle.setGPSPower(false, "gipy"); - // setTimeout(function () { - // Bangle.setGPSPower(true, "gipy"); - // }, time_to_next_point); - // } - // } - if (this.reaching != next_point && this.distance_to_next_point <= 100) { - this.activate(); - this.reaching = next_point; - let reaching_waypoint = this.path.is_waypoint(next_point); - if (reaching_waypoint) { - if (settings.buzz_on_turns) { - Bangle.buzz(); - setTimeout(() => Bangle.buzz(), 500); - setTimeout(() => Bangle.buzz(), 1000); - setTimeout(() => Bangle.buzz(), 1500); - } - } - } + if (this.is_lost(next_segment)) { + // start_profiling(); + // it did not work, try anywhere + res = this.path.nearest_segment( + this.displayed_position, + 0, + this.path.len - 1, + cos_direction, + sin_direction + ); + orientation = res[0]; + next_segment = res[1]; + // end_profiling("repositioning"); + } + // now check if we strayed away from path or back to it + let lost = this.is_lost(next_segment); + if (this.on_path == lost) { + // if status changes + if (lost) { + Bangle.buzz(); // we lost path + setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); } - - // abort most frames if inactive - if (!this.active && this.gps_coordinates_counter % 5 != 0) { - return; + this.on_path = !lost; + } + if (!this.on_path) { + this.activate(); + } + + this.current_segment = next_segment; + + // check if we are nearing the next point on our path and alert the user + let next_point = this.current_segment + (1 - orientation); + this.distance_to_next_point = Math.ceil( + this.position.distance(this.path.point(next_point)) + ); + + // disable gps when far from next point and locked + // if (Bangle.isLocked() && !settings.keep_gps_alive) { + // let time_to_next_point = + // (this.distance_to_next_point * 3.6) / settings.max_speed; + // if (time_to_next_point > 60) { + // Bangle.setGPSPower(false, "gipy"); + // setTimeout(function () { + // Bangle.setGPSPower(true, "gipy"); + // }, time_to_next_point); + // } + // } + if (this.distance_to_next_point <= 100) { + this.activate(); + } + if (this.reaching != next_point && this.distance_to_next_point <= 100) { + this.reaching = next_point; + let reaching_waypoint = this.path.is_waypoint(next_point); + if (reaching_waypoint) { + if (settings.buzz_on_turns) { + Bangle.buzz(); + setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); + } } + } + } - // re-display - this.display(); + // abort most frames if inactive + if (!this.active && this.gps_coordinates_counter % 5 != 0) { + return; } - display_direction() { - //TODO: go towards point on path at 20 meter - if (this.current_segment === null) { - return; - } - let next_point = this.path.point(this.current_segment + (1 - go_backwards)); - let distance_to_next_point = Math.ceil( - this.projected_point.distance(next_point) - ); - let towards; - if (distance_to_next_point < 20) { - towards = this.path.point(this.current_segment + 2 * (1 - go_backwards)); - } else { - towards = next_point; - } - let diff = towards.minus(this.projected_point); - direction = Math.atan2(diff.lat, diff.lon); + // re-display + this.display(); + } + display_direction() { + //TODO: go towards point on path at 20 meter + if (this.current_segment === null) { + return; + } + let next_point = this.path.point(this.current_segment + (1 - go_backwards)); - let full_angle = direction - this.angle; - // let c = towards.coordinates(p, this.adjusted_cos_direction, this.adjusted_sin_direction, this.scale_factor); - // g.setColor(1,0,1).fillCircle(c[0], c[1], 5); + let distance_to_next_point = Math.ceil( + this.projected_point.distance(next_point) + ); + let towards; + if (distance_to_next_point < 20) { + towards = this.path.point(this.current_segment + 2 * (1 - go_backwards)); + } else { + towards = next_point; + } + let diff = towards.minus(this.projected_point); + direction = Math.atan2(diff.lat, diff.lon); - let scale; - if (zoomed) { - scale = this.scale_factor; - } else { - scale = this.scale_factor / 2; - } + let full_angle = direction - this.angle; + // let c = towards.coordinates(p, this.adjusted_cos_direction, this.adjusted_sin_direction, this.scale_factor); + // g.setColor(1,0,1).fillCircle(c[0], c[1], 5); - c = this.projected_point.coordinates( - this.displayed_position, - this.adjusted_cos_direction, - this.adjusted_sin_direction, - scale - ); + let scale; + if (zoomed) { + scale = this.scale_factor; + } else { + scale = this.scale_factor / 2; + } - let cos1 = Math.cos(full_angle + 0.6 + Math.PI / 2); - let cos2 = Math.cos(full_angle + Math.PI / 2); - let cos3 = Math.cos(full_angle - 0.6 + Math.PI / 2); - let sin1 = Math.sin(-full_angle - 0.6 - Math.PI / 2); - let sin2 = Math.sin(-full_angle - Math.PI / 2); - let sin3 = Math.sin(-full_angle + 0.6 - Math.PI / 2); - g.setColor(0, 1, 0).fillPoly([ - c[0] + cos1 * 15, - c[1] + sin1 * 15, - c[0] + cos2 * 20, - c[1] + sin2 * 20, - c[0] + cos3 * 15, - c[1] + sin3 * 15, - c[0] + cos3 * 10, - c[1] + sin3 * 10, - c[0] + cos2 * 15, - c[1] + sin2 * 15, - c[0] + cos1 * 10, - c[1] + sin1 * 10, - ]); - } - remaining_distance() { - let remaining_in_correct_orientation = - this.remaining_distances[this.current_segment + 1] + - this.position.distance(this.path.point(this.current_segment + 1)); + c = this.projected_point.coordinates( + this.displayed_position, + this.adjusted_cos_direction, + this.adjusted_sin_direction, + scale + ); + let cos1 = Math.cos(full_angle + 0.6 + Math.PI / 2); + let cos2 = Math.cos(full_angle + Math.PI / 2); + let cos3 = Math.cos(full_angle - 0.6 + Math.PI / 2); + let sin1 = Math.sin(-full_angle - 0.6 - Math.PI / 2); + let sin2 = Math.sin(-full_angle - Math.PI / 2); + let sin3 = Math.sin(-full_angle + 0.6 - Math.PI / 2); + g.setColor(0, 1, 0).fillPoly([ + c[0] + cos1 * 15, + c[1] + sin1 * 15, + c[0] + cos2 * 20, + c[1] + sin2 * 20, + c[0] + cos3 * 15, + c[1] + sin3 * 15, + c[0] + cos3 * 10, + c[1] + sin3 * 10, + c[0] + cos2 * 15, + c[1] + sin2 * 15, + c[0] + cos1 * 10, + c[1] + sin1 * 10, + ]); + } + remaining_distance() { + if (go_backwards) { + return this.remaining_distances[0] - this.remaining_distances[this.current_segment] + + this.position.distance(this.path.point(this.current_segment)); + } else { + return this.remaining_distances[this.current_segment + 1] + + this.position.distance(this.path.point(this.current_segment + 1)); + } + } + // check if we are lost (too far from segment we think we are on) + // if we are adjust scale so that path will still be displayed. + // we do the scale adjustment here to avoid recomputations later on. + is_lost(segment) { + let projection = this.displayed_position.closest_segment_point( + this.path.point(segment), + this.path.point(segment + 1) + ); + this.projected_point = projection; // save this info for display + let distance_to_projection = this.displayed_position.distance(projection); + if (distance_to_projection > settings.lost_distance) { + return true; + } else { + return false; + } + } + display() { + if (displaying || in_menu) { + return; // don't draw on drawings + } + displaying = true; + g.clear(); + if (this.screen == MAP) { + this.display_map(); + } else { + let current_position = 0; + if (this.current_segment !== null) { if (go_backwards) { - return this.remaining_distances[0] - remaining_in_correct_orientation; + current_position = this.remaining_distance(); } else { - return remaining_in_correct_orientation; + current_position = + this.remaining_distances[0] - this.remaining_distance(); } - } - // check if we are lost (too far from segment we think we are on) - // if we are adjust scale so that path will still be displayed. - // we do the scale adjustment here to avoid recomputations later on. - is_lost(segment) { - let projection = this.displayed_position.closest_segment_point( - this.path.point(segment), - this.path.point(segment + 1) - ); - this.projected_point = projection; // save this info for display - let distance_to_projection = this.displayed_position.distance(projection); - if (distance_to_projection > settings.lost_distance) { - return true; + } + if (this.screen == HEIGHTS_FULL) { + this.display_heights(0, current_position, this.remaining_distances[0]); + } else { + // only display 2500m + let start; + if (go_backwards) { + start = Math.max(0, current_position - 2000); } else { - return false; + start = Math.max(0, current_position - 500); } + let length = Math.min(2500, this.remaining_distances[0] - start); + this.display_heights(start, current_position, length); + } + } + Bangle.drawWidgets(); + displaying = false; + } + display_heights(display_start, current_position, displayed_length) { + let path_length = this.remaining_distances[0]; + let widgets_height = 24; + let graph_width = g.getWidth(); + let graph_height = g.getHeight() - 20 - widgets_height; + + let distance_per_pixel = displayed_length / graph_width; + + let start_point_index = 0; + let end_point_index = this.path.len - 1; + for (let i = 0; i < this.path.len; i++) { + let point_distance = path_length - this.remaining_distances[i]; + if (point_distance <= display_start) { + start_point_index = i; + } + if (point_distance >= display_start + displayed_length) { + end_point_index = i; + break; + } + } + end_point_index = Math.min(end_point_index+1, this.path.len -1); + let max_height = Number.NEGATIVE_INFINITY; + let min_height = Number.POSITIVE_INFINITY; + for (let i = start_point_index; i <= end_point_index; i++) { + let height = this.heights[i]; + max_height = Math.max(max_height, height); + min_height = Math.min(min_height, height); + } + // we'll set the displayed height to a minimum value of 100m + // if we don't, then we'll see too much noise + if (max_height - min_height < 100) { + min_height = min_height - 10; + max_height = min_height + 110; } - display() { - if (displaying || in_menu) { - return; // don't draw on drawings - } - displaying = true; - g.clear(); - let scale_factor = this.scale_factor; - if (!zoomed) { - scale_factor /= 2; - } - // start_profiling(); - for (let i = 0; i < this.maps.length; i++) { - this.maps[i].display( - this.displayed_position.lon, - this.displayed_position.lat, - scale_factor, - this.adjusted_cos_direction, - this.adjusted_sin_direction + let displayed_height = max_height - min_height; + let height_per_pixel = displayed_height / graph_height; + // g.setColor(0, 0, 0).drawRect(0, widgets_height, graph_width, graph_height + widgets_height); + + let previous_x = null; + let previous_y = null; + let previous_height = null; + let previous_distance = null; + let current_x; + let current_y; + for (let i = start_point_index; i < end_point_index; i++) { + let point_distance = path_length - this.remaining_distances[i]; + let height = this.heights[i]; + let x = Math.round((point_distance - display_start) / distance_per_pixel); + if (go_backwards) { + x = graph_width - x; + } + let y = + widgets_height + + graph_height - + Math.round((height - min_height) / height_per_pixel); + if (x != previous_x) { + if (previous_x !== null) { + let steepness = + (height - previous_height) / (point_distance - previous_distance); + if (go_backwards) { + steepness *= -1; + } + let color; + if (steepness > 0.15) { + color = "#ff0000"; + } else if (steepness > 0.8) { + color = "#ff8000"; + } else if (steepness > 0.03) { + color = "#ffff00"; + } else if (steepness > -0.03) { + color = "#00ff00"; + } else if (steepness > -0.08) { + color = "#00aa44"; + } else if (steepness > -0.015) { + color = "#0044aa"; + } else { + color = "#0000ff"; + } + g.setColor(color); + g.fillPoly([ + previous_x, + previous_y, + x, + y, + x, + widgets_height + graph_height, + previous_x, + widgets_height + graph_height, + ]); + if ( + current_position >= previous_distance && + current_position < point_distance + ) { + let current_height = + previous_height + + ((current_position - previous_distance) / + (point_distance - previous_distance)) * + (height - previous_height); + current_x = Math.round( + (current_position - display_start) / distance_per_pixel ); + if (go_backwards) { + current_x = graph_width - current_x; + } + current_y = + widgets_height + + graph_height - + Math.round((current_height - min_height) / height_per_pixel); + } } - // end_profiling("map"); - if (this.interests !== null) { - this.interests.display( - this.displayed_position.lon, - this.displayed_position.lat, - scale_factor, - this.adjusted_cos_direction, - this.adjusted_sin_direction - ); - } - if (this.position !== null) { - this.display_path(); - } - - this.display_direction(); - this.display_stats(); - Bangle.drawWidgets(); - displaying = false; + previous_distance = point_distance; + previous_height = height; + previous_x = x; + previous_y = y; + } } - display_stats() { - let now = new Date(); - let minutes = now.getMinutes().toString(); - if (minutes.length < 2) { - minutes = "0" + minutes; - } - let hours = now.getHours().toString(); - - // display the clock - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString(hours + ":" + minutes, 0, 24); - - let approximate_speed; - // display speed (avg and instant) - if (this.old_times.length > 0) { - let point_time = this.old_times[this.old_times.length - 1]; - let done_in = point_time - this.starting_time; - approximate_speed = Math.round( - (this.advanced_distance * 3.6) / done_in - ); - let approximate_instant_speed = Math.round(this.instant_speed * 3.6); - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .drawString( - "" + - approximate_speed + - "km/h", - 0, - g.getHeight() - 15 - ); - - g.setFont("6x8:3") - .setFontAlign(1, -1, 0) - .drawString( - "" + approximate_instant_speed, - g.getWidth(), - g.getHeight() - 22 - ); - } - - if (this.path === null || this.position === null) { - return; - } - - let remaining_distance = this.remaining_distance(); - let rounded_distance = Math.round(remaining_distance / 100) / 10; - let total = Math.round(this.remaining_distances[0] / 100) / 10; - // now, distance to next point in meters - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString( - "" + this.distance_to_next_point + "m", - 0, - g.getHeight() - 49 - ); + if (this.on_path) { + g.setColor(0, 0, 0); + } else { + g.setColor(1, 0, 1); + } + g.fillCircle(current_x, current_y, 5); + + // display min dist/max dist and min height/max height + g.setColor(g.theme.fg); + g.setFont("6x8:2"); + g.setFontAlign(-1, 1, 0).drawString( + Math.ceil(display_start / 100) / 10, + 0, + g.getHeight() + ); - let forward_eta = compute_eta( - now.getHours(), - now.getMinutes(), - approximate_speed, - remaining_distance / 1000 - ); + g.setFontAlign(1, 1, 0).drawString( + Math.ceil((display_start + displayed_length) / 100) / 10, + g.getWidth(), + g.getHeight() + ); - // now display ETA - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString(forward_eta, 0, 42); - - // display distance on path - g.setFont("6x8:2").drawString( - "" + rounded_distance + "/" + total, - 0, - g.getHeight() - 32 - ); + g.setFontAlign(1, 1, 0).drawString( + min_height, + g.getWidth(), + widgets_height + graph_height + ); + g.setFontAlign(1, -1, 0).drawString( + max_height, + g.getWidth(), + widgets_height + ); + } + display_map() { + let scale_factor = this.scale_factor; + if (!zoomed) { + scale_factor /= 2; + } - // display various indicators - if (this.distance_to_next_point <= 100) { - if (this.path.is_waypoint(this.reaching)) { - g.setColor(0.0, 1.0, 0.0) - .setFont("6x15") - .drawString("turn", g.getWidth() - 50, 30); - } - } - if (!this.on_path) { - g.setColor(1.0, 0.0, 0.0) - .setFont("6x15") - .drawString("lost", g.getWidth() - 55, 35); - } + // start_profiling(); + for (let i = 0; i < this.maps.length; i++) { + this.maps[i].display( + this.displayed_position.lon, + this.displayed_position.lat, + scale_factor, + this.adjusted_cos_direction, + this.adjusted_sin_direction + ); + } + // end_profiling("map"); + if (this.interests !== null) { + this.interests.display( + this.displayed_position.lon, + this.displayed_position.lat, + scale_factor, + this.adjusted_cos_direction, + this.adjusted_sin_direction + ); + } + if (this.position !== null) { + this.display_path(); } - display_path() { - // don't display all segments, only those neighbouring current segment - // this is most likely to be the correct display - // while lowering the cost a lot - // - // note that all code is inlined here to speed things up - let cos = this.adjusted_cos_direction; - let sin = this.adjusted_sin_direction; - let displayed_x = this.displayed_position.lon; - let displayed_y = this.displayed_position.lat; - let width = g.getWidth(); - let height = g.getHeight(); - let half_width = width / 2; - let half_height = height / 2 + Y_OFFSET; - let scale_factor = this.scale_factor; - if (!zoomed) { - scale_factor /= 2; - } - if (this.path !== null) { - // compute coordinate for projection on path - let tx = (this.projected_point.lon - displayed_x) * scale_factor; - let ty = (this.projected_point.lat - displayed_y) * scale_factor; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let projected_x = half_width - Math.round(rotated_x); // x is inverted - let projected_y = half_height + Math.round(rotated_y); - - // display direction to next point if lost - if (!this.on_path) { - let next_point = this.path.point(this.current_segment + 1); - let previous_point = this.path.point(this.current_segment); - let nearest_point; - if ( - previous_point.fake_distance(this.position) < - next_point.fake_distance(this.position) - ) { - nearest_point = previous_point; - } else { - nearest_point = next_point; - } - let tx = (nearest_point.lon - displayed_x) * scale_factor; - let ty = (nearest_point.lat - displayed_y) * scale_factor; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let x = half_width - Math.round(rotated_x); // x is inverted - let y = half_height + Math.round(rotated_y); - g.setColor(1, 0, 1).drawLine(half_width, half_height, x, y); - } + this.display_direction(); + this.display_stats(); + } + display_stats() { + let now = new Date(); + let minutes = now.getMinutes().toString(); + if (minutes.length < 2) { + minutes = "0" + minutes; + } + let hours = now.getHours().toString(); - // display current-segment's projection - g.setColor(0, 0, 0); - g.fillCircle(projected_x, projected_y, 4); - } + // display the clock + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(hours + ":" + minutes, 0, 24); + + let approximate_speed; + // display speed (avg and instant) + if (this.old_times.length > 0) { + let point_time = this.old_times[this.old_times.length - 1]; + let done_in = point_time - this.starting_time; + approximate_speed = Math.round((this.advanced_distance * 3.6) / done_in); + let approximate_instant_speed = Math.round(this.instant_speed * 3.6); + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .drawString("" + approximate_speed + "km/h", 0, g.getHeight() - 15); + + g.setFont("6x8:3") + .setFontAlign(1, -1, 0) + .drawString( + "" + approximate_instant_speed, + g.getWidth(), + g.getHeight() - 22 + ); + } - // now display ourselves - g.setColor(0, 0, 0); - g.fillCircle(half_width, half_height, 5); + if (this.path === null || this.position === null) { + return; } -} -function load_gps(filename) { - // let's display splash screen while loading file + let remaining_distance = this.remaining_distance(); + let rounded_distance = Math.round(remaining_distance / 100) / 10; + let total = Math.round(this.remaining_distances[0] / 100) / 10; + // now, distance to next point in meters + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString( + "" + this.distance_to_next_point + "m", + 0, + g.getHeight() - 49 + ); + + let forward_eta = compute_eta( + now.getHours(), + now.getMinutes(), + approximate_speed, + remaining_distance / 1000 + ); - let splashscreen = require("heatshrink").decompress( - atob( - "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" - ) + // now display ETA + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(forward_eta, 0, 42); + + // display distance on path + g.setFont("6x8:2").drawString( + "" + rounded_distance + "/" + total, + 0, + g.getHeight() - 32 ); - g.clear(); + // display various indicators + if (this.distance_to_next_point <= 100) { + if (this.path.is_waypoint(this.reaching)) { + g.setColor(0.0, 1.0, 0.0) + .setFont("6x15") + .drawString("turn", g.getWidth() - 50, 30); + } + } + } + display_path() { + // don't display all segments, only those neighbouring current segment + // this is most likely to be the correct display + // while lowering the cost a lot + // + // note that all code is inlined here to speed things up + let cos = this.adjusted_cos_direction; + let sin = this.adjusted_sin_direction; + let displayed_x = this.displayed_position.lon; + let displayed_y = this.displayed_position.lat; + let width = g.getWidth(); + let height = g.getHeight(); + let half_width = width / 2; + let half_height = height / 2 + Y_OFFSET; + let scale_factor = this.scale_factor; + if (!zoomed) { + scale_factor /= 2; + } - g.drawImage(splashscreen, 0, 0); - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(0xf800) - .drawString(filename, 0, g.getHeight() - 30); - g.flip(); - - let buffer = s.readArrayBuffer(filename); - let file_size = buffer.length; - let offset = 0; - - let path = null; - let maps = []; - let interests = null; - while (offset < file_size) { - let block_type = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - if (block_type == 0) { - // it's a map - console.log("loading map"); - let res = new Map(buffer, offset, filename); - let map = res[0]; - offset = res[1]; - maps.push(map); - } else if (block_type == 2) { - console.log("loading path"); - let res = new Path(buffer, offset); - path = res[0]; - offset = res[1]; - } else if (block_type == 3) { - console.log("loading interests"); - let res = new Interests(buffer, offset); - interests = res[0]; - offset = res[1]; + if (this.path !== null) { + // compute coordinate for projection on path + let tx = (this.projected_point.lon - displayed_x) * scale_factor; + let ty = (this.projected_point.lat - displayed_y) * scale_factor; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let projected_x = half_width - Math.round(rotated_x); // x is inverted + let projected_y = half_height + Math.round(rotated_y); + + // display direction to next point if lost + if (!this.on_path) { + let next_point = this.path.point(this.current_segment + 1); + let previous_point = this.path.point(this.current_segment); + let nearest_point; + if ( + previous_point.fake_distance(this.position) < + next_point.fake_distance(this.position) + ) { + nearest_point = previous_point; } else { - console.log("todo : block type", block_type); + nearest_point = next_point; } + let tx = (nearest_point.lon - displayed_x) * scale_factor; + let ty = (nearest_point.lat - displayed_y) * scale_factor; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let x = half_width - Math.round(rotated_x); // x is inverted + let y = half_height + Math.round(rotated_y); + g.setColor(1, 0, 1).drawLine(half_width, half_height, x, y); + } + + // display current-segment's projection + g.setColor(0, 0, 0); + g.fillCircle(projected_x, projected_y, 4); } - // checksum file size - if (offset != file_size) { - console.log("invalid file size", file_size, "expected", offset); - let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset; - E.showAlert(msg).then(function() { - E.showAlert(); - start_gipy(path, maps, interests); - }); + // now display ourselves + if (this.on_path) { + g.setColor(0, 0, 0); } else { - start_gipy(path, maps, interests); + g.setColor(1, 0, 1); } + g.fillCircle(half_width, half_height, 5); + } } -class Path { - constructor(buffer, offset) { - // let p = Uint16Array(buffer, offset, 1); - // console.log(p); - let points_number = Uint16Array(buffer, offset, 1)[0]; - offset += 2; - - // path points - this.points = Float64Array(buffer, offset, points_number * 2); - offset += 8 * points_number * 2; - - // path waypoints - let waypoints_len = Math.ceil(points_number / 8.0); - this.waypoints = Uint8Array(buffer, offset, waypoints_len); - offset += waypoints_len; - - return [this, offset]; - } - - is_waypoint(point_index) { - let i = Math.floor(point_index / 8); - let subindex = point_index % 8; - let r = this.waypoints[i] & (1 << subindex); - return r != 0; - } - - // return point at given index - point(index) { - let lon = this.points[2 * index]; - let lat = this.points[2 * index + 1]; - return new Point(lon, lat); - } - - // return index of segment which is nearest from point. - // we need a direction because we need there is an ambiguity - // for overlapping segments which are taken once to go and once to come back. - // (in the other direction). - nearest_segment(point, start, end, cos_direction, sin_direction) { - // we are going to compute two min distances, one for each direction. - let indices = [0, 0]; - let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; - - let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]); - for (let i = start + 1; i < end + 1; i++) { - let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]); - - let closest_point = point.closest_segment_point(p1, p2); - let distance = point.length_squared(closest_point); - - let dot = - cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat); - let orientation = +(dot < 0); // index 0 is good orientation - if (distance <= mins[orientation]) { - mins[orientation] = distance; - indices[orientation] = i - 1; - } - - p1 = p2; - } +function load_gps(filename) { + // let's display splash screen while loading file + + let splashscreen = require("heatshrink").decompress( + atob( + "2Gwgdly1ZATttAQfZARm2AQXbAREsyXJARmyAQXLAViDgARm2AQVbAR0kyVJAQ2yAQVLARZfBAQSD/ARXZAQVtARnbAQe27aAE5ICClgCMLgICCQEQCCkqDnARb+BAQW2AQyDEARdLAQeyAR3LAQSDXL51v+x9bfAICC7ICM23ZPpD4BAQXJn//7IFCAQ2yAQR6YQZOSQZpBBsiDZARm2AQVbAQSDIAQt///btufTAOyBYL+DARJrBAQSDWLJvvQYNlz/7tiAeEYICBtoCHQZ/+7ds//7tu2pMsyXJlmOnAFDyRoBAQSAWAQUlyVZAQxcBAQX//3ZsjIBWYUtBYN8uPHjqMeAQVbQZ/2QYXbQYNbQwRNBnHjyVLkhNBARvLAQSDLIgNJKZf/+1ZsjIBlmzQwXPjlwg8cux9YtoCD7ICCQZ192yDBIINt2f7tuSvED/0AgeOhMsyXJAQeyAQR6MARElyT+BAQ9lIIL+CsqDF21Ajlx4EAuPBQa4CIQZ0EQYNnAQNt2QCByU48f+nEAh05kuyC4L+DARJ3BAQSDJsmWpICEfwJQEkESoNl2wXByaDB2PAQYPHgEB4cgEYKDc7KDOkmAgMkyCABy3bsuegHjx/4QYM4sk27d/+XJlmSAQpcBAQSAKAQQ1BZAVZkoCHBYNIgEApMgEwcHQYUcgPHEYVv+SDaGQSDNAQZDByUbDQM48eOn/ggCDB23bIIICB/1LC4ICB2QCLPoICEfwNJARA1BAQZEDgEJkkyQAKDB/gCBQYUt+ACB/yDsAQVA8ESrKDC//+nIjB7dt/0bQYNJlmS5ICG2QCCcwQCGGQslAQdZAQ4RDQAPJQYUf//DGQKAB31LQYKeCQbmT//8QZlIQAM4QYkZQYe+raDCC4eyAQVLARaDBAoL4CAQNkz///4FCAQxWCp8AQAKDCjlwU4OCQYcv3yDfIAP/+SDM8EOQYOPCgOAhFl2CDB20bQwIUCfwICMLgICC2XLGQsnIISnDKAVZkoCDpKADAQUSoARBhcs2/Dlm2QbEEiFJggvBeAIAC5KDKpKDF8AIBgEAhMkw3LQYgCIfYICC2QCHCgl/IIf5smWpICIniDELgQdBoEAgVJkqDboMkiVBIAYABQZcjxyDB//4Bw2QRAIIEfAICC5ICM2XJkGSUgIXBIIvkEwklAQdZkiDD4IOBrILDC4UAQbYCBo5BF/iDKkiDB//+LgYCY2QCCpYCCkGCpEkwVPIIv/fwMkAQNkAQuRQYNwBAVZAQRoCRgSDcv5BG+RlLvHjQDHJAQUsAQ6DBhACBn5BG/wpOrMlARZuBAQSDRgEQgMAiJAGAAPJgmQpMEfbQCSpaDDx5BJCgVkAQWWARhoBAQR9SQY0AoEEv5BI/MkiVBPs0sAQfJAQUAQYQ5Bj4CB/hHEExz+BAQT+BARVlAQSDPAAKDJ/8EiFBAQeQQ0gCFkECgEj//HQYUcuPHIIXkwQaHfYICCsgCMrICCQByDFHwQAI/iDFiVBkkSQc3JIIfx46ACAQ1yhEgyUJAQImOrICCkoCLPQICCQZCCKAAXBQYYCFyFJgiGiIIX8QBACD4EgwVIkmCDo1kAQWWARh0BAQR9GQY8H8aDM/CDJiVBkkSQccHQBQCDgGChCGBAQOShImLfYICFfwICKsoCCQYcAQRn+n/8iEBgCGIAQWQQbtPQaMcuSDEwVIkmCEw77BAQVkARlZAQSACAQN/IIM/8f+nCCI8f//H/x0AgkAoCDJiVBkkSQbOT/8AgKANAQiDEAQsJkA1PrICCkoCIz5BBhyDBxyDJAAYOB/iZBAAMBgCGIAQdJgiDUFwKDUjkCQZEIkmCpApCsgCFywCLv9lAoNl//HQYk/P5Hjx4GE+CEDgkAoCDKoMkiQCBPpeT//8AoMnQYSARAQVwH4OAQxMgyUJAQQ7IfwICCrMlz48B+VZngsBgeP/CAIAAaDB8YGD/CEDAAMDMQUQgKJJyFJAQRKGEYK8BhIqCQCQCEgECgEggUIEAX8QwkkwVIHAz7BAQVkAQN/+KqCg4pCOIKDN/0/QwQADwCCCBYIRDoEEgCDHAQMkiQCBJQiABnHggE4VoSDXAQPAgEPKoyDCAQkJkCGFAQdPEYcBFIaAMABsDBA/8gEBgEQgKGIAQNJgmSnCDDhwFDQbICBv5MI5CGFkmCpCACsgCCyImJfAYAOCIPjBA4TI8kAoCDKoMnPQJ9CgeAAQKDdAQMfHgXxBYl+QYYCEhMgyUJngRBgAAHf6R6Cx4FCnALDxyGC/BuCAQVAFoUQgKDEoARF8EOgACBiSDdjlwg4LIpMkhSGHo8cQJEkyRuDABxcBQwaDBMoIFCEYMONwY+BnFL12SoEgoEEgCDCCIfjwE4gYCBhMk2SDeuPAIQKGDFIOSIgICCyCDDwPAQY8SCgXjQaL4FAowAB+EAgYIB9cu3Xrlmy5JECGwIOCDQYCC0gOBCgKAbuB9DAQUAgPHQAgCEkUHP4wABTAplDABaSDPogCDEgMOQwX6r/+QYJrB5csySDCpaAIx06pYUEQbUAAQQABBAPSpF145uFAQOXjkB4ACCC4VIgCVGQYf+n7+FAgYLFMonghyrEh0SpeuyVIkmypEgF4MuQBE49IRB9euQYWyQbUcdw0HNYoCCpFwg8AAQYVDSo6DDKAKDLnAFF8EAfYOAgHj1gjBRIPjlxrDGQOQQBACBnVLl269esQbhrBhMh4BoEw8dNwslDQvAjkBAQKAHQYn4QZHjx4EBL4IJCMokA9ck3ED1xoBlmS8LyB5MgRgSAIAQOkPoIaD2VLlmCQbF0L4ZrLrgUBgCYBAQYABTYgCGPQwAELgX//xfBAQRlCxmS9euyTsCdISABAQKPBQBOOnVJCgKDCC4cgQbEAMpQCDkoaHgPAjkEDRj4C8aGCQY4CGwm48EEMoOscwQFBAQNIkApBhyAInCABTwSbB1waCAoMk2SDVuj1BAQJoLrgXFuEHgFwgUJTxpWDfASADn5iFgYCBgEO2XpLgPL0mSMQOSF4UIkmQTxOOiCYCQYIdBAQUuQYILBPprjBAoMAAQUAMplJkojKuAaNQYoCCQY47BnHgeQPggG69aDENwOChEgwUJCIKDKTAKDCAQKDC5Ms3XIkCDFPQYCE4VcIQIABi8cMptIU5UADRqDHgHj/xiG9JBDiXj0hlB1hrB0mCEAKABkmQDQihDAQQyCPQOyTYIdB1iGBBANIAQMcgLaCgBiIKwtdMpmHDpApBQB4CCeoXhh0QQY+Q9ek3Xr1z+BcYLsDQYKABEYIgBDQYgE9eOiQXCAQI4DQwIIBkmyhYLBgBZBjpZBL4clMQhlQpCAIAQMJQacAgiDBl26L4M6fYO4AoJ3BxgCB126pekL4fJkGChEgyT+FAQvpF4PJOgKDBwR6BUgYCCBwOygB6BVQR9BgVckmXjkAMSIUBQZPSQCKDDl04eoKDDoeu3DmBfYRZBSQLpCQYIdBQYJcBPomP/AFDwm4fYXJkmCpACBHAOy5CPCBAMJCIMJkPCI4VcuESeQcBMqCAJAQNwQCQCCheunT4CoeAiXr1m69MAmSDDcAlLL4MIkGSpb+E8f+AoihBVoXLCgL7C9csDodJAoMLQYZ3DrkAKAkgRIYCLQBICCuiDWPQKDCcYL4BBAaJCBAMsLgWShKDCkmQPQgCG8L7B5aDDAoaDBTwKJC1ytDI4tIL4qPEARMlQBVxDRoCKbQXol2y9JxBpaDBKASJB2TmBQAkgwVJhx9Ex/4QYkQDoVLF4IjFQAXIkizCFgSDGASlcQBICBuAmYpcuJQICCcYRZBL4YIB5MgQYKABQYOSfwvj/wFD8MAPoIgEhICB5L4FQYQRBRIKDaw6AJAQMBVTLRCJQSDCAoTpDPoKDCQAOCDQKAEAQ8LlhxCyRxChCnCliPB1wOBEYI7C5ACBQbCAKjdtwCqZQYZTDAoSDBBYtJLgKDBC4J9F//4AoXbtuwpcuOgIdBfYL4DEwOS9aDBFIOC5ckAQMuQbCAIAQPG7VtmiDbkGy5IFB5KGDAQYIChKDCkm4fwv/Aoc27dp01L0gmCwXr1gjDDoIFB1ytBBwIRCBARZVkqAIAQX2YoMwQbbdB5L1BhJZBboR9BAoSABQYNJhyADAQ2P2xBBw9LPoNIC4KDBOIIvB5B6CAoICBEwIFB9aDWriAJAQRBCnCDgbQJQCwUJlzdCBYWQPov//yDFYoXHof8EwRxBFgJ3CEYOC5KwBQYVLl26SoZWSw6AKAQMB/5KCjsEQbICBLgO65JWBhJWBpbUEd4J6Ex0//6JEoel4BCB48IDoPrkiGBAQa2CWASDBBAQvBSoZWRQBYCBpMF/8DI4NAQCyDEwT4BZwJTBBYJQBl2ShIOBhZ6EfwP/RIk68eBQQKDBgKDCeoPIFgYpBBYIFCQYXLQAPr1iDSQBYCB6VIurFB/04pf0QbFJkGChMsQYOucwRTCBwW4PQgCB//4BAkQYoUcv/CpMMEAOu3QgBwVIF4QpCAoPJAoICB2SGCKB8lQBaDDKYOS/+kWwaDZJQLOCcYLRByVLcAUOQAmPQAoCCEAME3UJZANBDQPJlxxD5AvBQZFIQadIQBgCBF4NIkrCBkkSQDCDE5ZKB9YCBRIJcBLIMDPQv/QY+uPQMEiVBgmyhBrCAQIpBU4R0DPQOCBwY7BBwIIBKBqAMkoCBCgeQpApBQb5oBAQSDBhEg3B6F//+QAmEyCDBTYWyfAL+BFIQgBF4SDCQAIFE126QYQUBQZp0CQZd0y4UCpB9aAQihCKYSJCFIOChEuPQmOn//RIiDB3VJlz+CTYRxBJRCDF1g1B1myRIOCTwKDMpCALQYYUEQcACBdISDBwSMBwVDPQuP/6JEQYfrdgIjC5CDD2QFBF4Wy5ICDQYOu2XrQYKPBQYI1BJpaAMAQVwQchWCAoZKBdgO4PQwCJPQMu3RxCPoyqB5YCCFgeyQYKeBBYNIQZ0lQBoCCuiDkLIRlCJQUIhyAOnHpDoRuBfAZoCQAosEpAUBBAKDB1iDBBYNLkiDJpCAOAQMJPr4CFJoLXCyUIMoMDQBoCB3FL1gdBNwPrEYSGCQAQFDBYaDDAoKPCQYcsQZKAOjskw6AjAQREBQYuAPQ3//AIFoeu3VLAQSDCRIQmB9ekFgSDBGQe6PQKABGQIOCAQQ+DJQ2HQZvXQEwCDIgMJkGCQYL+G//+BAs6QAL1C3TvDQYJoCRIOCpYsBhYIBpEuCga2BfwdLBYUsRIRHEkKALAQXCrqDuhaAEAQM//4IGQYW6QYKABQYQFBQYXLSQMLkgmBBAMIO4UgGoICCQYQjBQZFcQBgCDQE4CBhJWCQYJ3EAQOP/4IGAQKbBL4RlBeQQCCQYR6B9esR4fIBANLQAeCDQOShaDJy6AOQY+CMQaDgAQKDB3CDQiXJO4PJEARiBQwQICNYKDDpYOBC4IRDBAIRCQYYaBQYklQB6DFpCDBQAazDATcIEwICBfY3j//4QY86MQSDDfwREDwXLNYPrPoQUBQASPD1wLDQZMhQaEgwCDEMoiDfpBfBhMOQY3//yMHeQIdDdgZuBPQILBwRrCQwQCB3SDCpcuBAJ9BDQKGCAQJEFQBwCBjt0PRkJQbkIQYMDfYwCJ8JcBcAaDBQARrCQYYICQYnrTwPLQYKGBTYYaCCIOCIgSAOQYbdDQdSAO8eunFBPoKDByTmBQYOkRgIFBEwSDC5MgBYR6B1x3BAQQIBQAXIEASDDy6DPkmHpAXDTwZlGQb24QZ+kyFLOgSDD2RiBPoYmCKYL1DBYSACpcufwQCBSQKDD1hoCw6DPkvXLgiDpPQ3//yDIdgJcBfwVL0h3CyRuCFIiDDAQSYCUIJ9BCIMLQYwaBkqANAQV16S2EMQqJDBY6DWlx6Fn//QAoCCwkyQYJ3BlxfB0iACQZCVDfwYFBpJ9CBwMJRIQRC1gdBQBwCCuAvDO4cgQYgFBQbsLO4uP/6AGAQPhhxWBQYe6QAXJEw4LDOIRNBQYXIQYMIQYYIBBYNLFINIQaEJQYIdCHAaDCAQqDcgZ6F/6DJpYyCLgPrkm6EAiMBQY5TGfwSDB5AOEboaDBQByDDkESQYogCEYYCfO4qCB/CDI8ckiVLC4KDBPoQCBMQPr0gLB1jvCFgcIkGCKYOy5YLBQYQUCQa3CQASDIQECDHn///yAHx069ZWBOIXL1zyDBYO65esAoICBhIUBNwKDCQAKDEDQYgDQbB6jQZ6AGQYfBQYZoBl265JuCkm6PQQFBwUIBYPJBAKJC5MgBwKDCRgKDBSoWCCISDQ6VBL5AsBAoVIQceP/6DKiR6CO4QaBQYQjGQYRHBPoILDQYWCRgVIQYNL126RgOyeQOCQZ50EC4OSWwImCQwaDkQQKAHAQOEEaR9BQYTRGKwOCpaDBhCDBR4SDCBwSDPuAmCwSDCAQQ1DQwSDiQQKDKx0SFjSDFBASDCcwQRDBwIA=" + ) + ); + + g.clear(); + + g.drawImage(splashscreen, 0, 0); + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(0xf800) + .drawString(filename, 0, g.getHeight() - 30); + g.flip(); + + let buffer = s.readArrayBuffer(filename); + let file_size = buffer.length; + let offset = 0; + + let path = null; + let heights = null; + let maps = []; + let interests = null; + while (offset < file_size) { + let block_type = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + if (block_type == 0) { + // it's a map + console.log("loading map"); + let res = new Map(buffer, offset, filename); + let map = res[0]; + offset = res[1]; + maps.push(map); + } else if (block_type == 2) { + console.log("loading path"); + let res = new Path(buffer, offset); + path = res[0]; + offset = res[1]; + } else if (block_type == 3) { + console.log("loading interests"); + let res = new Interests(buffer, offset); + interests = res[0]; + offset = res[1]; + } else if (block_type == 4) { + console.log("loading heights"); + let heights_number = path.points.length / 2; + heights = Int16Array(buffer, offset, heights_number); + offset += 2 * heights_number; + } else { + console.log("todo : block type", block_type); + } + } + + // checksum file size + if (offset != file_size) { + console.log("invalid file size", file_size, "expected", offset); + let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset; + E.showAlert(msg).then(function () { + E.showAlert(); + start_gipy(path, maps, interests, heights); + }); + } else { + start_gipy(path, maps, interests, heights); + } +} - // by default correct orientation (0) wins - // but if other one is really closer, return other one - if (mins[1] < mins[0] / 100.0) { - return [1, indices[1]]; - } else { - return [0, indices[0]]; - } +class Path { + constructor(buffer, offset) { + // let p = Uint16Array(buffer, offset, 1); + // console.log(p); + let points_number = Uint16Array(buffer, offset, 1)[0]; + offset += 2; + + // path points + this.points = Float64Array(buffer, offset, points_number * 2); + offset += 8 * points_number * 2; + + // path waypoints + let waypoints_len = Math.ceil(points_number / 8.0); + this.waypoints = Uint8Array(buffer, offset, waypoints_len); + offset += waypoints_len; + + return [this, offset]; + } + + is_waypoint(point_index) { + let i = Math.floor(point_index / 8); + let subindex = point_index % 8; + let r = this.waypoints[i] & (1 << subindex); + return r != 0; + } + + // return point at given index + point(index) { + let lon = this.points[2 * index]; + let lat = this.points[2 * index + 1]; + return new Point(lon, lat); + } + + // return index of segment which is nearest from point. + // we need a direction because we need there is an ambiguity + // for overlapping segments which are taken once to go and once to come back. + // (in the other direction). + nearest_segment(point, start, end, cos_direction, sin_direction) { + // we are going to compute two min distances, one for each direction. + let indices = [0, 0]; + let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; + + let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]); + for (let i = start + 1; i < end + 1; i++) { + let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]); + + let closest_point = point.closest_segment_point(p1, p2); + let distance = point.length_squared(closest_point); + + let dot = + cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat); + let orientation = +(dot < 0); // index 0 is good orientation + if (distance <= mins[orientation]) { + mins[orientation] = distance; + indices[orientation] = i - 1; + } + + p1 = p2; } - get len() { - return this.points.length / 2; + + // by default correct orientation (0) wins + // but if other one is really closer, return other one + if (mins[1] < mins[0] / 100.0) { + return [1, indices[1]]; + } else { + return [0, indices[0]]; } + } + get len() { + return this.points.length / 2; + } } class Point { - constructor(lon, lat) { - this.lon = lon; - this.lat = lat; - } - coordinates(current_position, cos_direction, sin_direction, scale_factor) { - let translated = this.minus(current_position).times(scale_factor); - let rotated_x = - translated.lon * cos_direction - translated.lat * sin_direction; - let rotated_y = - translated.lon * sin_direction + translated.lat * cos_direction; - return [ - g.getWidth() / 2 - Math.round(rotated_x), // x is inverted - g.getHeight() / 2 + Math.round(rotated_y) + Y_OFFSET, - ]; - } - minus(other_point) { - let xdiff = this.lon - other_point.lon; - let ydiff = this.lat - other_point.lat; - return new Point(xdiff, ydiff); - } - plus(other_point) { - return new Point(this.lon + other_point.lon, this.lat + other_point.lat); - } - length_squared(other_point) { - let londiff = this.lon - other_point.lon; - let latdiff = this.lat - other_point.lat; - return londiff * londiff + latdiff * latdiff; - } - times(scalar) { - return new Point(this.lon * scalar, this.lat * scalar); - } - // dot(other_point) { - // return this.lon * other_point.lon + this.lat * other_point.lat; - // } - distance(other_point) { - //see https://www.movable-type.co.uk/scripts/latlong.html - const R = 6371e3; // metres - const phi1 = (this.lat * Math.PI) / 180; - const phi2 = (other_point.lat * Math.PI) / 180; - const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180; - const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180; - - const a = - Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + - Math.cos(phi1) * - Math.cos(phi2) * - Math.sin(deltalambda / 2) * - Math.sin(deltalambda / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return R * c; // in meters - } - fake_distance(other_point) { - return Math.sqrt(this.length_squared(other_point)); - } - // return closest point from 'this' on [v,w] segment. - // since this function is critical we inline all code here. - closest_segment_point(v, w) { - // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment - // Return minimum distance between line segment vw and point p - let segment_londiff = w.lon - v.lon; - let segment_latdiff = w.lat - v.lat; - let l2 = - segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt - if (l2 == 0.0) { - return v; // v == w case - } - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. - - // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below - let start_londiff = this.lon - v.lon; - let start_latdiff = this.lat - v.lat; - let t = - (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2; - if (t < 0) { - t = 0; - } else { - if (t > 1) { - t = 1; - } - } - let lon = v.lon + segment_londiff * t; - let lat = v.lat + segment_latdiff * t; - return new Point(lon, lat); + constructor(lon, lat) { + this.lon = lon; + this.lat = lat; + } + coordinates(current_position, cos_direction, sin_direction, scale_factor) { + let translated = this.minus(current_position).times(scale_factor); + let rotated_x = + translated.lon * cos_direction - translated.lat * sin_direction; + let rotated_y = + translated.lon * sin_direction + translated.lat * cos_direction; + return [ + g.getWidth() / 2 - Math.round(rotated_x), // x is inverted + g.getHeight() / 2 + Math.round(rotated_y) + Y_OFFSET, + ]; + } + minus(other_point) { + let xdiff = this.lon - other_point.lon; + let ydiff = this.lat - other_point.lat; + return new Point(xdiff, ydiff); + } + plus(other_point) { + return new Point(this.lon + other_point.lon, this.lat + other_point.lat); + } + length_squared(other_point) { + let londiff = this.lon - other_point.lon; + let latdiff = this.lat - other_point.lat; + return londiff * londiff + latdiff * latdiff; + } + times(scalar) { + return new Point(this.lon * scalar, this.lat * scalar); + } + // dot(other_point) { + // return this.lon * other_point.lon + this.lat * other_point.lat; + // } + distance(other_point) { + //see https://www.movable-type.co.uk/scripts/latlong.html + const R = 6371e3; // metres + const phi1 = (this.lat * Math.PI) / 180; + const phi2 = (other_point.lat * Math.PI) / 180; + const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180; + const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180; + + const a = + Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + + Math.cos(phi1) * + Math.cos(phi2) * + Math.sin(deltalambda / 2) * + Math.sin(deltalambda / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return R * c; // in meters + } + fake_distance(other_point) { + return Math.sqrt(this.length_squared(other_point)); + } + // return closest point from 'this' on [v,w] segment. + // since this function is critical we inline all code here. + closest_segment_point(v, w) { + // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment + // Return minimum distance between line segment vw and point p + let segment_londiff = w.lon - v.lon; + let segment_latdiff = w.lat - v.lat; + let l2 = + segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt + if (l2 == 0.0) { + return v; // v == w case + } + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. + + // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below + let start_londiff = this.lon - v.lon; + let start_latdiff = this.lat - v.lat; + let t = + (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2; + if (t < 0) { + t = 0; + } else { + if (t > 1) { + t = 1; + } } + let lon = v.lon + segment_londiff * t; + let lat = v.lat + segment_latdiff * t; + return new Point(lon, lat); + } } let fake_gps_point = 0; - function drawMenu() { - const menu = { - "": { - title: "choose trace" - }, - }; - var files = s.list(".gps"); - for (var i = 0; i < files.length; ++i) { - menu[files[i]] = start.bind(null, files[i]); - } - menu["Exit"] = function() { - load(); - }; - E.showMenu(menu); + const menu = { + "": { + title: "choose trace", + }, + }; + var files = s.list(".gps"); + for (var i = 0; i < files.length; ++i) { + menu[files[i]] = start.bind(null, files[i]); + } + menu["Exit"] = function () { + load(); + }; + E.showMenu(menu); } function start(fn) { - E.showMenu(); - console.log("loading", fn); + E.showMenu(); + console.log("loading", fn); - load_gps(fn); + load_gps(fn); } -function start_gipy(path, maps, interests) { - console.log("starting"); +function start_gipy(path, maps, interests, heights) { + console.log("starting"); - if (!simulated && settings.disable_bluetooth) { - NRF.sleep(); // disable bluetooth completely - } + if (!simulated && settings.disable_bluetooth) { + NRF.sleep(); // disable bluetooth completely + } - status = new Status(path, maps, interests); + status = new Status(path, maps, interests, heights); - setWatch( - function() { - status.activate(); - if (in_menu) { - return; - } - in_menu = true; - const menu = { - "": { - title: "choose action" - }, - "Go Backward": { - value: go_backwards, - format: (v) => (v ? "On" : "Off"), - onchange: (v) => { - go_backwards = v; - }, - }, - Zoom: { - value: zoomed, - format: (v) => (v ? "In" : "Out"), - onchange: (v) => { - status.invalidate_caches(); - zoomed = v; - }, - }, - /*LANG*/ - "powersaving": { - value: powersaving, - onchange: (v) => { - powersaving = v; - } - }, - "back to map": function() { - in_menu = false; - E.showMenu(); - g.clear(); - g.flip(); - if (status !== null) { - status.display(); - } - }, - }; - E.showMenu(menu); + setWatch( + function () { + status.activate(); + if (in_menu) { + return; + } + in_menu = true; + const menu = { + "": { + title: "choose action", }, - BTN1, { - repeat: true - } + "Go Backward": { + value: go_backwards, + format: (v) => (v ? "On" : "Off"), + onchange: (v) => { + go_backwards = v; + }, + }, + Zoom: { + value: zoomed, + format: (v) => (v ? "In" : "Out"), + onchange: (v) => { + status.invalidate_caches(); + zoomed = v; + }, + }, + /*LANG*/ + powersaving: { + value: powersaving, + onchange: (v) => { + powersaving = v; + }, + }, + "back to map": function () { + in_menu = false; + E.showMenu(); + g.clear(); + g.flip(); + if (status !== null) { + status.display(); + } + }, + }; + E.showMenu(menu); + }, + BTN1, + { + repeat: true, + } + ); + + if (status.path !== null) { + let start = status.path.point(0); + status.displayed_position = start; + } else { + let first_map = maps[0]; + status.displayed_position = new Point( + first_map.start_coordinates[0] + + (first_map.side * first_map.grid_size[0]) / 2, + first_map.start_coordinates[1] + + (first_map.side * first_map.grid_size[1]) / 2 ); + } + status.display(); + Bangle.on("touch", () => { + status.activate(); + if (in_menu) { + return; + } + if (status.heights !== null) { + status.screen = (status.screen + 1) % 3; + status.display(); + } + }); - if (status.path !== null) { - let start = status.path.point(0); - status.displayed_position = start; - } else { - let first_map = maps[0]; - status.displayed_position = new Point( - first_map.start_coordinates[0] + - (first_map.side * first_map.grid_size[0]) / 2, - first_map.start_coordinates[1] + - (first_map.side * first_map.grid_size[1]) / 2); + Bangle.on("stroke", (o) => { + status.activate(); + if (in_menu) { + return; } + // we move display according to stroke + let first_x = o.xy[0]; + let first_y = o.xy[1]; + let last_x = o.xy[o.xy.length - 2]; + let last_y = o.xy[o.xy.length - 1]; + let xdiff = last_x - first_x; + let ydiff = last_y - first_y; + + let c = status.adjusted_cos_direction; + let s = status.adjusted_sin_direction; + let rotated_x = xdiff * c - ydiff * s; + let rotated_y = xdiff * s + ydiff * c; + status.displayed_position.lon += (1.3 * rotated_x) / status.scale_factor; + status.displayed_position.lat -= (1.3 * rotated_y) / status.scale_factor; status.display(); - - Bangle.on("stroke", (o) => { - status.activate(); - if (in_menu) { - return; + }); + + if (simulated) { + status.starting_time = getTime(); + // let's keep the screen on in simulations + Bangle.setLCDTimeout(0); + Bangle.setLCDPower(1); + Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen + + function simulate_gps(status) { + if (status.path === null) { + let map = status.maps[0]; + let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]); + let p2 = new Point( + map.start_coordinates[0] + map.side * map.grid_size[0], + map.start_coordinates[1] + map.side * map.grid_size[1] + ); + let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point)); + if (fake_gps_point < 1) { + fake_gps_point += 0.05; } - // we move display according to stroke - let first_x = o.xy[0]; - let first_y = o.xy[1]; - let last_x = o.xy[o.xy.length - 2]; - let last_y = o.xy[o.xy.length - 1]; - let xdiff = last_x - first_x; - let ydiff = last_y - first_y; - - let c = status.adjusted_cos_direction; - let s = status.adjusted_sin_direction; - let rotated_x = xdiff * c - ydiff * s; - let rotated_y = xdiff * s + ydiff * c; - status.displayed_position.lon += 1.3 * rotated_x / status.scale_factor; - status.displayed_position.lat -= 1.3 * rotated_y / status.scale_factor; - status.display(); - }); - - if (simulated) { - status.starting_time = getTime(); - // let's keep the screen on in simulations - Bangle.setLCDTimeout(0); - Bangle.setLCDPower(1); - Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen - - - function simulate_gps(status) { - if (status.path === null) { - let map = status.maps[0]; - let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]); - let p2 = new Point( - map.start_coordinates[0] + map.side * map.grid_size[0], - map.start_coordinates[1] + map.side * map.grid_size[1] - ); - let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point)); - if (fake_gps_point < 1) { - fake_gps_point += 0.05; - } - status.update_position(pos); - } else { - if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { - return; - } - let point_index = Math.floor(fake_gps_point); - if (point_index >= status.path.len / 2 - 1) { - return; - } - let p1 = status.path.point(2 * point_index); // use these to approximately follow path - let p2 = status.path.point(2 * (point_index + 1)); - //let p1 = status.path.point(point_index); // use these to strictly follow path - //let p2 = status.path.point(point_index + 1); - - let alpha = fake_gps_point - point_index; - let pos = p1.times(1 - alpha).plus(p2.times(alpha)); - - if (go_backwards) { - fake_gps_point -= 0.05; // advance simulation - } else { - fake_gps_point += 0.05; // advance simulation - } - status.update_position(pos); - } + status.update_position(pos); + } else { + if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { + return; } + let point_index = Math.floor(fake_gps_point); + if (point_index >= status.path.len / 2 - 1) { + return; + } + let p1 = status.path.point(2 * point_index); // use these to approximately follow path + let p2 = status.path.point(2 * (point_index + 1)); + //let p1 = status.path.point(point_index); // use these to strictly follow path + //let p2 = status.path.point(point_index + 1); - setInterval(simulate_gps, 500, status); - } else { - status.activate(); - - let frame = 0; - let set_coordinates = function(data) { - frame += 1; - // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere - let valid_coordinates = !isNaN(data.lat) && - !isNaN(data.lon) && - (data.lat != 0.0 || data.lon != 0.0); - if (valid_coordinates) { - if (status.starting_time === null) { - status.starting_time = getTime(); - Bangle.loadWidgets(); // load them even in simulation to eat mem - } - status.update_position(new Point(data.lon, data.lat)); - } - let gps_status_color; - if (frame % 2 == 0 || valid_coordinates) { - gps_status_color = g.theme.bg; - } else { - gps_status_color = g.theme.fg; - } - if (!in_menu) { - g.setColor(gps_status_color) - .setFont("6x8:2") - .drawString("gps", g.getWidth() - 40, 30); - } - }; + let alpha = fake_gps_point - point_index; + let pos = p1.times(1 - alpha).plus(p2.times(alpha)); - Bangle.setGPSPower(true, "gipy"); - Bangle.on("GPS", set_coordinates); + if (go_backwards) { + fake_gps_point -= 0.2; // advance simulation + } else { + fake_gps_point += 0.2; // advance simulation + } + status.update_position(pos); + } } + + setInterval(simulate_gps, 500, status); + } else { + status.activate(); + + let frame = 0; + let set_coordinates = function (data) { + frame += 1; + // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere + let valid_coordinates = + !isNaN(data.lat) && + !isNaN(data.lon) && + (data.lat != 0.0 || data.lon != 0.0); + if (valid_coordinates) { + if (status.starting_time === null) { + status.starting_time = getTime(); + Bangle.loadWidgets(); // load them even in simulation to eat mem + } + status.update_position(new Point(data.lon, data.lat)); + } + let gps_status_color; + if (frame % 2 == 0 || valid_coordinates) { + gps_status_color = g.theme.bg; + } else { + gps_status_color = g.theme.fg; + } + if (!in_menu) { + g.setColor(gps_status_color) + .setFont("6x8:2") + .drawString("gps", g.getWidth() - 40, 30); + } + }; + + Bangle.setGPSPower(true, "gipy"); + Bangle.on("GPS", set_coordinates); + } } let files = s.list(".gps"); if (files.length <= 1) { - if (files.length == 0) { - load(); - } else { - start(files[0]); - } + if (files.length == 0) { + load(); + } else { + start(files[0]); + } } else { - drawMenu(); -} \ No newline at end of file + drawMenu(); +} diff --git a/apps/gipy/heights.png b/apps/gipy/heights.png new file mode 100644 index 0000000000..07f82511b7 Binary files /dev/null and b/apps/gipy/heights.png differ diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 7dd4123f68..d6b5e1405d 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,13 +2,13 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.20", + "version": "0.21", "description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.", "allow_emulator":false, "icon": "gipy.png", "type": "app", "tags": "tool,outdoors,gps", - "screenshots": [{"url":"splash.png"}], + "screenshots": [{"url":"splash.png"}, {"url":"heights.png"}, {"url":"shot.png"}], "supports": ["BANGLEJS2"], "readme": "README.md", "interface": "interface.html", diff --git a/apps/gipy/pkg/gps.d.ts b/apps/gipy/pkg/gps.d.ts index c881052f4f..e4644f74fd 100644 --- a/apps/gipy/pkg/gps.d.ts +++ b/apps/gipy/pkg/gps.d.ts @@ -12,6 +12,11 @@ export function get_gps_map_svg(gps: Gps): string; export function get_polygon(gps: Gps): Float64Array; /** * @param {Gps} gps +* @returns {boolean} +*/ +export function has_heights(gps: Gps): boolean; +/** +* @param {Gps} gps * @returns {Float64Array} */ export function get_polyline(gps: Gps): Float64Array; @@ -59,6 +64,7 @@ export interface InitOutput { readonly __wbg_gps_free: (a: number) => void; readonly get_gps_map_svg: (a: number, b: number) => void; readonly get_polygon: (a: number, b: number) => void; + readonly has_heights: (a: number) => number; readonly get_polyline: (a: number, b: number) => void; readonly get_gps_content: (a: number, b: number) => void; readonly request_map: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number) => number; @@ -67,11 +73,11 @@ export interface InitOutput { readonly __wbindgen_malloc: (a: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; readonly __wbindgen_export_2: WebAssembly.Table; - readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1: (a: number, b: number, c: number) => void; + readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7: (a: number, b: number, c: number) => void; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_free: (a: number, b: number) => void; readonly __wbindgen_exn_store: (a: number) => void; - readonly wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137: (a: number, b: number, c: number, d: number) => void; + readonly wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027: (a: number, b: number, c: number, d: number) => void; } export type SyncInitInput = BufferSource | WebAssembly.Module; diff --git a/apps/gipy/pkg/gps.js b/apps/gipy/pkg/gps.js index 39c2a68045..563bf6251c 100644 --- a/apps/gipy/pkg/gps.js +++ b/apps/gipy/pkg/gps.js @@ -205,7 +205,7 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } function __wbg_adapter_24(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7(arg0, arg1, addHeapObject(arg2)); } function _assertClass(instance, klass) { @@ -263,6 +263,16 @@ export function get_polygon(gps) { } } +/** +* @param {Gps} gps +* @returns {boolean} +*/ +export function has_heights(gps) { + _assertClass(gps, Gps); + const ret = wasm.has_heights(gps.ptr); + return ret !== 0; +} + /** * @param {Gps} gps * @returns {Float64Array} @@ -368,8 +378,8 @@ function handleError(f, args) { wasm.__wbindgen_exn_store(addHeapObject(e)); } } -function __wbg_adapter_84(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); +function __wbg_adapter_85(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); } /** @@ -430,9 +440,6 @@ async function load(module, imports) { function getImports() { const imports = {}; imports.wbg = {}; - imports.wbg.__wbg_log_d04343b58be82b0f = function(arg0, arg1) { - console.log(getStringFromWasm0(arg0, arg1)); - }; imports.wbg.__wbindgen_string_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof(obj) === 'string' ? obj : undefined; @@ -441,6 +448,9 @@ function getImports() { getInt32Memory0()[arg0 / 4 + 1] = len0; getInt32Memory0()[arg0 / 4 + 0] = ptr0; }; + imports.wbg.__wbg_log_d04343b58be82b0f = function(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); + }; imports.wbg.__wbindgen_object_drop_ref = function(arg0) { takeObject(arg0); }; @@ -460,6 +470,10 @@ function getImports() { const ret = getObject(arg0).fetch(getObject(arg1)); return addHeapObject(ret); }; + imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) { const ret = getObject(arg0).signal; return addHeapObject(ret); @@ -471,10 +485,6 @@ function getImports() { imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) { getObject(arg0).abort(); }; - imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); - return addHeapObject(ret); - }, arguments) }; imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () { const ret = new Headers(); return addHeapObject(ret); @@ -610,7 +620,7 @@ function getImports() { const a = state0.a; state0.a = 0; try { - return __wbg_adapter_84(a, state0.b, arg0, arg1); + return __wbg_adapter_85(a, state0.b, arg0, arg1); } finally { state0.a = a; } @@ -675,8 +685,8 @@ function getImports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper2245 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 267, __wbg_adapter_24); + imports.wbg.__wbindgen_closure_wrapper2214 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 268, __wbg_adapter_24); return addHeapObject(ret); }; diff --git a/apps/gipy/pkg/gps_bg.wasm b/apps/gipy/pkg/gps_bg.wasm index 8e0fbc07eb..7a42fb564e 100644 Binary files a/apps/gipy/pkg/gps_bg.wasm and b/apps/gipy/pkg/gps_bg.wasm differ diff --git a/apps/gipy/pkg/gps_bg.wasm.d.ts b/apps/gipy/pkg/gps_bg.wasm.d.ts index b4303ee301..3b95ada789 100644 --- a/apps/gipy/pkg/gps_bg.wasm.d.ts +++ b/apps/gipy/pkg/gps_bg.wasm.d.ts @@ -4,6 +4,7 @@ export const memory: WebAssembly.Memory; export function __wbg_gps_free(a: number): void; export function get_gps_map_svg(a: number, b: number): void; export function get_polygon(a: number, b: number): void; +export function has_heights(a: number): number; export function get_polyline(a: number, b: number): void; export function get_gps_content(a: number, b: number): void; export function request_map(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number): number; @@ -12,8 +13,8 @@ export function gps_from_area(a: number, b: number, c: number, d: number): numbe export function __wbindgen_malloc(a: number): number; export function __wbindgen_realloc(a: number, b: number, c: number): number; export const __wbindgen_export_2: WebAssembly.Table; -export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(a: number, b: number, c: number): void; +export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7(a: number, b: number, c: number): void; export function __wbindgen_add_to_stack_pointer(a: number): number; export function __wbindgen_free(a: number, b: number): void; export function __wbindgen_exn_store(a: number): void; -export function wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(a: number, b: number, c: number, d: number): void; +export function wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027(a: number, b: number, c: number, d: number): void; diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js index 395b1ac936..1b030f5cd5 100644 --- a/apps/gipy/settings.js +++ b/apps/gipy/settings.js @@ -3,6 +3,8 @@ // Load settings var settings = Object.assign({ lost_distance: 50, + wake_up_speed: 13, + active_time: 10, buzz_on_turns: false, disable_bluetooth: true, brightness: 0.5, @@ -44,6 +46,24 @@ writeSettings(); }, }, + "wake-up speed": { + value: settings.wake_up_speed, + min: 0, + max: 30, + onchange: (v) => { + settings.wake_up_speed = v; + writeSettings(); + }, + }, + "active time": { + value: settings.active_time, + min: 5, + max: 60, + onchange: (v) => { + settings.active_time = v; + writeSettings(); + }, + }, "brightness": { value: settings.brightness, min: 0, diff --git a/apps/gipy/shot.png b/apps/gipy/shot.png new file mode 100644 index 0000000000..c2ffea7241 Binary files /dev/null and b/apps/gipy/shot.png differ