mirror of https://github.com/espruino/BangleApps
1629 lines
56 KiB
JavaScript
1629 lines
56 KiB
JavaScript
let simulated = false;
|
|
let displaying = false;
|
|
let in_menu = false;
|
|
let go_backwards = false;
|
|
let status;
|
|
|
|
let initial_options = Bangle.getOptions();
|
|
|
|
let interests_colors = [
|
|
[1, 1, 1], // Waypoints, white
|
|
[1, 0, 0], // Bakery, red
|
|
[0, 0, 1], // DrinkingWater, blue
|
|
[0, 1, 1], // Toilets, cyan
|
|
[0, 1, 0], // 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,
|
|
wake_up_speed: 13,
|
|
active_time: 10,
|
|
brightness: 0.5,
|
|
buzz_on_turns: false,
|
|
disable_bluetooth: false,
|
|
power_lcd_off: false,
|
|
powersave_by_default: false,
|
|
sleep_between_waypoints: false,
|
|
},
|
|
s.readJSON("gipy.json", true) || {}
|
|
);
|
|
|
|
let powersaving = settings.powersave_by_default;
|
|
|
|
// let profile_start_times = [];
|
|
|
|
// function start_profiling() {
|
|
// profile_start_times.push(getTime());
|
|
// }
|
|
|
|
// function end_profiling(label) {
|
|
// let end_time = getTime();
|
|
// let elapsed = end_time - profile_start_times.pop();
|
|
// console.log("profile:", label, "took", elapsed);
|
|
// }
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// return a string containing estimated time of arrival.
|
|
// speed is in km/h
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
class TilesOffsets {
|
|
constructor(filename, offset) {
|
|
let header = E.toArrayBuffer(s.read(filename, offset, 4));
|
|
let type_size = Uint8Array(header, 0, 1)[0];
|
|
offset += 1;
|
|
this.entry_size = Uint8Array(header, 1, 1)[0];
|
|
offset += 1;
|
|
let non_empty_tiles_number = Uint16Array(header, 2, 1)[0];
|
|
offset += 2;
|
|
|
|
let bytes = (type_size==24)?3:2;
|
|
let buffer = E.toArrayBuffer(s.read(filename, offset, 2*non_empty_tiles_number+bytes*non_empty_tiles_number));
|
|
this.non_empty_tiles = Uint16Array(buffer, 0, non_empty_tiles_number);
|
|
offset += 2 * non_empty_tiles_number;
|
|
if (type_size == 24) {
|
|
this.non_empty_tiles_ends = Uint24Array(
|
|
buffer,
|
|
2*non_empty_tiles_number,
|
|
non_empty_tiles_number
|
|
);
|
|
offset += 3 * non_empty_tiles_number;
|
|
} else if (type_size == 16) {
|
|
this.non_empty_tiles_ends = Uint16Array(
|
|
buffer,
|
|
2*non_empty_tiles_number,
|
|
non_empty_tiles_number
|
|
);
|
|
offset += 2 * non_empty_tiles_number;
|
|
} else {
|
|
throw "unknown size";
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
end_offset() {
|
|
return (
|
|
this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] *
|
|
this.entry_size
|
|
);
|
|
}
|
|
}
|
|
|
|
class Map {
|
|
constructor(filename, offset) {
|
|
// header
|
|
|
|
let header = E.toArrayBuffer(s.read(filename, offset, 43));
|
|
let color_array = Uint8Array(header, 0, 3);
|
|
this.color = [
|
|
color_array[0] / 255,
|
|
color_array[1] / 255,
|
|
color_array[2] / 255,
|
|
];
|
|
offset += 3;
|
|
this.first_tile = Int32Array(header, 3, 2); // absolute tile id of first tile
|
|
offset += 2 * 4;
|
|
this.grid_size = Uint32Array(header, 11, 2); // tiles width and height
|
|
offset += 2 * 4;
|
|
this.start_coordinates = Float64Array(header, 19, 2); // min x and y coordinates
|
|
offset += 2 * 8;
|
|
let side_array = Float64Array(header, 35, 1); // side of a tile
|
|
this.side = side_array[0];
|
|
offset += 8;
|
|
|
|
// tiles offsets
|
|
let res = new TilesOffsets(filename, 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;
|
|
}
|
|
|
|
add_to_tile_image(img, absolute_tile_x, absolute_tile_y) {
|
|
let tile_x = absolute_tile_x - this.first_tile[0];
|
|
let tile_y = absolute_tile_y - this.first_tile[1];
|
|
let side = img.getWidth() - 6;
|
|
|
|
let thick = this.color[0] == 1;
|
|
img.setColor(this.color[0], this.color[1], this.color[2]);
|
|
|
|
let tile_num = tile_x + tile_y * this.grid_size[0];
|
|
|
|
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];
|
|
for (let i = offset; i < upper_limit; i += 4) {
|
|
let x1 = (line.buffer[i] / 255) * side + 3;
|
|
let y1 = ((255 - line.buffer[i + 1]) / 255) * side + 3;
|
|
let x2 = (line.buffer[i + 2] / 255) * side + 3;
|
|
let y2 = ((255 - line.buffer[i + 3]) / 255) * side + 3;
|
|
|
|
let thickness = 1;
|
|
if (thick) {
|
|
thickness = 3.5;
|
|
}
|
|
|
|
let xdiff = x2 - x1;
|
|
let ydiff = y2 - y1;
|
|
let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
|
|
let ox = (-ydiff / d) * thickness;
|
|
let oy = (xdiff / d) * thickness;
|
|
img.fillPoly([
|
|
x1 + ox,
|
|
y1 + oy,
|
|
x2 + ox,
|
|
y2 + oy,
|
|
x2 - ox,
|
|
y2 - oy,
|
|
x1 - ox,
|
|
y1 - oy,
|
|
]);
|
|
|
|
// } else {
|
|
// img.drawLine(x1, y1, x2, y2);
|
|
|
|
// }
|
|
}
|
|
}
|
|
}
|
|
|
|
class Interests {
|
|
constructor(filename, offset) {
|
|
let header = E.toArrayBuffer(s.read(filename, offset, 40));
|
|
this.first_tile = Int32Array(header, 0, 2); // absolute tile id of first tile
|
|
offset += 2 * 4;
|
|
this.grid_size = Uint32Array(header, 8, 2); // tiles width and height
|
|
offset += 2 * 4;
|
|
this.start_coordinates = Float64Array(header, 16, 2); // min x and y coordinates
|
|
offset += 2 * 8;
|
|
let side_array = Float64Array(header, 32, 1); // side of a tile
|
|
this.side = side_array[0];
|
|
offset += 8;
|
|
|
|
let res = new TilesOffsets(filename, offset);
|
|
offset = res[1];
|
|
this.offsets = res[0];
|
|
let end = this.offsets.end_offset();
|
|
this.binary_interests = new Uint8Array(end);
|
|
let buffer = E.toArrayBuffer(s.read(filename, offset, end));
|
|
this.binary_interests = Uint8Array(buffer);
|
|
offset += end;
|
|
return [this, offset];
|
|
}
|
|
|
|
add_to_tile_image(img, absolute_tile_x, absolute_tile_y) {
|
|
let tile_x = absolute_tile_x - this.first_tile[0];
|
|
let tile_y = absolute_tile_y - this.first_tile[1];
|
|
let side = img.getWidth() - 6;
|
|
|
|
let tile_num = tile_x + tile_y * this.grid_size[0];
|
|
|
|
let offset = this.offsets.tile_start_offset(tile_num);
|
|
let upper_limit = this.offsets.tile_end_offset(tile_num);
|
|
|
|
let buffer = this.binary_interests;
|
|
for (let i = offset; i < upper_limit; i += 3) {
|
|
let type = buffer[i];
|
|
let x = (buffer[i + 1] / 255) * side + 3;
|
|
let y = ((255 - buffer[i + 2]) / 255) * side + 3;
|
|
|
|
let color = interests_colors[type];
|
|
if (type == 0) {
|
|
img.setColor(0, 0, 0).fillCircle(x, y, 6);
|
|
}
|
|
img.setColor(color[0], color[1], color[2]).fillCircle(x, y, 5);
|
|
}
|
|
}
|
|
}
|
|
|
|
class Status {
|
|
constructor(path, maps, interests, heights) {
|
|
this.path = path;
|
|
this.default_options = true; // do we still have default options ?
|
|
this.active = true; // should we have screen on
|
|
this.last_activity = getTime();
|
|
this.maps = maps;
|
|
this.interests = interests;
|
|
this.heights = heights;
|
|
this.screen = MAP;
|
|
this.on_path = true; // are we on the path or lost ?
|
|
this.position = null; // where we are
|
|
this.direction = 0;
|
|
this.adjusted_cos_direction = Math.cos(-Math.PI / 2.0);
|
|
this.adjusted_sin_direction = Math.sin(-Math.PI / 2.0);
|
|
this.zoomed_in = true;
|
|
|
|
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;
|
|
this.reset_images_cache();
|
|
|
|
let width = g.getWidth();
|
|
let height = g.getHeight();
|
|
let diagonal_third = Math.sqrt(width * width + height * height) / 3;
|
|
this.scale_factor = diagonal_third / maps[0].side; // multiply geo coordinates by this to get pixels coordinates
|
|
|
|
if (this.path !== null) {
|
|
//TODO: float32 ??
|
|
let r = new Float64Array(this.path.len);
|
|
// 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[i] = r[i+1] + 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
|
|
}
|
|
reset_images_cache() {
|
|
let tiles_per_diagonals = this.zoomed_in ? 3 : 5;
|
|
let screen_width = g.getWidth();
|
|
let screen_height = g.getHeight();
|
|
this.images_cache = [];
|
|
|
|
let img_side =
|
|
Math.ceil(
|
|
Math.sqrt(screen_width * screen_width + screen_height * screen_height) /
|
|
tiles_per_diagonals
|
|
) + 6; // three extra pixels on each side to allow thick lines
|
|
|
|
E.defrag();
|
|
let limit = tiles_per_diagonals * (tiles_per_diagonals + 1);
|
|
|
|
for (let i = 0; i < limit; i++) {
|
|
let img = Graphics.createArrayBuffer(img_side, img_side, 4, {
|
|
msb: true,
|
|
});
|
|
this.images_cache.push({ image: img, x: -1, y: -1 });
|
|
}
|
|
}
|
|
activate() {
|
|
if (!powersaving) {
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
check_activity() {
|
|
if (!this.active || !powersaving) {
|
|
return;
|
|
}
|
|
if (getTime() - this.last_activity > settings.active_time) {
|
|
this.active = false;
|
|
Bangle.setLCDBrightness(0);
|
|
if (settings.power_lcd_off) {
|
|
Bangle.setLCDPower(false);
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
|
|
let oldest_point = this.old_points[0];
|
|
let distance_to_oldest = oldest_point.distance(position);
|
|
let time_to_oldest = now - this.old_times[0];
|
|
|
|
// every 3 points we count the distance
|
|
if (this.gps_coordinates_counter % 3 == 0) {
|
|
if (time_to_oldest > 6 || distance_to_oldest < 150.0) {
|
|
// to avoid gps glitches (sometimes the gps signal will make you jump around)
|
|
// however if gps disconnects (time_to_oldest becomes large) we still count the distance
|
|
// when it re-activates
|
|
this.advanced_distance += distance_to_oldest;
|
|
}
|
|
}
|
|
|
|
this.instant_speed = distance_to_oldest / (now - this.old_times[0]);
|
|
|
|
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;
|
|
}
|
|
this.direction = direction;
|
|
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(initial_options);
|
|
}
|
|
} else {
|
|
if (this.default_options && powersaving) {
|
|
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
|
|
);
|
|
|
|
if (this.path !== null) {
|
|
// detect segment we are on now
|
|
let next_segment = 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
|
|
);
|
|
|
|
if (this.is_lost(next_segment)) {
|
|
// start_profiling();
|
|
// it did not work, try anywhere
|
|
next_segment = this.path.nearest_segment(
|
|
this.displayed_position,
|
|
0,
|
|
this.path.len - 1,
|
|
cos_direction,
|
|
sin_direction
|
|
);
|
|
// 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);
|
|
}
|
|
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 + (go_backwards ? 0 : 1);
|
|
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);
|
|
// }
|
|
// }
|
|
let reaching_waypoint = this.path.is_waypoint(next_point);
|
|
if (this.distance_to_next_point <= 100) {
|
|
if (reaching_waypoint || !settings.sleep_between_waypoints) {
|
|
this.activate();
|
|
}
|
|
|
|
if (this.reaching != next_point) {
|
|
this.reaching = next_point;
|
|
if (reaching_waypoint && settings.buzz_on_turns) {
|
|
Bangle.buzz();
|
|
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;
|
|
}
|
|
|
|
// re-display
|
|
this.display();
|
|
}
|
|
display_direction() {
|
|
let scale_factor = this.scale_factor;
|
|
if (!this.zoomed_in) {
|
|
scale_factor *= 3/5;
|
|
}
|
|
//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);
|
|
|
|
let full_angle = direction - this.angle;
|
|
|
|
c = this.projected_point.coordinates(
|
|
this.displayed_position,
|
|
this.adjusted_cos_direction,
|
|
this.adjusted_sin_direction,
|
|
scale_factor
|
|
);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
tile_image(absolute_tile_x, absolute_tile_y) {
|
|
// in the cache old images are front and recently used ones are back
|
|
let cached_img_index = this.images_cache.findIndex((i) => {
|
|
return i.x == absolute_tile_x && i.y == absolute_tile_y;
|
|
});
|
|
if (cached_img_index == -1) {
|
|
// console.log("loading", absolute_tile_x, absolute_tile_y);
|
|
let old_image = this.images_cache.shift();
|
|
this.compute_tile_image(old_image.image, absolute_tile_x, absolute_tile_y);
|
|
this.images_cache.push({
|
|
image: old_image.image,
|
|
x: absolute_tile_x,
|
|
y: absolute_tile_y,
|
|
});
|
|
return old_image.image;
|
|
} else {
|
|
let cached_img = this.images_cache.splice(cached_img_index, 1)[0];
|
|
this.images_cache.push(cached_img);
|
|
return cached_img.image;
|
|
}
|
|
}
|
|
|
|
compute_tile_image(img, absolute_tile_x, absolute_tile_y) {
|
|
img.transparent = img.toColor(1, 1, 1);
|
|
img.setBgColor(1, 1, 1).clear();
|
|
|
|
this.maps.forEach((m) => {
|
|
m.add_to_tile_image(img, absolute_tile_x, absolute_tile_y);
|
|
});
|
|
this.interests.add_to_tile_image(img, absolute_tile_x, absolute_tile_y);
|
|
}
|
|
|
|
display_map() {
|
|
|
|
// start_profiling();
|
|
let displayed_x = this.displayed_position.lon;
|
|
let displayed_y = this.displayed_position.lat;
|
|
let tile_x_coord = displayed_x / this.maps[0].side;
|
|
let tile_y_coord = displayed_y / this.maps[0].side;
|
|
let absolute_tile_x = Math.floor(tile_x_coord);
|
|
let absolute_tile_y = Math.floor(tile_y_coord);
|
|
|
|
let tiles_per_diagonals = this.zoomed_in ? 3 : 5;
|
|
let diagonal = Math.ceil(
|
|
Math.sqrt(g.getWidth() * g.getWidth() + g.getHeight() * g.getHeight()) /
|
|
tiles_per_diagonals
|
|
);
|
|
let angle = this.direction - Math.PI / 2;
|
|
let cos_direction = Math.cos(angle);
|
|
let sin_direction = Math.sin(angle);
|
|
let d = Math.floor(tiles_per_diagonals / 2);
|
|
|
|
for (let x = -d; x <= d; x++) {
|
|
for (let y = -d; y <= d; y++) {
|
|
let img = this.tile_image(absolute_tile_x + x, absolute_tile_y + y);
|
|
|
|
let screen_x =
|
|
(absolute_tile_x + x + 0.5 - tile_x_coord) * diagonal + 3;
|
|
let screen_y =
|
|
-(absolute_tile_y + y + 0.5 - tile_y_coord) * diagonal - 3;
|
|
|
|
let rotated_x = screen_x * cos_direction - screen_y * sin_direction;
|
|
let rotated_y = screen_x * sin_direction + screen_y * cos_direction;
|
|
let final_x = g.getWidth() / 2 + rotated_x;
|
|
let final_y = g.getHeight() / 2 + Y_OFFSET + rotated_y;
|
|
|
|
g.drawImage(img, final_x, final_y, { rotate: angle });
|
|
}
|
|
}
|
|
// end_profiling("map display");
|
|
}
|
|
display() {
|
|
if (displaying || in_menu) {
|
|
return; // don't draw on drawings
|
|
}
|
|
g.reset();
|
|
displaying = true;
|
|
g.clear();
|
|
if (this.screen == MAP) {
|
|
this.display_map();
|
|
if (this.position !== null) {
|
|
// start_profiling();
|
|
this.display_path();
|
|
// end_profiling("path display");
|
|
}
|
|
|
|
// start_profiling();
|
|
this.display_direction();
|
|
this.display_stats();
|
|
// end_profiling("direction and stats display");
|
|
} else {
|
|
let current_position = 0;
|
|
if (this.current_segment !== null) {
|
|
current_position =
|
|
this.remaining_distances[0] -
|
|
(this.remaining_distances[this.current_segment + 1] +
|
|
this.projected_point.distance(
|
|
this.path.point(this.current_segment + 1)
|
|
));
|
|
}
|
|
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 {
|
|
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();
|
|
g.flip();
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
previous_distance = point_distance;
|
|
previous_height = height;
|
|
previous_x = x;
|
|
previous_y = y;
|
|
}
|
|
}
|
|
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()
|
|
);
|
|
|
|
g.setFontAlign(1, 1, 0).drawString(
|
|
Math.ceil((display_start + displayed_length) / 100) / 10,
|
|
g.getWidth(),
|
|
g.getHeight()
|
|
);
|
|
|
|
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_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
|
|
);
|
|
|
|
let forward_eta = compute_eta(
|
|
now.getHours(),
|
|
now.getMinutes(),
|
|
approximate_speed,
|
|
remaining_distance / 1000
|
|
);
|
|
|
|
// 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
|
|
);
|
|
|
|
// 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 (!this.zoomed_in) {
|
|
scale_factor *= 3/5;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// display current-segment's projection
|
|
g.setColor(0, 0, 0);
|
|
g.fillCircle(projected_x, projected_y, 4);
|
|
}
|
|
|
|
// now display ourselves
|
|
if (this.on_path) {
|
|
g.setColor(0, 0, 0);
|
|
} else {
|
|
g.setColor(1, 0, 1);
|
|
}
|
|
g.fillCircle(half_width, half_height, 5);
|
|
}
|
|
}
|
|
|
|
function load_gps(filename) {
|
|
// let's display splash screen while loading file
|
|
|
|
let splashscreen = require("heatshrink").decompress(
|
|
atob(
|
|
"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"
|
|
)
|
|
);
|
|
|
|
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 file_size;
|
|
{
|
|
let buffer = s.readArrayBuffer(filename);
|
|
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(E.toArrayBuffer(s.read(filename, offset, 1)))[0];
|
|
offset += 1;
|
|
if (block_type == 0) {
|
|
// it's a map
|
|
console.log("loading map");
|
|
let res = new Map(filename, offset);
|
|
let map = res[0];
|
|
offset = res[1];
|
|
maps.push(map);
|
|
} else if (block_type == 2) {
|
|
console.log("loading path");
|
|
let res = new Path(filename, offset);
|
|
path = res[0];
|
|
offset = res[1];
|
|
} else if (block_type == 3) {
|
|
console.log("loading interests");
|
|
let res = new Interests(filename, offset);
|
|
interests = res[0];
|
|
offset = res[1];
|
|
} else if (block_type == 4) {
|
|
let heights_number = path.points.length / 2;
|
|
let buffer = E.toArrayBuffer(s.read(filename, offset, heights_number*2));
|
|
heights = Int16Array(buffer);
|
|
offset += 2 * heights_number;
|
|
} else {
|
|
console.log("todo : block type", block_type);
|
|
}
|
|
}
|
|
|
|
start_gipy(path, maps, interests, heights);
|
|
}
|
|
|
|
class Path {
|
|
constructor(filename, offset) {
|
|
let points_number = Uint16Array(E.toArrayBuffer(s.read(filename, offset, 2)))[0];
|
|
offset += 2;
|
|
let waypoints_len = Math.ceil(points_number / 8.0);
|
|
let buffer = E.toArrayBuffer(s.read(filename, offset, points_number * 16 + waypoints_len));
|
|
|
|
// path points
|
|
this.points = Float64Array(buffer, 0, 2 * points_number);
|
|
offset += 8 * points_number * 2;
|
|
|
|
// path waypoints
|
|
this.waypoints = Uint8Array(buffer, points_number * 2, 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 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 you go forward)
|
|
if (distance <= mins[orientation]) {
|
|
mins[orientation] = distance;
|
|
indices[orientation] = i - 1;
|
|
}
|
|
|
|
p1 = p2;
|
|
}
|
|
|
|
// by default correct orientation (0 forward, 1 backward) wins
|
|
// but if other one is really closer, return other one
|
|
let good_orientation = go_backwards ? 1 : 0;
|
|
if (mins[1 - good_orientation] < mins[good_orientation] / 100.0) {
|
|
return indices[1 - good_orientation];
|
|
} else {
|
|
return indices[good_orientation];
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
//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();
|
|
};
|
|
Bangle.setLCDBrightness(settings.brightness);
|
|
Bangle.setLocked(false);
|
|
E.showMenu(menu);
|
|
}
|
|
|
|
|
|
function ask_options(fn) {
|
|
g.clear();
|
|
let height = g.getHeight();
|
|
let width = g.getWidth();
|
|
g.drawRect(10, 10, width - 10, height / 2 - 10);
|
|
g.drawRect(10, height/2 + 10, width - 10, height - 10);
|
|
g.setFont("Vector:30").setFontAlign(0,0).drawString("Forward", width/2, height/4);
|
|
g.drawString("Backward", width/2, 3*height/4);
|
|
g.flip();
|
|
|
|
function options_select(b, xy) {
|
|
end = false;
|
|
if (xy.y < height / 2 - 10) {
|
|
g.setColor(0, 0, 0).fillRect(10, 10, width - 10, height / 2 - 10);
|
|
g.setColor(1, 1, 1).setFont("Vector:30").setFontAlign(0,0).drawString("Forward", width/2, height/4);
|
|
end = true;
|
|
} else if (xy.y > height/2 + 10) {
|
|
g.setColor(0, 0, 0).fillRect(10, height/2 + 10, width - 10, height - 10);
|
|
g.setColor(1, 1, 1).setFont("Vector:30").setFontAlign(0,0).drawString("Backward", width/2, 3*height/4);
|
|
go_backwards = true;
|
|
end = true;
|
|
}
|
|
if (end) {
|
|
g.flip();
|
|
Bangle.removeListener("touch", options_select);
|
|
console.log("loading", fn);
|
|
load_gps(fn);
|
|
}
|
|
}
|
|
Bangle.on("touch", options_select);
|
|
}
|
|
|
|
function start(fn) {
|
|
E.showMenu();
|
|
ask_options(fn);
|
|
}
|
|
|
|
function start_gipy(path, maps, interests, heights) {
|
|
console.log("starting");
|
|
|
|
if (!simulated && settings.disable_bluetooth) {
|
|
NRF.sleep(); // disable bluetooth completely
|
|
}
|
|
|
|
console.log("creating status");
|
|
status = new Status(path, maps, interests, heights);
|
|
console.log("done creating status");
|
|
|
|
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: status.zoomed_in,
|
|
format: (v) => (v ? "In" : "Out"),
|
|
onchange: (v) => {
|
|
status.zoomed_in = v;
|
|
status.reset_images_cache();
|
|
},
|
|
},
|
|
/*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", () => {
|
|
let active = status.active;
|
|
status.activate();
|
|
if (in_menu) {
|
|
return;
|
|
}
|
|
if (active && status.heights !== null) {
|
|
g.clear();
|
|
g.flip();
|
|
status.screen = (status.screen + 1) % 3;
|
|
status.display();
|
|
}
|
|
});
|
|
|
|
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();
|
|
});
|
|
|
|
if (simulated) {
|
|
console.log("un-comment simulator to use it");
|
|
// 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(8 * point_index); // use these to approximately follow path
|
|
// let p2 = status.path.point(8 * (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.2; // advance simulation
|
|
// } else {
|
|
// fake_gps_point += 0.2; // advance simulation
|
|
// }
|
|
// console.log(fake_gps_point);
|
|
// 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]);
|
|
}
|
|
} else {
|
|
drawMenu();
|
|
}
|