diff --git a/.eslintignore b/.eslintignore index fcbea07f9..4af79d129 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,5 @@ apps/banglerun/rollup.config.js apps/schoolCalendar/fullcalendar/main.js apps/authentiwatch/qr_packed.js apps/qrcode/qr-scanner.umd.min.js +apps/gipy/pkg/gpconv.js *.test.js diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog new file mode 100644 index 000000000..3b0d62009 --- /dev/null +++ b/apps/gipy/ChangeLog @@ -0,0 +1,65 @@ +0.01: Initial code + +0.05: + * We now buzz before reaching a waypoint. + * Display is only updated when not locked. + * We detect leaving path and finding path again. + * We display remaining distance to next point. + +0.06: + * Special display for points with steep turns. + * Buzz on points with steep turns and unlock. + * Losing gps is now displayed. + +0.07: + * We now use orientation to detect current segment + when segments overlap going in both directions. + * File format is now versioned. + +0.08: + * Don't use gps course anymore but figure it from previous positions. + * Bugfix: path colors are back. + * Always buzz when reaching waypoint even if unlocked. + +0.09: + * We now display interest points. + * Menu to choose which file to load. + +0.10: + * Display performances enhancement. + * Waypoints information is embedded in file and extracted from comments on + points. + * Bugfix in map display (last segment was missing + wrong colors). + * Waypoint detections using OSM + sharp angles + * New algorith for direction detection + +0.11: + * Better fonts (more free space, still readable). + * Display direction to nearest point when lost. + * Display average speed. + * Turn off gps when locked and between points + +0.12: + * Bugfix in speed computation. + * Bugfix in current segment detection. + * Bugfix : lost direction. + * Larger fonts. + * Detecting next point correctly when going back. + +0.13: + * Bugfix in lost direction. + * Buzzing 100m ahead instead of 50m. + * Detect sharp turns. + * Display instant speed. + * New instant speed algorithm. + * Bugfix for remaining distance when going back. + +0.14: + * Detect starting distance to compute a good average speed. + * Settings + * Account for breaks in average speed. + +0.15: + * Record traveled distance to get a good average speed. + * Breaks (low speed) will not count in average speed. + * Bugfix in average speed. diff --git a/apps/gipy/README.md b/apps/gipy/README.md new file mode 100644 index 000000000..6c9b87c23 --- /dev/null +++ b/apps/gipy/README.md @@ -0,0 +1,109 @@ +# Gipy + +Gipy allows you to follow gpx traces on your watch. + +![Screenshot](screenshot1.png) + + +It is for now meant for bicycling and not hiking +(it uses your movement to figure out your orientation +and walking is too slow). + +It is untested on Banglejs1. If you can try it, you would be welcome. + +This software is not perfect but surprisingly useful. + +## Features + +It provides the following features : + +- display the path with current position from gps +- detects and buzzes if you leave the path +- buzzes before sharp turns +- buzzes before nodes with comments +(for example when you need to turn in https://mapstogpx.com/) +- display instant / average speed +- display distance to next node +- display additional data from openstreetmap : + - water points + - toilets + - artwork + - bakeries + +optionally it can also: + +- try to turn off gps between crossroads to save battery + +## Usage + +### Preparing the file + +You first need to have a trace file in *gpx* format. +Usually I download from [komoot](https://www.komoot.com/) or I export +from google maps using [mapstogpx](https://mapstogpx.com/). + +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. +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. +This will request additional information from openstreetmap. +Your path will be displayed in svg. + +### Starting Gipy + +Once you start gipy you will have a menu for selecting your trace (if more than one). +Choose the one you want and here you go : + +![Screenshot](screenshot2.png) + +On your screen you can see : + +- yourself (the big black dot) +- the path (the top of the screen is in front of you) +- if needed a projection of yourself on the path (small black dot) +- extremities of segments as white dots +- turning points as doubled white dots +- some text on the left (from top to bottom) : + * current time + * left distance till end of current segment + * distance from start of path / path length + * average speed / instant speed +- interest points from openstreetmap as color dots : + * red : bakery + * deep blue : water point + * cyan : toilets (often doubles as water point) + * 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 +- a black segment extending from you when you are lost, indicating the rough direction of where to go + +### Settings + +Few settings for now (feel free to suggest me more) : + +- keep gps alive : if turned off, will try to save battery by turning the gps off on long segments +- max speed : used to compute how long to turn the gps off + +### Caveats + +It is good to use but you should know : + +- the gps might take a long time to start initially (see the assisted gps update app). +- gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else. +- your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY** +- sometimes the watch will tell you that you are lost but you are in fact on the path. +- battery saving by turning off gps is not very well tested (disabled by default). +- buzzing does not always work: when there is a high load on the watch, the buzzes might just never happen :-(. +- buzzes are not strong enough to be always easily noticed. +- be careful when **GOING DOWNHILL AT VERY HIGH SPEED**. I already missed a few turning points and by the time I realized it, +I had to go back uphill by quite a distance. + +## Creator + +Feel free to give me feedback : is it useful for you ? what other features would you like ? + +frederic.wagner@imag.fr diff --git a/apps/gipy/TODO b/apps/gipy/TODO new file mode 100644 index 000000000..53c3530e2 --- /dev/null +++ b/apps/gipy/TODO @@ -0,0 +1,25 @@ + +* bugs + +- when exactly on turn, distance to next point is still often 50m + -----> it does not buzz very often on turns + +- when going backwards we have a tendencing to get a wrong current_segment + +* additional features + +- config screen + - are we on foot (and should use compass) + +- we need to buzz 200m before sharp turns (or even better, 30seconds) +(and look at more than next point) + +- display distance to next water/toilet ? +- dynamic map rescale +- display scale (100m) + +- compress path ? + +* misc + +- code is becoming messy diff --git a/apps/gipy/app-icon.js b/apps/gipy/app-icon.js new file mode 100644 index 000000000..0fc51609f --- /dev/null +++ b/apps/gipy/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIA/AE8VqoAGCy1RiN3CyYuBi93uIXJIBV3AAIuMBY4XjQ5YXPRAIAEOwIABPBC4LF54wGF6IwFC5jWGIwxIJC4xJFgDuJJAxJFC6TEIJBzEHGCIYPGA5JQC44YPGBBJKY4gwRfQL4DGCL4GGCAXPGAxGBAAJIMGAwWCGCoWGC55HHJB5HIC8pGDSChfXC5AWIL5ynOC45GJC4h3IIyYwCFxwADgB1SC44uSC4guSAH4Ab")) diff --git a/apps/gipy/app.js b/apps/gipy/app.js new file mode 100644 index 000000000..ae82e5dfb --- /dev/null +++ b/apps/gipy/app.js @@ -0,0 +1,766 @@ +let simulated = false; +let file_version = 3; +let code_key = 47490; + +var settings = Object.assign( + { + keep_gps_alive: true, + max_speed: 35, + }, + require("Storage").readJSON("gipy.json", true) || {} +); + +let interests_colors = [ + 0xf800, // Bakery, red + 0x001f, // DrinkingWater, blue + 0x07ff, // Toilets, cyan + 0x07e0, // Artwork, green +]; + +function binary_search(array, x) { + let start = 0, + end = array.length - 1; + + while (start <= end) { + let mid = Math.floor((start + end) / 2); + if (array[mid] < x) start = mid + 1; + else end = mid - 1; + } + return start; +} + +class Status { + constructor(path) { + this.path = path; + this.on_path = false; // are we on the path or lost ? + this.position = null; // where we are + this.adjusted_cos_direction = null; // cos of where we look at + this.adjusted_sin_direction = null; // 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.paused_time = 0.0; // how long did we stop (stops don't count in avg speed) + this.paused_since = getTime(); + + 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 = this.paused_since; // time we start + this.advanced_distance = 0.0; + this.gps_coordinates_counter = 0; // how many coordinates did we receive + this.old_points = []; + this.old_times = []; + } + 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. + this.gps_coordinates_counter += 1; + let now = getTime(); + this.old_points.push(position); + this.old_times.push(now); + + if (this.old_points.length == 1) { + return null; + } + + let last_point = this.old_points[this.old_points.length - 1]; + let oldest_point = this.old_points[0]; + + // every 7 points we count the distance + if (this.gps_coordinates_counter % 7 == 0) { + let distance = last_point.distance(oldest_point); + if (distance < 150.0) { + // to avoid gps glitches + this.advanced_distance += distance; + } + } + + if (this.old_points.length == 8) { + let p1 = this.old_points[0] + .plus(this.old_points[1]) + .plus(this.old_points[2]) + .plus(this.old_points[3]) + .times(1 / 4); + let p2 = this.old_points[4] + .plus(this.old_points[5]) + .plus(this.old_points[6]) + .plus(this.old_points[7]) + .times(1 / 4); + let t1 = (this.old_times[1] + this.old_times[2]) / 2; + let t2 = (this.old_times[5] + this.old_times[6]) / 2; + this.instant_speed = p1.distance(p2) / (t2 - t1); + this.old_points.shift(); + this.old_times.shift(); + } else { + this.instant_speed = + oldest_point.distance(last_point) / (now - this.old_times[0]); + + // update paused time if we are too slow + if (this.instant_speed < 2) { + if (this.paused_since === null) { + this.paused_since = now; + } + } else { + if (this.paused_since !== null) { + this.paused_time += now - this.paused_since; + this.paused_since = null; + } + } + } + // 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, maybe_direction) { + let direction = this.new_position_reached(new_position); + if (direction === null) { + if (maybe_direction === null) { + return; + } else { + direction = maybe_direction; + } + } + + this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); + this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); + cos_direction = Math.cos(direction); + sin_direction = Math.sin(direction); + this.position = new_position; + + // detect segment we are on now + let res = this.path.nearest_segment( + this.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)) { + // it did not work, try anywhere + res = this.path.nearest_segment( + this.position, + 0, + this.path.len - 1, + cos_direction, + sin_direction + ); + orientation = res[0]; + next_segment = res[1]; + } + // 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; + } + + 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.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) { + Bangle.buzz(); + setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); + if (Bangle.isLocked()) { + Bangle.setLocked(false); + } + } + } + // re-display + this.display(orientation); + } + remaining_distance(orientation) { + let remaining_in_correct_orientation = + this.remaining_distances[this.current_segment + 1] + + this.position.distance(this.path.point(this.current_segment + 1)); + + if (orientation == 0) { + return remaining_in_correct_orientation; + } else { + return this.remaining_distances[0] - remaining_in_correct_orientation; + } + } + is_lost(segment) { + let distance_to_nearest = this.position.distance_to_segment( + this.path.point(segment), + this.path.point(segment + 1) + ); + return distance_to_nearest > 50; + } + display(orientation) { + g.clear(); + this.display_map(); + + this.display_interest_points(); + this.display_stats(orientation); + Bangle.drawWidgets(); + } + display_interest_points() { + // this is the algorithm in case we have a lot of interest points + // let's draw all points for 5 segments centered on current one + let starting_group = Math.floor(Math.max(this.current_segment - 2, 0) / 3); + let ending_group = Math.floor( + Math.min(this.current_segment + 2, this.path.len - 2) / 3 + ); + let starting_bucket = binary_search( + this.path.interests_starts, + starting_group + ); + let ending_bucket = binary_search( + this.path.interests_starts, + ending_group + 0.5 + ); + // we have 5 points per bucket + let end_index = Math.min( + this.path.interests_types.length - 1, + ending_bucket * 5 + ); + for (let i = starting_bucket * 5; i <= end_index; i++) { + let index = this.path.interests_on_path[i]; + let interest_point = this.path.interest_point(index); + let color = this.path.interest_color(i); + let c = interest_point.coordinates( + this.position, + this.adjusted_cos_direction, + this.adjusted_sin_direction + ); + g.setColor(color).fillCircle(c[0], c[1], 5); + } + } + display_stats(orientation) { + let remaining_distance = this.remaining_distance(orientation); + let rounded_distance = Math.round(remaining_distance / 100) / 10; + let total = Math.round(this.remaining_distances[0] / 100) / 10; + let now = new Date(); + let minutes = now.getMinutes().toString(); + if (minutes.length < 2) { + minutes = "0" + minutes; + } + let hours = now.getHours().toString(); + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(hours + ":" + minutes, 0, 30); + + g.setFont("6x8:2").drawString( + "" + this.distance_to_next_point + "m", + 0, + g.getHeight() - 49 + ); + + let point_time = this.old_times[this.old_times.length - 1]; + let done_in = point_time - this.starting_time - this.paused_time; + let 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 (in." + approximate_instant_speed + ")", + 0, + g.getHeight() - 15 + ); + + g.setFont("6x8:2").drawString( + "" + rounded_distance + "/" + total, + 0, + g.getHeight() - 32 + ); + + 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); + } + } + display_map() { + // 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 from 400ms to 200ms + let start = Math.max(this.current_segment - 4, 0); + let end = Math.min(this.current_segment + 6, this.path.len); + let pos = this.position; + let cos = this.adjusted_cos_direction; + let sin = this.adjusted_sin_direction; + let points = this.path.points; + let cx = pos.lon; + let cy = pos.lat; + let half_width = g.getWidth() / 2; + let half_height = g.getHeight() / 2; + let previous_x = null; + let previous_y = null; + for (let i = start; i < end; i++) { + let tx = (points[2 * i] - cx) * 40000.0; + let ty = (points[2 * i + 1] - cy) * 40000.0; + 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); + if (previous_x !== null) { + if (i == this.current_segment + 1) { + g.setColor(0.0, 1.0, 0.0); + } else { + g.setColor(1.0, 0.0, 0.0); + } + g.drawLine(previous_x, previous_y, x, y); + + if (this.path.is_waypoint(i - 1)) { + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 6); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 5); + } + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 4); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 3); + } + + previous_x = x; + previous_y = y; + } + + if (this.path.is_waypoint(end - 1)) { + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 6); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 5); + } + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 4); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 3); + + // now display ourselves + g.setColor(g.theme.fgH); + g.fillCircle(half_width, half_height, 5); + + // display old points for direction debug + // for (let i = 0; i < this.old_points.length; i++) { + // let tx = (this.old_points[i].lon - cx) * 40000.0; + // let ty = (this.old_points[i].lat - cy) * 40000.0; + // 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((i + 1) / 4.0, 0.0, 0.0); + // g.fillCircle(x, y, 3); + // } + + // display current-segment's projection for debug + let projection = pos.closest_segment_point( + this.path.point(this.current_segment), + this.path.point(this.current_segment + 1) + ); + + let tx = (projection.lon - cx) * 40000.0; + let ty = (projection.lat - cy) * 40000.0; + 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(g.theme.fg); + g.fillCircle(x, y, 4); + + // display direction to next point if lost + if (!this.on_path) { + let next_point = this.path.point(this.current_segment + 1); + let diff = next_point.minus(this.position); + let angle = Math.atan2(diff.lat, diff.lon); + let tx = Math.cos(angle) * 50.0; + let ty = Math.sin(angle) * 50.0; + 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(g.theme.fgH).drawLine(half_width, half_height, x, y); + } + } +} + +function load_gpc(filename) { + let buffer = require("Storage").readArrayBuffer(filename); + let offset = 0; + + // header + let header = Uint16Array(buffer, offset, 5); + offset += 5 * 2; + let key = header[0]; + let version = header[1]; + let points_number = header[2]; + if (key != code_key || version > file_version) { + E.showMessage("Invalid gpc file"); + load(); + } + + // path points + let points = Float64Array(buffer, offset, points_number * 2); + offset += 8 * points_number * 2; + + // path waypoints + let waypoints_len = Math.ceil(points_number / 8.0); + let waypoints = Uint8Array(buffer, offset, waypoints_len); + offset += waypoints_len; + + // interest points + let interests_number = header[3]; + let interests_coordinates = Float64Array( + buffer, + offset, + interests_number * 2 + ); + offset += 8 * interests_number * 2; + let interests_types = Uint8Array(buffer, offset, interests_number); + offset += interests_number; + + // interests on path + let interests_on_path_number = header[4]; + let interests_on_path = Uint16Array(buffer, offset, interests_on_path_number); + offset += 2 * interests_on_path_number; + let starts_length = Math.ceil(interests_on_path_number / 5.0); + let interests_starts = Uint16Array(buffer, offset, starts_length); + offset += 2 * starts_length; + + return [ + points, + waypoints, + interests_coordinates, + interests_types, + interests_on_path, + interests_starts, + ]; +} + +class Path { + constructor(arrays) { + this.points = arrays[0]; + this.waypoints = arrays[1]; + this.interests_coordinates = arrays[2]; + this.interests_types = arrays[3]; + this.interests_on_path = arrays[4]; + this.interests_starts = arrays[5]; + } + + 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; + } + + // execute op on all segments. + // start is index of first wanted segment + // end is 1 after index of last wanted segment + on_segments(op, start, end) { + let previous_point = null; + for (let i = start; i < end + 1; i++) { + let point = new Point(this.points[2 * i], this.points[2 * i + 1]); + if (previous_point !== null) { + op(previous_point, point, i); + } + previous_point = point; + } + } + + // 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); + } + + interest_point(index) { + let lon = this.interests_coordinates[2 * index]; + let lat = this.interests_coordinates[2 * index + 1]; + return new Point(lon, lat); + } + + interest_color(index) { + return interests_colors[this.interests_types[index]]; + } + + // 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]; + this.on_segments( + function (p1, p2, i) { + // we use the dot product to figure out if oriented correctly + // let distance = point.fake_distance_to_segment(p1, p2); + + let projection = point.closest_segment_point(p1, p2); + let distance = point.fake_distance(projection); + + // let d = projection.minus(point).times(40000.0); + // let rotated_x = d.lon * acos - d.lat * asin; + // let rotated_y = d.lon * asin + d.lat * acos; + // let x = g.getWidth() / 2 - Math.round(rotated_x); // x is inverted + // let y = g.getHeight() / 2 + Math.round(rotated_y); + // + let diff = p2.minus(p1); + let dot = cos_direction * diff.lon + sin_direction * diff.lat; + let orientation = +(dot < 0); // index 0 is good orientation + // g.setColor(0.0, 0.0 + orientation, 1.0 - orientation).fillCircle( + // x, + // y, + // 10 + // ); + if (distance <= mins[orientation]) { + mins[orientation] = distance; + indices[orientation] = i - 1; + } + }, + start, + end + ); + // by default correct orientation (0) wins + // but if other one is really closer, return other one + if (mins[1] < mins[0] / 10.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) { + let translated = this.minus(current_position).times(40000.0); + 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), + ]; + } + 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 d = this.minus(other_point); + return d.lon * d.lon + d.lat * d.lat; + } + 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)); + } + 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 l2 = v.length_squared(w); // 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)); + return v.plus(w.minus(v).times(t)); // Projection falls on the segment + } + distance_to_segment(v, w) { + let projection = this.closest_segment_point(v, w); + return this.distance(projection); + } + fake_distance_to_segment(v, w) { + let projection = this.closest_segment_point(v, w); + return this.fake_distance(projection); + } +} + +Bangle.loadWidgets(); + +let fake_gps_point = 0.0; +function simulate_gps(status) { + if (fake_gps_point > status.path.len - 1) { + return; + } + let point_index = Math.floor(fake_gps_point); + if (point_index >= status.path.len) { + return; + } + //let p1 = status.path.point(0); + //let n = status.path.len; + //let p2 = status.path.point(n - 1); + let p1 = status.path.point(point_index); + 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)); + let old_pos = status.position; + + fake_gps_point += 0.05; // advance simulation + status.update_position(pos, null); +} + +function drawMenu() { + const menu = { + "": { title: "choose trace" }, + }; + var files = require("Storage").list(".gpc"); + 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); + + // let path = new Path(load_gpx("test.gpx")); + let path = new Path(load_gpc(fn)); + let status = new Status(path); + + if (simulated) { + status.position = new Point(status.path.point(0)); + setInterval(simulate_gps, 500, status); + } else { + // let's display start while waiting for gps signal + let p1 = status.path.point(0); + let p2 = status.path.point(1); + let diff = p2.minus(p1); + let direction = Math.atan2(diff.lat, diff.lon); + Bangle.setLocked(false); + status.update_position(p1, direction); + + 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) { + status.update_position(new Point(data.lon, data.lat), null); + } + let gps_status_color; + if (frame % 2 == 0 || valid_coordinates) { + gps_status_color = g.theme.bg; + } else { + gps_status_color = g.theme.fg; + } + g.setColor(gps_status_color) + .setFont("6x8:2") + .drawString("gps", g.getWidth() - 40, 30); + }; + + Bangle.setGPSPower(true, "gipy"); + Bangle.on("GPS", set_coordinates); + Bangle.on("lock", function (on) { + if (!on) { + Bangle.setGPSPower(true, "gipy"); // activate gps when unlocking + } + }); + } +} + +let files = require("Storage").list(".gpc"); +if (files.length <= 1) { + if (files.length == 0) { + load(); + } else { + start(files[0]); + } +} else { + drawMenu(); +} diff --git a/apps/gipy/gipy.png b/apps/gipy/gipy.png new file mode 100644 index 000000000..e9e472f5c Binary files /dev/null and b/apps/gipy/gipy.png differ diff --git a/apps/gipy/interface.html b/apps/gipy/interface.html new file mode 100644 index 000000000..a1c405ed7 --- /dev/null +++ b/apps/gipy/interface.html @@ -0,0 +1,196 @@ + + + + + + + +

