Implement Waypointer Moto
|
|
@ -1,176 +1,158 @@
|
||||||
# Waypointer - navigate to waypoints
|
# Waypointer Moto
|
||||||
|
|
||||||
The app is aimed at navigation whilst walking. Please note that it
|
Waypointer Moto is a GPS navigation aid intended to be attached to
|
||||||
would be foolish in the extreme to rely on this as your only
|
the handlebars of a motorcycle.
|
||||||
navigation aid!
|
It uses the GPS to find out which direction it's
|
||||||
|
travelling and shows the direction and distance to the destination
|
||||||
|
"as the crow flies". It gives you an indication of where to go,
|
||||||
|
but exploring and navigating the environment is left up to the user.
|
||||||
|
|
||||||
Please refer to the section on calibration of the compass. This
|

|
||||||
should be done each time the app is going to be used.
|
|
||||||
|
|
||||||
The main part of the display is a compass arrow that points in the
|
(Please note that it would be foolish in the extreme to rely on this
|
||||||
direction you need to walk in. Once you have selected a waypoint a
|
as your only navigation aid! Make sure you read this entire document
|
||||||
bearing from your current position (received from a GPS fix) is
|
before using the app for navigation so that you know the drawbacks
|
||||||
calculated and the compass is set to point in that direction. If the
|
and shortcomings.)
|
||||||
arrow is pointing to the left, turning left should straighten the arrow
|
|
||||||
up so that it is pointing straight ahead.
|
|
||||||
|
|
||||||
|
## App usage
|
||||||
|
|
||||||

|
### Main screen
|
||||||
|
|
||||||
The large digits are the bearing from the current position. On the
|