Please select a gpx file to be converted to gpc and loaded.

+ + + gpx file : +
+ gpc filename : .gpc (max 24 characters) +
+ + + + + + + + + + + + + + + + + + + + + +
OpenstreetMap NODE Tags
colorkeyvalue
red
blue
cyan
green
+ +

nice tags could be : + shop/bicycle, amenity/bank, shop/supermarket, leisure/picnic_table, tourism/information, amenity/pharmacy +

+ + + + + +
+
+ + + + + + + + diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json new file mode 100644 index 000000000..2d06a7c2d --- /dev/null +++ b/apps/gipy/metadata.json @@ -0,0 +1,23 @@ +{ + "id": "gipy", + "name": "Gipy", + "shortName": "Gipy", + "version": "0.15", + "description": "Follow gpx files", + "allow_emulator":false, + "icon": "gipy.png", + "type": "app", + "tags": "tool,outdoors,gps", + "screenshots": [], + "supports": ["BANGLEJS2"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"gipy.app.js","url":"app.js"}, + {"name":"gipy.settings.js","url":"settings.js"}, + {"name":"gipy.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"gipy.json"} + ] +} diff --git a/apps/gipy/pkg/gpconv.d.ts b/apps/gipy/pkg/gpconv.d.ts new file mode 100644 index 000000000..ecffa7b69 --- /dev/null +++ b/apps/gipy/pkg/gpconv.d.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +/** +* @param {GpcSvg} gpcsvg +* @returns {Uint8Array} +*/ +export function get_gpc(gpcsvg: GpcSvg): Uint8Array; +/** +* @param {GpcSvg} gpcsvg +* @returns {Uint8Array} +*/ +export function get_svg(gpcsvg: GpcSvg): Uint8Array; +/** +* @param {string} input_str +* @returns {Promise} +*/ +export function convert_gpx_strings_no_osm(input_str: string): Promise; +/** +* @param {string} input_str +* @param {string} key1 +* @param {string} value1 +* @param {string} key2 +* @param {string} value2 +* @param {string} key3 +* @param {string} value3 +* @param {string} key4 +* @param {string} value4 +* @returns {Promise} +*/ +export function convert_gpx_strings(input_str: string, key1: string, value1: string, key2: string, value2: string, key3: string, value3: string, key4: string, value4: string): Promise; +/** +*/ +export class GpcSvg { + free(): void; +} + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly __wbg_gpcsvg_free: (a: number) => void; + readonly get_gpc: (a: number, b: number) => void; + readonly get_svg: (a: number, b: number) => void; + readonly convert_gpx_strings_no_osm: (a: number, b: number) => number; + readonly convert_gpx_strings: (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, r: number) => number; + 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__h0601691a32604cdd: (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__h25ed812378167476: (a: number, b: number, c: number, d: number) => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; +/** +* Instantiates the given `module`, which can either be bytes or +* a precompiled `WebAssembly.Module`. +* +* @param {SyncInitInput} module +* +* @returns {InitOutput} +*/ +export function initSync(module: SyncInitInput): InitOutput; + +/** +* If `module_or_path` is {RequestInfo} or {URL}, makes a request and +* for everything else, calls `WebAssembly.instantiate` directly. +* +* @param {InitInput | Promise} module_or_path +* +* @returns {Promise} +*/ +export default function init (module_or_path?: InitInput | Promise): Promise; diff --git a/apps/gipy/pkg/gpconv.js b/apps/gipy/pkg/gpconv.js new file mode 100644 index 000000000..97b37e340 --- /dev/null +++ b/apps/gipy/pkg/gpconv.js @@ -0,0 +1,645 @@ + +let wasm; + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +let WASM_VECTOR_LEN = 0; + +let cachedUint8Memory0 = new Uint8Array(); + +function getUint8Memory0() { + if (cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} + +const cachedTextEncoder = new TextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachedInt32Memory0 = new Int32Array(); + +function getInt32Memory0() { + if (cachedInt32Memory0.byteLength === 0) { + cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32Memory0; +} + +const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); + + } else { + state.a = a; + } + } + }; + real.original = state; + + 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__h0601691a32604cdd(arg0, arg1, addHeapObject(arg2)); +} + +function _assertClass(instance, klass) { + if (!(instance instanceof klass)) { + throw new Error(`expected instance of ${klass.name}`); + } + return instance.ptr; +} + +function getArrayU8FromWasm0(ptr, len) { + return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +} +/** +* @param {GpcSvg} gpcsvg +* @returns {Uint8Array} +*/ +export function get_gpc(gpcsvg) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + _assertClass(gpcsvg, GpcSvg); + wasm.get_gpc(retptr, gpcsvg.ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v0 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1); + return v0; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +/** +* @param {GpcSvg} gpcsvg +* @returns {Uint8Array} +*/ +export function get_svg(gpcsvg) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + _assertClass(gpcsvg, GpcSvg); + wasm.get_svg(retptr, gpcsvg.ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v0 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1); + return v0; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +/** +* @param {string} input_str +* @returns {Promise} +*/ +export function convert_gpx_strings_no_osm(input_str) { + const ptr0 = passStringToWasm0(input_str, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.convert_gpx_strings_no_osm(ptr0, len0); + return takeObject(ret); +} + +/** +* @param {string} input_str +* @param {string} key1 +* @param {string} value1 +* @param {string} key2 +* @param {string} value2 +* @param {string} key3 +* @param {string} value3 +* @param {string} key4 +* @param {string} value4 +* @returns {Promise} +*/ +export function convert_gpx_strings(input_str, key1, value1, key2, value2, key3, value3, key4, value4) { + const ptr0 = passStringToWasm0(input_str, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(key1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passStringToWasm0(value1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len2 = WASM_VECTOR_LEN; + const ptr3 = passStringToWasm0(key2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len3 = WASM_VECTOR_LEN; + const ptr4 = passStringToWasm0(value2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len4 = WASM_VECTOR_LEN; + const ptr5 = passStringToWasm0(key3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len5 = WASM_VECTOR_LEN; + const ptr6 = passStringToWasm0(value3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len6 = WASM_VECTOR_LEN; + const ptr7 = passStringToWasm0(key4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len7 = WASM_VECTOR_LEN; + const ptr8 = passStringToWasm0(value4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len8 = WASM_VECTOR_LEN; + const ret = wasm.convert_gpx_strings(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, ptr5, len5, ptr6, len6, ptr7, len7, ptr8, len8); + return takeObject(ret); +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} +function __wbg_adapter_69(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); +} + +/** +*/ +export class GpcSvg { + + static __wrap(ptr) { + const obj = Object.create(GpcSvg.prototype); + obj.ptr = ptr; + + return obj; + } + + __destroy_into_raw() { + const ptr = this.ptr; + this.ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_gpcsvg_free(ptr); + } +} + +async function load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function getImports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbg_gpcsvg_new = function(arg0) { + const ret = GpcSvg.__wrap(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_string_get = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_fetch_386f87a3ebf5003c = function(arg0) { + const ret = fetch(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_cb_drop = function(arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + const ret = false; + return ret; + }; + imports.wbg.__wbg_fetch_749a56934f95c96c = function(arg0, arg1) { + const ret = getObject(arg0).fetch(getObject(arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_instanceof_Response_eaa426220848a39e = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Response; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_url_74285ddf2747cb3d = function(arg0, arg1) { + const ret = getObject(arg1).url; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_status_c4ef3dd591e63435 = function(arg0) { + const ret = getObject(arg0).status; + return ret; + }; + imports.wbg.__wbg_headers_fd64ad685cf22e5d = function(arg0) { + const ret = getObject(arg0).headers; + return addHeapObject(ret); + }; + imports.wbg.__wbg_text_1169d752cc697903 = function() { return handleError(function (arg0) { + const ret = getObject(arg0).text(); + return addHeapObject(ret); + }, arguments) }; + 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); + }, arguments) }; + imports.wbg.__wbg_append_de37df908812970d = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }, arguments) }; + imports.wbg.__wbindgen_is_object = function(arg0) { + const val = getObject(arg0); + const ret = typeof(val) === 'object' && val !== null; + return ret; + }; + imports.wbg.__wbg_newnoargs_b5b063fc6c2f0376 = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_next_579e583d33566a86 = function(arg0) { + const ret = getObject(arg0).next; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_is_function = function(arg0) { + const ret = typeof(getObject(arg0)) === 'function'; + return ret; + }; + imports.wbg.__wbg_value_1ccc36bc03462d71 = function(arg0) { + const ret = getObject(arg0).value; + return addHeapObject(ret); + }; + imports.wbg.__wbg_iterator_6f9d4f28845f426c = function() { + const ret = Symbol.iterator; + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_0b9bfdd97583284e = function() { + const ret = new Object(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_self_6d479506f72c6a71 = function() { return handleError(function () { + const ret = self.self; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_window_f2557cc78490aceb = function() { return handleError(function () { + const ret = window.window; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_globalThis_7f206bda628d5286 = function() { return handleError(function () { + const ret = globalThis.globalThis; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_global_ba75c50d1cf384f4 = function() { return handleError(function () { + const ret = global.global; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = getObject(arg0) === undefined; + return ret; + }; + imports.wbg.__wbg_call_97ae9d8645dc388b = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_call_168da88779e35f61 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_next_aaef7c8aa5e212ac = function() { return handleError(function (arg0) { + const ret = getObject(arg0).next(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_done_1b73b0672e15f234 = function(arg0) { + const ret = getObject(arg0).done; + return ret; + }; + imports.wbg.__wbg_new_9962f939219f1820 = function(arg0, arg1) { + try { + var state0 = {a: arg0, b: arg1}; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_69(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return addHeapObject(ret); + } finally { + state0.a = state0.b = 0; + } + }; + imports.wbg.__wbg_resolve_99fe17964f31ffc0 = function(arg0) { + const ret = Promise.resolve(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_then_11f7a54d67b4bfad = function(arg0, arg1) { + const ret = getObject(arg0).then(getObject(arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_then_cedad20fbbd9418a = function(arg0, arg1, arg2) { + const ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_buffer_3f3d764d4747d564 = function(arg0) { + const ret = getObject(arg0).buffer; + return addHeapObject(ret); + }; + imports.wbg.__wbg_newwithbyteoffsetandlength_d9aa266703cb98be = function(arg0, arg1, arg2) { + const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_8c3f0052272a457a = function(arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_stringify_d6471d300ded9b68 = function() { return handleError(function (arg0) { + const ret = JSON.stringify(getObject(arg0)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_get_765201544a2b6869 = function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_has_8359f114ce042f5a = function() { return handleError(function (arg0, arg1) { + const ret = Reflect.has(getObject(arg0), getObject(arg1)); + return ret; + }, arguments) }; + imports.wbg.__wbg_set_bf3f89b92d5a34bf = function() { return handleError(function (arg0, arg1, arg2) { + const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); + return ret; + }, arguments) }; + imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbindgen_memory = function() { + const ret = wasm.memory; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper947 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 147, __wbg_adapter_24); + return addHeapObject(ret); + }; + + return imports; +} + +function initMemory(imports, maybe_memory) { + +} + +function finalizeInit(instance, module) { + wasm = instance.exports; + init.__wbindgen_wasm_module = module; + cachedInt32Memory0 = new Int32Array(); + cachedUint8Memory0 = new Uint8Array(); + + + return wasm; +} + +function initSync(module) { + const imports = getImports(); + + initMemory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return finalizeInit(instance, module); +} + +async function init(input) { + if (typeof input === 'undefined') { + input = new URL('gpconv_bg.wasm', import.meta.url); + } + const imports = getImports(); + + if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { + input = fetch(input); + } + + initMemory(imports); + + const { instance, module } = await load(await input, imports); + + return finalizeInit(instance, module); +} + +export { initSync } +export default init; diff --git a/apps/gipy/pkg/gpconv_bg.wasm b/apps/gipy/pkg/gpconv_bg.wasm new file mode 100644 index 000000000..edeb4eb59 Binary files /dev/null and b/apps/gipy/pkg/gpconv_bg.wasm differ diff --git a/apps/gipy/pkg/gpconv_bg.wasm.d.ts b/apps/gipy/pkg/gpconv_bg.wasm.d.ts new file mode 100644 index 000000000..6bc5d3719 --- /dev/null +++ b/apps/gipy/pkg/gpconv_bg.wasm.d.ts @@ -0,0 +1,16 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export function __wbg_gpcsvg_free(a: number): void; +export function get_gpc(a: number, b: number): void; +export function get_svg(a: number, b: number): void; +export function convert_gpx_strings_no_osm(a: number, b: number): number; +export function convert_gpx_strings(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, r: number): number; +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__h0601691a32604cdd(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__h25ed812378167476(a: number, b: number, c: number, d: number): void; diff --git a/apps/gipy/pkg/package.json b/apps/gipy/pkg/package.json new file mode 100644 index 000000000..dee41f5cc --- /dev/null +++ b/apps/gipy/pkg/package.json @@ -0,0 +1,12 @@ +{ + "name": "gpconv", + "version": "0.1.0", + "files": [ + "gpconv_bg.wasm", + "gpconv.js", + "gpconv.d.ts" + ], + "module": "gpconv.js", + "types": "gpconv.d.ts", + "sideEffects": false +} \ No newline at end of file diff --git a/apps/gipy/screenshot1.png b/apps/gipy/screenshot1.png new file mode 100644 index 000000000..c7c45fa3b Binary files /dev/null and b/apps/gipy/screenshot1.png differ diff --git a/apps/gipy/screenshot2.png b/apps/gipy/screenshot2.png new file mode 100644 index 000000000..ed61eb795 Binary files /dev/null and b/apps/gipy/screenshot2.png differ diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js new file mode 100644 index 000000000..af9cbef22 --- /dev/null +++ b/apps/gipy/settings.js @@ -0,0 +1,38 @@ +(function (back) { + var FILE = "gipy.json"; + // Load settings + var settings = Object.assign( + { + keep_gps_alive: false, + max_speed: 35, + }, + require("Storage").readJSON(FILE, true) || {} + ); + + function writeSettings() { + require("Storage").writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "": { title: "Gipy" }, + "< Back": () => back(), + "keep gps alive": { + value: !!settings.keep_gps_alive, // !! converts undefined to false + format: (v) => (v ? "Yes" : "No"), + onchange: (v) => { + settings.keep_gps_alive = v; + writeSettings(); + }, + }, + "max speed": { + value: 35 | settings.max_speed, // 0| converts undefined to 0 + min: 0, + max: 130, + onchange: (v) => { + settings.max_speed = v; + writeSettings(); + }, + }, + }); +}); diff --git a/tsconfig.json b/tsconfig.json index db3db1fc3..8da08b8e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,9 @@ "strict": true, "typeRoots": ["./typescript/types"] }, - "include": ["./**/*"] + "include": ["./**/*"], + "exclude": [ + "**/gpconv.d.ts", + "**/gpconv_bg.wasm.d.ts" + ] }