|
||||||
left is the distance to the waypoint in local units. The top of the
|
|
||||||
display is a circular compass which displays the direction you will
|
|
||||||
need to travel in to reach the selected waypoint. The blue text is
|
|
||||||
the name of the current waypoint. NONE means that there is no
|
|
||||||
waypoint set and so bearing and distance will remain at 0. To select
|
|
||||||
a waypoint, press BTN2 (middle) and wait for the blue text to turn
|
|
||||||
white. Then use BTN1 and BTN3 to select a waypoint. The waypoint
|
|
||||||
choice is fixed by pressing BTN2 again. In the screen shot below a
|
|
||||||
waypoint giving the location of Stone Henge has been selected.
|
|
||||||
|
|
||||||
The screenshot above shows that Stone Henge is 259.9 miles from the
|
The main screen shows the direction arrow, the distance to the waypoint,
|
||||||
current location. To travel towards Stone Henge I need to turn
|
and the name of the selected waypoint.
|
||||||
slightly left until the arrow is pointing straight ahead. As you
|
|
||||||
continue to walk in the pointed direction you should see the distance
|
|
||||||
to the waypoint reduce. The frequency of updates will depend on
|
|
||||||
which settings you have used in the GPS.
|
|
||||||
|
|
||||||
At the top of the screen you can see two widgets. These are the [GPS
|
It also shows the status of the GPS fix in the colour of the arrow:
|
||||||
Power
|
|
||||||
Widget](https://github.com/espruino/BangleApps/tree/master/apps/widgps)
|
|
||||||
and the [Compass Power Indicator Widget]. These can be installed
|
|
||||||
seperately and provide you a indication of when the GPS and Compass
|
|
||||||
are switched on and drawing power.
|
|
||||||
|
|
||||||
|
* Red: no GPS fix at all
|
||||||
|
* Yellow: GPS location, but no GPS course (probably you're moving too slowly);
|
||||||
|
in this case the direction of travel comes from the compass bearing instead
|
||||||
|
of the GPS course, but note that the compass is unreliable
|
||||||
|
* White: GPS fix includes both location and course, and the GPS course is used
|
||||||
|
to determine the direction of travel
|
||||||
|
|
||||||
## Marking Waypoints
|
### Select a waypoint
|
||||||
|
|
||||||
The app lets you mark your current location as follows. There are
|

|
||||||
vacant slots in the waypoint file which can be allocated a
|
|
||||||
location. In the distributed waypoint file these are labelled WP0 to
|
|
||||||
WP4. Select one of these - WP2 is shown below.
|
|
||||||
|
|
||||||

|
Press the middle button (`BTN2`) to enter the menu, choose a waypoint using
|
||||||
|
the up/down arrows, and use the middle button again to select a waypoint and
|
||||||
|
return to the main screen.
|
||||||
|
|
||||||
Bearing and distance are both zero as WP2 has currently no GPS
|
### Add a waypoint
|
||||||
location associated with it. To mark the location, press BTN2.
|
|
||||||
|
|
||||||

|
Press the middle button (`BTN2`) to enter the menu, and select the "+ Here"
|
||||||
|
option. This will add a waypoint named "WP*n*" marking your current location,
|
||||||
|
where "*n*" is the next unused number.
|
||||||
|
|
||||||
The app indicates that WP2 is now marked by adding the prefix @ to
|
### Delete a waypoint
|
||||||
it's name. The distance should be small as shown in the screen shot
|
|
||||||
as you have just marked your current location.
|
|
||||||
|
|
||||||
## Waypoint JSON file
|

|
||||||
|
|
||||||
When the app is loaded from the app loader, a file named
|
Select a waypoint using the menu. Once the waypoint is selected and you're
|
||||||
`waypoints.json` is loaded along with the javascript etc. The file
|
back on the main screen, press either the top or bottom button (`BTN1` or
|
||||||
has the following contents:
|
`BTN3`). Confirm that you want to delete the waypoint with the middle
|
||||||
|
button (`BTN2`).
|
||||||
|
|
||||||
|
## Waypoint editor
|
||||||
|
|
||||||
```
|
With the Bangle.js app loader connected to the watch, find the
|
||||||
[
|
Waypointer Moto app and click on the floppy disk icon:
|
||||||
{
|
|
||||||
"name":"NONE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"No10",
|
|
||||||
"lat":51.5032,
|
|
||||||
"lon":-0.1269
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Stone",
|
|
||||||
"lat":51.1788,
|
|
||||||
"lon":-1.8260
|
|
||||||
},
|
|
||||||
{ "name":"WP0" },
|
|
||||||
{ "name":"WP1" },
|
|
||||||
{ "name":"WP2" },
|
|
||||||
{ "name":"WP3" },
|
|
||||||
{ "name":"WP4" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
The file contains the initial NONE waypoint which is useful if you
|

|
||||||
just want to display course and speed. The next two entries are
|
|
||||||
waypoints to No 10 Downing Street and to Stone Henge - obtained from
|
|
||||||
Google Maps. The last five entries are entries which can be *marked*.
|
|
||||||
|
|
||||||
You add and delete entries using the Web IDE to load and then save
|
This will load up the waypoint editor:
|
||||||
the file from and to watch storage. The app itself does not limit the
|
|
||||||
number of entries although it does load the entire file into RAM
|
|
||||||
which will obviously limit this.
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Waypoint Editor
|
### Add a waypoint
|
||||||
|
|
||||||
Clicking on the download icon of gpsnav in the app loader invokes the
|
Use the map to find your destination. Clicking on the map will
|
||||||
waypoint editor. The editor downloads and displays the current
|
populate the latitude/longitude input boxes with the coordinates
|
||||||
`waypoints.json` file. Clicking the `Edit` button beside an entry
|
of the point you clicked on. Type in a name for the waypoint and
|
||||||
causes the entry to be deleted from the list and displayed in the
|
click "Add Waypoint". Click "Upload" to send the updated list of
|
||||||
edit boxes. It can be restored - by clicking the `Add waypoint`
|
waypoints to the watch.
|
||||||
button. A new markable entry is created by using the `Add name`
|
|
||||||
button. The edited `waypoints.json` file is uploaded to the Bangle by
|
|
||||||
clicking the `Upload` button.
|
|
||||||
|
|
||||||
|
### Edit a waypoint
|
||||||
|
|
||||||
## Calibration of the Compass
|
Click on the pencil icon next to the waypoint you wish to edit.
|
||||||
|
This will remove the waypoint from the list and populate the
|
||||||
|
input boxes.
|
||||||
|
Edit the coordinates by hand, or by clicking on the map. Edit
|
||||||
|
the name if you want. Click "Add Waypoint" to save the waypoint
|
||||||
|
back to the list. Click "Upload" to send the updated list of
|
||||||
|
waypoints to the watch.
|
||||||
|
|
||||||
The Compass should be calibrated before using the App to navigate to
|
### Delete a waypoint
|
||||||
a waypoint (or a series of waypoints). To do this use either the
|
|
||||||
Arrow Compass or the [Navigation
|
|
||||||
Compass](https://github.com/espruino/BangleApps/tree/master/apps/magnav).
|
|
||||||
Open the compass app and clicking on BTN3. The calibration process
|
|
||||||
takes 30 seconds during which you should move the watch slowly
|
|
||||||
through figures of 8. It is important that during calibration the
|
|
||||||
watch is fully rotated around each of it axes. If the app does give
|
|
||||||
the correct direction heading or is not stable with respect to tilt
|
|
||||||
and roll - redo the calibration by pressing *BTN3*. Calibration data
|
|
||||||
is recorded in a storage file named `magnav.json`.
|
|
||||||
|
|
||||||
|
Click on the pencil icon next to the waypoint you wish to edit.
|
||||||
|
This will remove the waypoint from the list.
|
||||||
|
Click "Upload" to send the updated list of waypoints to the watch.
|
||||||
|
|
||||||
## Advantages and Disadvantages
|
## Mounting the watch on the bike
|
||||||
|
|
||||||
This approach has some advantages and disadvantages. First following
|
There is a 3d-printable "artificial wrist" which will fit over a 7/8"
|
||||||
the arrow is fairly easy to do and once the bearing has been
|
handlebar and allow the watch strap to tighten up.
|
||||||
established it does not matter if there is not another GPS fix for a
|
Alternatively, in a pinch you can strap the watch around a glove or a sponge
|
||||||
while as the compass will continue to point in the general direction.
|
or anything else that will pad out the space so that the watch is a tight
|
||||||
Second the GPS will only supply a course to the waypoint (a bearing)
|
fit.
|
||||||
once you are travelling above 8m/s or 28kph. This is not a practical
|
|
||||||
walking speed. 5kmph is considered a marching pace.
|
|
||||||
|
|
||||||
One disadvantage is that the compass is not very accurate. I have
|
The 3d-printed part should be a snug fit on the handlebar so that it does
|
||||||
observed it being 20-30 degrees off when compared to a hiking
|
not flop around. If it is too loose, line it with a layer or 2 of tape.
|
||||||
compass. Sometime its is necessary to walk in the opposite direction
|
|
||||||
for a bit to establish the correct direction to go in. The accuracy
|
|
||||||
of the compass is impacted by the magnetic clamps on the charging
|
|
||||||
cable, so it is particularly important that you recalibtrate the
|
|
||||||
compass after the watch has been charged. That said I have found I
|
|
||||||
am successfully able to follow a chain of waypoints as a route.
|
|
||||||
|
|
||||||
|
[Download the handlebar mount STL »](handlebar-mount.stl)
|
||||||
|
|
||||||
|
[Download the handlebar mount FreeCAD source »](handlebar-mount.FCStd)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Comparison to Way Pointer
|
||||||
|
|
||||||
|
Compared to the original Way Pointer app, Waypointer Moto:
|
||||||
|
|
||||||
|
* removes the numerical display of compass bearing
|
||||||
|
* makes the distance text bigger
|
||||||
|
* uses a higher-resolution arrow icon
|
||||||
|
* has a visual indication of the GPS status (the arrow colour)
|
||||||
|
* uses GPS course instead of compass bearing
|
||||||
|
* has OpenStreetMap integration in the waypoint editor
|
||||||
|
* uses Bangle.js menus to select waypoints instead of custom UI
|
||||||
|
* can add new waypoints from inside the app without requiring a blank slot
|
||||||
|
* can delete waypoints from inside the app without needing the PC
|
||||||
|
* still uses the same `waypoints.json` file
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
Waypointer Moto derives your current heading from the GPS course
|
||||||
|
rather than the compass, whenever GPS course is available.
|
||||||
|
The compass bearing is based on the angle the watch is held, but
|
||||||
|
the GPS course is based on the direction it's *travelling*. If the
|
||||||
|
watch is not aligned with the direction of travel of the vehicle
|
||||||
|
then the arrow will not point in the correct direction.
|
||||||
|
|
||||||
|
When travelling too slowly, there is no GPS course information, so the
|
||||||
|
app reverts to using the compass (and draws it in yellow), but
|
||||||
|
the compass is not very reliable, and I
|
||||||
|
have especially found it not to be reliable when placed on a motorcyle,
|
||||||
|
maybe because of all the metal in the immediate vicinity. So if
|
||||||
|
the arrow is not drawn in white, then you should probably not trust
|
||||||
|
it. If you're not sure, just ride in a straight line until the arrow
|
||||||
|
turns white again.
|
||||||
|
|
||||||
## Possible Future Enhancements
|
## Possible Future Enhancements
|
||||||
|
|
||||||
- Buzz when the GPS establishes its first fix.
|
- "routes" with multiple waypoints; automatically step from one
|
||||||
|
waypoint to the next when you get near to it
|
||||||
- Add a small LED to show the status of the GPS during the phase of
|
- some way to manually input coordinates directly on the watch
|
||||||
establishing a first fix.
|
- make the text & arrow more legible in direct sunlight
|
||||||
|
- integrate a charging connector into the handlebar mount
|
||||||
- Add an option to calibrate the Compass without having to use the
|
- upstream the map integration to the other waypoint apps
|
||||||
Arrow Compass or the Navigation Compass.
|
|
||||||
|
|
||||||
- Investigate the accuracy of the Compass and how it changes
|
|
||||||
throughout the day after the watch battery has been fully charged.
|
|
||||||
|
|
||||||
- Investigate the possibility of setting the GPS in low speed mode so
|
|
||||||
that a current course value can be obtained.
|
|
||||||
|
|
||||||
- Buzz when you arrive within 20m of a waypoint to signify arrival
|
|
||||||
|
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
The majority of the code in this application is a merge of
|
Waypointer Moto is a project by [James Stanley](https://incoherency.co.uk/). It is a derivative of [Adam Schmalhofer's](https://github.com/adamschmalhofer) Way Pointer app, which is in turn a derivative of
|
||||||
[jeffmer's](https://github.com/jeffmer/JeffsBangleAppsDev) GPS
|
[jeffmer's](https://github.com/jeffmer/JeffsBangleAppsDev) GPS
|
||||||
Navigation and Compass Navigation Applications.
|
Navigation and Compass Navigation apps.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,106 @@
|
||||||
var pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow
|
|
||||||
var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white
|
|
||||||
var pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue
|
|
||||||
|
|
||||||
// having 3 2 color pallette keeps the memory requirement lower
|
|
||||||
var buf1 = Graphics.createArrayBuffer(160,160,1, {msb:true});
|
|
||||||
var buf2 = Graphics.createArrayBuffer(80,40,1, {msb:true});
|
|
||||||
var arrow_img = require("heatshrink").decompress(atob("lEowIPMjAEDngEDvwED/4DCgP/wAEBgf/4AEBg//8AEBh//+AEBj///AEBn///gEBv///wmCAAImCAAIoBFggE/AkaaEABo="));
|
|
||||||
|
|
||||||
function flip1(x,y) {
|
|
||||||
g.drawImage({width:160,height:160,bpp:1,buffer:buf1.buffer, palette:pal_by},x,y);
|
|
||||||
buf1.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function flip2_bw(x,y) {
|
|
||||||
g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bw},x,y);
|
|
||||||
buf2.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function flip2_bb(x,y) {
|
|
||||||
g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bb},x,y);
|
|
||||||
buf2.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
var candraw = true;
|
|
||||||
var wp_bearing = 0;
|
|
||||||
var direction = 0;
|
|
||||||
var wpindex=0;
|
|
||||||
var loc = require("locale");
|
var loc = require("locale");
|
||||||
var selected = false;
|
|
||||||
|
var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
|
||||||
|
var wp = waypoints[0];
|
||||||
|
var wp_bearing = 0;
|
||||||
|
var candraw = true;
|
||||||
|
|
||||||
|
var direction = 0;
|
||||||
|
var dist = 0;
|
||||||
|
|
||||||
|
var savedfix;
|
||||||
|
|
||||||
var previous = {
|
var previous = {
|
||||||
bs: '',
|
|
||||||
dst: '',
|
dst: '',
|
||||||
wp_name: '',
|
wp_name: '',
|
||||||
course: 0,
|
course: 180,
|
||||||
selected: false,
|
selected: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// clear the attributes that control the display refresh
|
/*** Drawing ***/
|
||||||
function clear_previous() {
|
|
||||||
previous.bs = '-';
|
var pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow
|
||||||
previous.dst = '-';
|
var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white
|
||||||
previous.wp_name = '-';
|
var pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue
|
||||||
previous.course = -999;
|
var pal_br = new Uint16Array([0x0000,0xf800],0,1); // black, red
|
||||||
|
var pal_compass = pal_by;
|
||||||
|
|
||||||
|
var buf = Graphics.createArrayBuffer(160,160,1, {msb:true});
|
||||||
|
var arrow_img = require("heatshrink").decompress(atob("vF4wJC/AEMYBxs8Bxt+Bxv/BpkB/+ABxcD//ABxcH//gBxcP//wBxcf//4Bxc///8Bxd///+OxgABOxgABPBR2BAAJ4KOwIABPBR2BAAJ4KOwIABPBR2BAAJ4KOwIABPBQNCPBR2DPBR2DPBR2DPBR2DPBR2DPBR2DPBR2DPBQNEPBB2FPBB2FPBB2FPBB2FPBB2FPBB2FPBB2FPBANGPAx2HPAx2HPAx2HPAx2HPAx2HPAx2HeJTeJB34O/B34O/B34O/B34O/B34O/B34O/B34O/B34OTAH4AT"));
|
||||||
|
|
||||||
|
function flip1(x,y,palette) {
|
||||||
|
g.drawImage({width:160,height:160,bpp:1,buffer:buf.buffer, palette:palette},x,y);
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function flip2_bw(x,y) {
|
||||||
|
g.drawImage({width:160,height:40,bpp:1,buffer:buf.buffer, palette:pal_bw},x,y);
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function flip2_bb(x,y) {
|
||||||
|
g.drawImage({width:160,height:40,bpp:1,buffer:buf.buffer, palette:pal_bb},x,y);
|
||||||
|
buf.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCompass(course) {
|
function drawCompass(course) {
|
||||||
if(!candraw) return;
|
if (!candraw) return;
|
||||||
if (Math.abs(previous.course - course) < 9) return; // reduce number of draws due to compass jitter
|
|
||||||
previous.course = course;
|
previous.course = course;
|
||||||
|
|
||||||
buf1.setColor(1);
|
buf.setColor(1);
|
||||||
buf1.fillCircle(80,80,79,79);
|
buf.fillCircle(80,80, 79);
|
||||||
buf1.setColor(0);
|
buf.setColor(0);
|
||||||
buf1.fillCircle(80,80,69,69);
|
buf.fillCircle(80,80, 69);
|
||||||
buf1.setColor(1);
|
buf.setColor(1);
|
||||||
buf1.drawImage(arrow_img, 80, 80, {scale:3, rotate:radians(course)} );
|
buf.drawImage(arrow_img, 80, 80, {rotate:radians(course)} );
|
||||||
flip1(40, 30);
|
var palette = pal_br;
|
||||||
|
if (savedfix !== undefined && savedfix.fix !== 0) palette = pal_compass;
|
||||||
|
flip1(40, 30, palette);
|
||||||
}
|
}
|
||||||
|
|
||||||
/***** COMPASS CODE ***********/
|
function drawN(force){
|
||||||
|
if (!candraw) return;
|
||||||
|
|
||||||
|
buf.setFont("Vector",24);
|
||||||
|
var dst = loc.distance(dist);
|
||||||
|
|
||||||
|
// distance on left
|
||||||
|
if (force || previous.dst !== dst) {
|
||||||
|
previous.dst = dst;
|
||||||
|
buf.setColor(1);
|
||||||
|
buf.setFontAlign(-1, -1);
|
||||||
|
buf.setFont("Vector",40);
|
||||||
|
buf.drawString(dst,0,0);
|
||||||
|
flip2_bw(8, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// waypoint name on right
|
||||||
|
if (force || previous.wp_name !== wp.name) {
|
||||||
|
previous.wp_name = wp.name;
|
||||||
|
buf.setColor(1);
|
||||||
|
buf.setFontAlign(1, -1);
|
||||||
|
buf.setFont("Vector", 15);
|
||||||
|
buf.drawString(wp.name, 80, 0);
|
||||||
|
flip2_bw(160, 220);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawAll(force) {
|
||||||
|
if (!candraw) return;
|
||||||
|
|
||||||
|
g.setColor(1,1,1);
|
||||||
|
drawN(force);
|
||||||
|
drawCompass(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Heading ***/
|
||||||
|
|
||||||
var heading = 0;
|
var heading = 0;
|
||||||
function newHeading(m,h){
|
function newHeading(m,h){
|
||||||
var s = Math.abs(m - h);
|
var s = Math.abs(m - h);
|
||||||
var delta = (m>h)?1:-1;
|
var delta = (m>h)?1:-1;
|
||||||
if (s>=180){s=360-s; delta = -delta;}
|
if (s>=180){s=360-s; delta = -delta;}
|
||||||
if (s<2) return h;
|
if (s<2) return h;
|
||||||
var hd = h + delta*(1 + Math.round(s/5));
|
var hd = h + delta*(1 + Math.round(s/5));
|
||||||
if (hd<0) hd+=360;
|
if (hd<0) hd+=360;
|
||||||
|
|
@ -93,10 +128,18 @@ function tiltfixread(O,S){
|
||||||
return psi;
|
return psi;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note actual mag is 360-m, error in firmware
|
function read_heading() {
|
||||||
function read_compass() {
|
if (savedfix !== undefined && !isNaN(savedfix.course)) {
|
||||||
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
|
Bangle.setCompassPower(0);
|
||||||
heading = newHeading(d,heading);
|
heading = savedfix.course;
|
||||||
|
pal_compass = pal_bw;
|
||||||
|
} else {
|
||||||
|
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
|
||||||
|
Bangle.setCompassPower(1);
|
||||||
|
heading = newHeading(d,heading);
|
||||||
|
pal_compass = pal_by;
|
||||||
|
}
|
||||||
|
|
||||||
direction = wp_bearing - heading;
|
direction = wp_bearing - heading;
|
||||||
if (direction < 0) direction += 360;
|
if (direction < 0) direction += 360;
|
||||||
if (direction > 360) direction -= 360;
|
if (direction > 360) direction -= 360;
|
||||||
|
|
@ -104,12 +147,7 @@ function read_compass() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/***** END Compass ***********/
|
/*** Maths ***/
|
||||||
|
|
||||||
var speed = 0;
|
|
||||||
var satellites = 0;
|
|
||||||
var wp;
|
|
||||||
var dist=0;
|
|
||||||
|
|
||||||
function radians(a) {
|
function radians(a) {
|
||||||
return a*Math.PI/180;
|
return a*Math.PI/180;
|
||||||
|
|
@ -125,8 +163,7 @@ function bearing(a,b){
|
||||||
var alat = radians(a.lat);
|
var alat = radians(a.lat);
|
||||||
var blat = radians(b.lat);
|
var blat = radians(b.lat);
|
||||||
var y = Math.sin(delta) * Math.cos(blat);
|
var y = Math.sin(delta) * Math.cos(blat);
|
||||||
var x = Math.cos(alat)*Math.sin(blat) -
|
var x = Math.cos(alat)*Math.sin(blat) - Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||||
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
|
||||||
return Math.round(degrees(Math.atan2(y, x)));
|
return Math.round(degrees(Math.atan2(y, x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,133 +173,106 @@ function distance(a,b){
|
||||||
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
|
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** Waypoints ***/
|
||||||
|
|
||||||
function drawN(){
|
function addCurrentWaypoint() {
|
||||||
buf2.setFont("Vector",24);
|
var wpnum = 0;
|
||||||
var bs = wp_bearing.toString();
|
var ok = false;
|
||||||
bs = wp_bearing<10?"00"+bs : wp_bearing<100 ?"0"+bs : bs;
|
// XXX: O(n^2) search for lowest unused WP number
|
||||||
var dst = loc.distance(dist);
|
while (!ok) {
|
||||||
|
ok = true;
|
||||||
// -1=left (default), 0=center, 1=right
|
for (var i = 0; i < waypoints.length && ok; i++) {
|
||||||
|
if (waypoints[i].name == ("WP"+wpnum)) {
|
||||||
// show distance on the left
|
wpnum++;
|
||||||
if (previous.dst !== dst) {
|
ok = false;
|
||||||
previous.dst = dst
|
}
|
||||||
buf2.setColor(1);
|
}
|
||||||
buf2.setFontAlign(-1,-1);
|
|
||||||
buf2.setFont("Vector", 20);
|
|
||||||
buf2.drawString(dst,0,0);
|
|
||||||
flip2_bw(0, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// bearing, place in middle at bottom of compass
|
|
||||||
if (previous.bs !== bs) {
|
|
||||||
previous.bs = bs;
|
|
||||||
buf2.setColor(1);
|
|
||||||
buf2.setFontAlign(0, -1);
|
|
||||||
buf2.setFont("Vector",38);
|
|
||||||
buf2.drawString(bs,40,0);
|
|
||||||
flip2_bw(80, 200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// waypoint name on right
|
waypoints.push({
|
||||||
if (previous.wp_name !== wp.name || previous.selected !== selected) {
|
name: "WP" + wpnum,
|
||||||
previous.selected = selected;
|
lat: savedfix.lat,
|
||||||
buf2.setColor(1);
|
lon: savedfix.lon,
|
||||||
buf2.setFontAlign(1,-1); // right, bottom
|
});
|
||||||
buf2.setFont("Vector", 20);
|
wp = waypoints[waypoints.length-1];
|
||||||
buf2.drawString(wp.name, 80, 0);
|
saveWaypoints();
|
||||||
|
|
||||||
if (selected)
|
|
||||||
flip2_bw(160, 200);
|
|
||||||
else
|
|
||||||
flip2_bb(160, 200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var savedfix;
|
function saveWaypoints() {
|
||||||
|
require("Storage").writeJSON("waypoints.json", waypoints);
|
||||||
|
}
|
||||||
|
|
||||||
function onGPS(fix) {
|
function deleteWaypoint(w) {
|
||||||
savedfix = fix;
|
for (var i = 0; i < waypoints.length; i++) {
|
||||||
if (fix!==undefined){
|
if (waypoints[i] == w) {
|
||||||
satellites = fix.satellites;
|
waypoints.splice(i, 1);
|
||||||
}
|
saveWaypoints();
|
||||||
|
wp = {name:"NONE"};
|
||||||
if (candraw) {
|
|
||||||
if (fix!==undefined && fix.fix==1){
|
|
||||||
dist = distance(fix,wp);
|
|
||||||
if (isNaN(dist)) dist = 0;
|
|
||||||
wp_bearing = bearing(fix,wp);
|
|
||||||
if (isNaN(wp_bearing)) wp_bearing = 0;
|
|
||||||
drawN();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var intervalRef;
|
/*** Setup ***/
|
||||||
|
|
||||||
function stopdraw() {
|
function onGPS(fix) {
|
||||||
candraw=false;
|
savedfix = fix;
|
||||||
prev_course = -1;
|
|
||||||
if(intervalRef) {clearInterval(intervalRef);}
|
if (fix !== undefined && fix.fix == 1){
|
||||||
|
dist = distance(fix, wp);
|
||||||
|
if (isNaN(dist)) dist = 0;
|
||||||
|
wp_bearing = bearing(fix, wp);
|
||||||
|
if (isNaN(wp_bearing)) wp_bearing = 0;
|
||||||
|
drawN();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTimers() {
|
function startTimers() {
|
||||||
candraw=true;
|
setInterval(function() {
|
||||||
intervalRefSec = setInterval(function() {
|
Bangle.setLCDPower(1);
|
||||||
read_compass();
|
read_heading();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawAll(){
|
function addWaypointToMenu(menu, i) {
|
||||||
g.setColor(1,1,1);
|
menu[waypoints[i].name] = function() {
|
||||||
drawN();
|
wp = waypoints[i];
|
||||||
drawCompass(direction);
|
mainScreen();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function startdraw(){
|
function mainScreen() {
|
||||||
g.clear();
|
E.showMenu();
|
||||||
Bangle.drawWidgets();
|
candraw = true;
|
||||||
startTimers();
|
drawAll(true);
|
||||||
candraw=true;
|
|
||||||
drawAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setButtons(){
|
Bangle.setUI("updown", function(v) {
|
||||||
setWatch(nextwp.bind(null,-1), BTN1, {repeat:true,edge:"falling"});
|
if (v === undefined) {
|
||||||
setWatch(doselect, BTN2, {repeat:true,edge:"falling"});
|
candraw = false;
|
||||||
setWatch(nextwp.bind(null,1), BTN3, {repeat:true,edge:"falling"});
|
var menu = {
|
||||||
}
|
"": { "title": "-- Waypoints --" },
|
||||||
|
};
|
||||||
Bangle.on('lcdPower',function(on) {
|
for (let i = 0; i < waypoints.length; i++) {
|
||||||
if (on) {
|
addWaypointToMenu(menu, i);
|
||||||
clear_previous();
|
}
|
||||||
startdraw();
|
menu["+ Here"] = function() {
|
||||||
} else {
|
addCurrentWaypoint();
|
||||||
stopdraw();
|
mainScreen();
|
||||||
}
|
};
|
||||||
});
|
menu["< Back"] = mainScreen;
|
||||||
|
E.showMenu(menu);
|
||||||
var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
|
} else {
|
||||||
wp=waypoints[0];
|
candraw = false;
|
||||||
|
E.showPrompt("Delete waypoint: " + wp.name + "?").then(function(confirmed) {
|
||||||
function nextwp(inc){
|
var name = wp.name;
|
||||||
if (!selected) return;
|
if (confirmed) {
|
||||||
wpindex+=inc;
|
deleteWaypoint(wp);
|
||||||
if (wpindex>=waypoints.length) wpindex=0;
|
E.showAlert("Waypoint deleted: " + name).then(mainScreen);
|
||||||
if (wpindex<0) wpindex = waypoints.length-1;
|
} else {
|
||||||
wp = waypoints[wpindex];
|
mainScreen();
|
||||||
drawN();
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
function doselect(){
|
});
|
||||||
if (selected && wpindex!=0 && waypoints[wpindex].lat===undefined && savedfix.fix) {
|
|
||||||
waypoints[wpindex] ={name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon};
|
|
||||||
wp = waypoints[wpindex];
|
|
||||||
require("Storage").writeJSON("waypoints.json", waypoints);
|
|
||||||
}
|
|
||||||
selected=!selected;
|
|
||||||
drawN();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bangle.on('kill',()=>{
|
Bangle.on('kill',()=>{
|
||||||
|
|
@ -272,12 +282,7 @@ Bangle.on('kill',()=>{
|
||||||
|
|
||||||
g.clear();
|
g.clear();
|
||||||
Bangle.setLCDBrightness(1);
|
Bangle.setLCDBrightness(1);
|
||||||
Bangle.loadWidgets();
|
|
||||||
Bangle.drawWidgets();
|
|
||||||
// load widgets can turn off GPS
|
|
||||||
Bangle.setGPSPower(1);
|
Bangle.setGPSPower(1);
|
||||||
Bangle.setCompassPower(1);
|
|
||||||
drawAll();
|
|
||||||
startTimers();
|
startTimers();
|
||||||
Bangle.on('GPS', onGPS);
|
Bangle.on('GPS', onGPS);
|
||||||
setButtons();
|
mainScreen();
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
|
@ -1 +1 @@
|
||||||
require("heatshrink").decompress(atob("mEwwhC/AFcBiAWViMRDCkBiUhC68RC64AFGxsRC4UiAAY2HOAQAEC4MSn//AAXzGAwWGC4czC4f/mIwEFwIlEBoIXDBQnyGAkRiYWE/8yLAIXBGAhgEFw5WBC4R0BkYaBmRfFF44XCNI6OGGAQlBAAIXIX4yPJaBq/JC5oeHC/4X/C/4X/C/4X/C/4X/C88RiIXUDAIWVAH4AVA="))
|
require("heatshrink").decompress(atob("mEw4kA///tVK/feuekkEh1dSnnn5P2imlgdr221vvv0E5x9z8dqoEpMf4AqgMQCysRiIYUgMd6IXVqc1iIXXAAo2NiIXBivdAAfVGwxwCAAgXBicmswACsVRGAsRqoAEqIXC0QXDs0lGAkBisrBomBC4WqGAloMIkRqW72wNDlvTC4QwEwIvDLoOr3e7KwkzmsdmczxAABwaQFF4QwEC4SAGR4pfBGAO2kQABlQXIX4wlHL4LQNX5IXNDw4XygdZ96MFF58NvP8C6Mzu93uF9+oXQrQWBAAPU/5kPgoWDu+UoIQJ7vR6IGCm4XEz9MC5HMAAvHC4mc4gWHk93zPsC5F58oWHgQOE//8//M//+5mf9vAFxFwOQef5nu9wbB5nOiNQBoInCCYMCk8FjnM7qkB5lEp3//udRoKlBuueDwMyC4MojnJz2dmEA/1EovDn3d6nEiEHv3pzOc4oXB1nJ0Wo7JVBjnE/td6IxBgtQu/u4ci1vs6EAj+S3eynvBgEP92Z/+XzIXBiF59GOte27goB5OrlGDlWcMAP3SgJvBusBgF/61sycrlPFgvJsWZzMrziGB84XDvlBg+excp9N73P1gbQB5//+lEiMAi93vwaBz4XC1ep5OS2f9gPJlWZ93d+lBqNfzn//N5+lAg+cF4OZF4UA52LlWi3RfBSANEAAVBgEHz+W3VzlepVANczW73ep+L4CgNTnrfBAAPc8drUAOcR4MB/PYwefAwIXCjvRdgIABgvO7Ui1PMX4MA5n+96+BC4cRiIXDKAPOzPvIwIABhsc4oPEDAIGFhqhB4K2BACY+EAH4AG"))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"id": "wpmoto",
|
||||||
|
"name": "Waypointer Moto",
|
||||||
|
"shortName": "Waypointer Moto",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Waypoint-based motorcycle navigation aid",
|
||||||
|
"icon": "wpmoto.png",
|
||||||
|
"tags": "tool,outdoors,gps",
|
||||||
|
"supports": ["BANGLEJS"],
|
||||||
|
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-menu.png"},{"url":"screenshot-delete.png"}],
|
||||||
|
"readme": "README.md",
|
||||||
|
"interface": "wpmoto.html",
|
||||||
|
"storage": [
|
||||||
|
{"name":"wpmoto.app.js","url":"app.js"},
|
||||||
|
{"name":"wpmoto.img","url":"icon.js","evaluate":true}
|
||||||
|
],
|
||||||
|
"data": [{"name":"waypoints.json","url":"waypoints.json"}]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
|
@ -2,19 +2,4 @@
|
||||||
{
|
{
|
||||||
"name":"NONE"
|
"name":"NONE"
|
||||||
},
|
},
|
||||||
{
|
]
|
||||||
"name":"No10",
|
|
||||||
"lat":51.5032,
|
|
||||||
"lon":-0.1269
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Stone",
|
|
||||||
"lat":51.1788,
|
|
||||||
"lon":-1.8260
|
|
||||||
},
|
|
||||||
{ "name":"WP0" },
|
|
||||||
{ "name":"WP1" },
|
|
||||||
{ "name":"WP2" },
|
|
||||||
{ "name":"WP3" },
|
|
||||||
{ "name":"WP4" }
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
|
@ -2,6 +2,8 @@
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||||
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.12.0/css/ol.css" type="text/css">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/ol-geocoder@latest/dist/ol-geocoder.min.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -30,13 +32,10 @@
|
||||||
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_latitude" placeholder="Lat">
|
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_latitude" placeholder="Lat">
|
||||||
</div>
|
</div>
|
||||||
<div class="column col-3 col-xs-8">
|
<div class="column col-3 col-xs-8">
|
||||||
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_longtitude" placeholder="Long">
|
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_longitude" placeholder="Long">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column col-3 col-xs-8">
|
|
||||||
<button id="add_name_button" class="btn btn-primary btn-sm">Add Name Only</button>
|
|
||||||
</div>
|
|
||||||
<div class="column col-3 col-xs-8">
|
<div class="column col-3 col-xs-8">
|
||||||
<button id="add_waypoint_button" class="btn btn-primary btn-sm">Add Waypoint</button>
|
<button id="add_waypoint_button" class="btn btn-primary btn-sm">Add Waypoint</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -44,21 +43,60 @@
|
||||||
</form>
|
</form>
|
||||||
<br>
|
<br>
|
||||||
<button id="Download" class="btn btn-error">Reload</button> <button id="Upload" class="btn btn-primary">Upload</button>
|
<button id="Download" class="btn btn-error">Reload</button> <button id="Upload" class="btn btn-primary">Upload</button>
|
||||||
|
<br>
|
||||||
|
<div id="map" class="map" style="width:100%; height:400px"></div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.12.0/build/ol.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/ol-geocoder"></script>
|
||||||
<script src="../../core/lib/interface.js"></script>
|
<script src="../../core/lib/interface.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
var map = new ol.Map({
|
||||||
|
target: 'map',
|
||||||
|
layers: [
|
||||||
|
new ol.layer.Tile({
|
||||||
|
source: new ol.source.OSM()
|
||||||
|
})
|
||||||
|
],
|
||||||
|
view: new ol.View({
|
||||||
|
center: ol.proj.fromLonLat([37.41, 8.82]),
|
||||||
|
zoom: 4
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
var geocoder = new Geocoder('nominatim', {
|
||||||
|
provider: 'osm',
|
||||||
|
lang: 'en-GB',
|
||||||
|
placeholder: 'Search...',
|
||||||
|
targetType: 'text-input',
|
||||||
|
});
|
||||||
|
map.addControl(geocoder);
|
||||||
|
geocoder.on('addresschosen', function(e) {
|
||||||
|
map.getView().animate({
|
||||||
|
center: e.coordinate,
|
||||||
|
zoom: Math.max(map.getView().getZoom(),16)
|
||||||
|
});
|
||||||
|
|
||||||
|
var lonlat = ol.proj.toLonLat(e.coordinate);
|
||||||
|
$longitude.value = lonlat[0];
|
||||||
|
$latitude.value = lonlat[1];
|
||||||
|
});
|
||||||
|
|
||||||
var waypoints = []
|
var waypoints = []
|
||||||
|
|
||||||
var $name = document.getElementById('add_waypoint_name')
|
var $name = document.getElementById('add_waypoint_name')
|
||||||
var $form = document.getElementById('add_waypoint_form')
|
var $form = document.getElementById('add_waypoint_form')
|
||||||
var $button = document.getElementById('add_waypoint_button')
|
var $button = document.getElementById('add_waypoint_button')
|
||||||
var $name_button = document.getElementById('add_name_button')
|
|
||||||
var $latitude = document.getElementById('add_latitude')
|
var $latitude = document.getElementById('add_latitude')
|
||||||
var $longtitude = document.getElementById('add_longtitude')
|
var $longitude = document.getElementById('add_longitude')
|
||||||
var $list = document.getElementById('waypoints')
|
var $list = document.getElementById('waypoints')
|
||||||
|
|
||||||
|
map.on('click', function(e) {
|
||||||
|
var lonlat = ol.proj.toLonLat(e.coordinate);
|
||||||
|
$longitude.value = lonlat[0];
|
||||||
|
$latitude.value = lonlat[1];
|
||||||
|
});
|
||||||
|
|
||||||
function compare(a, b){
|
function compare(a, b){
|
||||||
var x = a.name.toLowerCase();
|
var x = a.name.toLowerCase();
|
||||||
var y = b.name.toLowerCase();
|
var y = b.name.toLowerCase();
|
||||||
|
|
@ -73,8 +111,8 @@
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
var name = $name.value.trim()
|
var name = $name.value.trim()
|
||||||
if(!name) return;
|
if(!name) return;
|
||||||
var lat = parseFloat($latitude.value).toPrecision(5);
|
var lat = parseFloat($latitude.value).toPrecision(8);
|
||||||
var lon = parseFloat($longtitude.value).toPrecision(5);
|
var lon = parseFloat($longitude.value).toPrecision(8);
|
||||||
|
|
||||||
waypoints.push({
|
waypoints.push({
|
||||||
name, lat,lon,
|
name, lat,lon,
|
||||||
|
|
@ -84,31 +122,21 @@
|
||||||
|
|
||||||
renderWaypoints()
|
renderWaypoints()
|
||||||
$name.value = ''
|
$name.value = ''
|
||||||
$latitude.value = (0).toPrecision(5);
|
$latitude.value = (0).toPrecision(8);
|
||||||
$longtitude.value = (0).toPrecision(5);
|
$longitude.value = (0).toPrecision(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
$name_button.addEventListener('click', event => {
|
|
||||||
event.preventDefault()
|
|
||||||
var name = $name.value.trim()
|
|
||||||
if(!name) return;
|
|
||||||
|
|
||||||
waypoints.push({
|
|
||||||
name
|
|
||||||
});
|
|
||||||
waypoints.sort(compare);
|
|
||||||
|
|
||||||
renderWaypoints()
|
|
||||||
$name.value = ''
|
|
||||||
$latitude.value = 0.0000
|
|
||||||
$longtitude.value = 0.0000
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function removeWaypoint(index){
|
function removeWaypoint(index){
|
||||||
$name.value = waypoints[index].name
|
$name.value = waypoints[index].name
|
||||||
$latitude.value = waypoints[index].lat
|
if (waypoints[index].lat !== undefined && waypoints[index].lon !== undefined
|
||||||
$longtitude.value = waypoints[index].lon
|
&& !isNaN(waypoints[index].lat) && !isNaN(waypoints[index].lon)) {
|
||||||
|
$latitude.value = waypoints[index].lat
|
||||||
|
$longitude.value = waypoints[index].lon
|
||||||
|
map.getView().animate({
|
||||||
|
center: ol.proj.fromLonLat([waypoints[index].lon, waypoints[index].lat]),
|
||||||
|
zoom: Math.max(map.getView().getZoom(),16)
|
||||||
|
});
|
||||||
|
}
|
||||||
waypoints = waypoints.filter((p,i) => i!==index)
|
waypoints = waypoints.filter((p,i) => i!==index)
|
||||||
renderWaypoints()
|
renderWaypoints()
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 15 KiB |