Waypointer Moto: Add routes, support for Bangle.js 2, better editor UI

"Routes" are where you preconfigure several waypoints in order and it automatically
jumps from one to the next as you approach. There is a flaw in that if you
accidentally miss one waypoint on your route, there's no way to make it go on to
the next one without going all the way back. We probably want some way to manually
skip back/forth in the route.

Bangle.js 2 support is provided via hardcoded layout coordinates. This should
probably use the Layout library instead, but it'll probably do for now.

I've also replaced the table-based waypoint editor UI with an entirely map-based
replacement which I find much more intuitive.
pull/1431/head
James Stanley 2022-01-28 11:47:23 +00:00
parent 2f3a9c8c87
commit f24ec9404d
8 changed files with 481 additions and 295 deletions

View File

@ -52,7 +52,8 @@ where "*n*" is the next unused number.
Select a waypoint using the menu. Once the waypoint is selected and you're Select a waypoint using the menu. Once the waypoint is selected and you're
back on the main screen, press either the top or bottom button (`BTN1` or back on the main screen, press either the top or bottom button (`BTN1` or
`BTN3`). Confirm that you want to delete the waypoint with the middle `BTN3`), or, on Bangle.js 2, scroll the screen up or down.
Confirm that you want to delete the waypoint with the middle
button (`BTN2`). button (`BTN2`).
## Waypoint editor ## Waypoint editor
@ -68,27 +69,43 @@ This will load up the waypoint editor:
### Add a waypoint ### Add a waypoint
Use the map to find your destination. Clicking on the map will Click on the map to add a waypoint. You'll be prompted to give it
populate the latitude/longitude input boxes with the coordinates a name.
of the point you clicked on. Type in a name for the waypoint and
click "Add Waypoint". Click "Upload" to send the updated list of
waypoints to the watch.
### Edit a waypoint ### Edit a waypoint
Click on the pencil icon next to the waypoint you wish to edit. Click on the map marker of the waypoint you wish to edit. You
This will remove the waypoint from the list and populate the can then click on the blue pencil icon to edit the name of the
input boxes. waypoint. If you want to move the waypoint to a new location then
Edit the coordinates by hand, or by clicking on the map. Edit you need to delete it and re-add it.
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.
### Delete a waypoint ### Delete a waypoint
Click on the pencil icon next to the waypoint you wish to edit. Click on the map marker of the waypoint you wish to delete. You
This will remove the waypoint from the list. can then click on the red bin icon to delete the waypoint.
Click "Upload" to send the updated list of waypoints to the watch.
### Add a route
![](newroute.png)
Click on the map to place the first waypoint. The name of the first
waypoint will become the name of the route.
Click on the map marker for the new waypoint and click "Make route".
Now every time you click on the map it will add another point
on this route. When you're done either right click or click the
"Close route" button above the map.
Points along the route don't have names by default, but if you wish
to add one you can click on the waypoint and use the blue pencil icon
to give it a name.
![](editroute.png)
### Delete a route
Click on the map marker for any point on the route, and select
"Delete entire route".
## Mounting the watch on the bike ## Mounting the watch on the bike
@ -123,6 +140,7 @@ Compared to the original Way Pointer app, Waypointer Moto:
* can add new waypoints from inside the app without requiring a blank slot * can add new waypoints from inside the app without requiring a blank slot
* can delete waypoints from inside the app without needing the PC * can delete waypoints from inside the app without needing the PC
* still uses the same `waypoints.json` file * still uses the same `waypoints.json` file
* supports "routes" which automatically step from one waypoint to the next
## Gotchas ## Gotchas
@ -144,8 +162,6 @@ turns white again.
## Possible Future Enhancements ## Possible Future Enhancements
- "routes" with multiple waypoints; automatically step from one
waypoint to the next when you get near to it
- some way to manually input coordinates directly on the watch - some way to manually input coordinates directly on the watch
- make the text & arrow more legible in direct sunlight - make the text & arrow more legible in direct sunlight
- integrate a charging connector into the handlebar mount - integrate a charging connector into the handlebar mount

View File

@ -1,10 +1,15 @@
var loc = require("locale"); var loc = require("locale");
var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; var waypoints = require("Storage").readJSON("waypoints.json") || [];
var wp = waypoints[0]; var wp = waypoints[0];
if (wp == undefined) wp = {name:"NONE"};
var wp_bearing = 0; var wp_bearing = 0;
var routeidx = 0;
var candraw = true; var candraw = true;
const ROUTE_STEP = 50; // metres
const EPSILON = 1; // degrees
var direction = 0; var direction = 0;
var dist = 0; var dist = 0;
@ -15,138 +20,130 @@ var previous = {
wp_name: '', wp_name: '',
course: 180, course: 180,
selected: false, selected: false,
routeidx: -1,
}; };
/*** Drawing ***/ /*** Drawing ***/
var pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow var W = g.getWidth();
var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white var H = g.getHeight();
var pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue // layout (XXX: this should probably use the Layout library instead)
var pal_br = new Uint16Array([0x0000,0xf800],0,1); // black, red var L = { // banglejs1
var pal_compass = pal_by; arrow: {
x: 120,
y: 80,
r1: 79,
r2: 69,
bufh: 160,
bufy: 40,
},
text: {
bufh: 40,
bufy: 200,
largesize: 40,
smallsize: 15,
waypointy: 20,
},
};
if (W == 176) {
L = { // banglejs2
arrow: {
x: 88,
y: 70,
r1: 70,
r2: 62,
bufh: 160,
bufy: 0,
},
text: {
bufh: 40,
bufy: 142,
largesize: 40,
smallsize: 14,
waypointy: 20,
},
};
}
var buf = Graphics.createArrayBuffer(160,160,1, {msb:true}); var pal_by = new Uint16Array([0x0000,0xffc0],0,1); // black, yellow
var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white
var pal_br = new Uint16Array([0x0000,0xf800],0,1); // black, red
var buf = Graphics.createArrayBuffer(240,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")); 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) { function flip(y,h,palette) {
g.drawImage({width:160,height:160,bpp:1,buffer:buf.buffer, palette:palette},x,y); g.drawImage({width:240,height:h,bpp:1,buffer:buf.buffer, palette:palette},0,y);
buf.clear(); buf.clear();
} }
function flip2_bw(x,y) { function draw(force) {
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) {
if (!candraw) return; if (!candraw) return;
previous.course = course; var course = direction;
buf.setColor(1);
buf.fillCircle(80,80, 79);
buf.setColor(0);
buf.fillCircle(80,80, 69);
buf.setColor(1);
buf.drawImage(arrow_img, 80, 80, {rotate:radians(course)} );
var palette = pal_br;
if (savedfix !== undefined && savedfix.fix !== 0) palette = pal_compass;
flip1(40, 30, palette);
}
function drawN(force){
if (!candraw) return;
buf.setFont("Vector",24);
var dst = loc.distance(dist); var dst = loc.distance(dist);
if (force || previous.dst !== dst || previous.wp_name !== wp.name || previous.routeidx !== routeidx || Math.abs(course-previous.course)>EPSILON) {
previous.course = course;
var palette = pal_br;
if (savedfix !== undefined && savedfix.fix !== 0)
palette = isNaN(savedfix.course) ? pal_by : pal_bw;
buf.setColor(1);
buf.fillCircle(L.arrow.x,L.arrow.y, L.arrow.r1);
buf.setColor(0);
buf.fillCircle(L.arrow.x,L.arrow.y, L.arrow.r2);
buf.setColor(1);
buf.drawImage(arrow_img, L.arrow.x, L.arrow.y, {rotate:radians(course)} );
flip(L.arrow.bufy,L.arrow.bufh,palette);
// distance on left // distance on left
if (force || previous.dst !== dst) {
previous.dst = dst; previous.dst = dst;
previous.wp_name = wp.name;
previous.routeidx = routeidx;
buf.setColor(1); buf.setColor(1);
buf.setFontAlign(-1, -1); buf.setFontAlign(-1, -1);
buf.setFont("Vector",40); buf.setFont("Vector",L.text.largesize);
buf.drawString(dst,0,0); buf.drawString(dst,0,0);
flip2_bw(8, 200);
}
// waypoint name on right // waypoint name on right
if (force || previous.wp_name !== wp.name) {
previous.wp_name = wp.name;
buf.setColor(1); buf.setColor(1);
buf.setFontAlign(1, -1); buf.setFontAlign(1, -1);
buf.setFont("Vector", 15); buf.setFont("Vector", L.text.smallsize);
buf.drawString(wp.name, 80, 0); buf.drawString(wp.name, W, L.text.waypointy);
flip2_bw(160, 220);
} // if this is a route, draw the step name above the route name
if (wp.route) {
buf.drawString((wp.route[routeidx].name||'') + " " + (routeidx+1) + "/" + wp.route.length, W, 0);
} }
function drawAll(force) { flip(L.text.bufy,L.text.bufh,pal_bw);
if (!candraw) return; }
g.setColor(1,1,1);
drawN(force);
drawCompass(direction);
} }
/*** Heading ***/ /*** Heading ***/
var heading = 0; var heading = 0;
function newHeading(m,h){
var s = Math.abs(m - h);
var delta = (m>h)?1:-1;
if (s>=180){s=360-s; delta = -delta;}
if (s<2) return h;
var hd = h + delta*(1 + Math.round(s/5));
if (hd<0) hd+=360;
if (hd>360)hd-= 360;
return hd;
}
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
function tiltfixread(O,S){
var start = Date.now();
var m = Bangle.getCompass();
var g = Bangle.getAccel();
m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
if (d<0) d+=360;
var phi = Math.atan(-g.x/-g.z);
var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
var costheta = Math.cos(theta), sintheta = Math.sin(theta);
var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
var yh = m.dz*sinphi - m.dx*cosphi;
var psi = Math.atan2(yh,xh)*180/Math.PI;
if (psi<0) psi+=360;
return psi;
}
function read_heading() { function read_heading() {
if (savedfix !== undefined && !isNaN(savedfix.course)) { if (savedfix !== undefined && savedfix.satellites > 0 && !isNaN(savedfix.course)) {
Bangle.setCompassPower(0); Bangle.setCompassPower(0);
heading = savedfix.course; heading = savedfix.course;
pal_compass = pal_bw;
} else { } else {
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
Bangle.setCompassPower(1); Bangle.setCompassPower(1);
heading = newHeading(d,heading); var d = 0;
pal_compass = pal_by; var m = Bangle.getCompass();
if (!isNaN(m.heading)) d = -m.heading;
heading = d;
} }
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;
drawCompass(direction); draw();
} }
/*** Maths ***/ /*** Maths ***/
function radians(a) { function radians(a) {
@ -218,23 +215,39 @@ function onGPS(fix) {
savedfix = fix; savedfix = fix;
if (fix !== undefined && fix.fix == 1){ if (fix !== undefined && fix.fix == 1){
if (wp.route) {
while (true) {
dist = distance(fix, wp.route[routeidx]);
// step to next point if we're within ROUTE_STEP metres
if (!isNaN(dist) && dist < ROUTE_STEP && routeidx < wp.route.length-1)
routeidx++;
else
break;
}
} else {
dist = distance(fix, wp); dist = distance(fix, wp);
}
if (isNaN(dist)) dist = 0; if (isNaN(dist)) dist = 0;
if (wp.route) {
wp_bearing = bearing(fix, wp.route[routeidx]);
} else {
wp_bearing = bearing(fix, wp); wp_bearing = bearing(fix, wp);
}
if (isNaN(wp_bearing)) wp_bearing = 0; if (isNaN(wp_bearing)) wp_bearing = 0;
drawN(); draw();
} }
} }
function startTimers() { function startTimers() {
setInterval(function() { setInterval(function() {
Bangle.setLCDPower(1); if (W==240) Bangle.setLCDPower(1); // keep banglejs1 display on
read_heading(); read_heading();
}, 500); }, 250);
} }
function addWaypointToMenu(menu, i) { function addWaypointToMenu(menu, i) {
menu[waypoints[i].name] = function() { menu[waypoints[i].name + (waypoints[i].route ? " (R)" : "")] = function() {
wp = waypoints[i]; wp = waypoints[i];
mainScreen(); mainScreen();
}; };
@ -243,7 +256,9 @@ function addWaypointToMenu(menu, i) {
function mainScreen() { function mainScreen() {
E.showMenu(); E.showMenu();
candraw = true; candraw = true;
drawAll(true); g.setColor(0,0,0);
g.fillRect(0,0,W,H);
draw(true);
Bangle.setUI("updown", function(v) { Bangle.setUI("updown", function(v) {
if (v === undefined) { if (v === undefined) {
@ -262,11 +277,13 @@ function mainScreen() {
E.showMenu(menu); E.showMenu(menu);
} else { } else {
candraw = false; candraw = false;
E.showPrompt("Delete waypoint: " + wp.name + "?").then(function(confirmed) { var thing = wp.route ? "route" : "waypoint";
E.showPrompt("Delete " + thing + ": " + wp.name + "?").then(function(confirmed) {
var name = wp.name; var name = wp.name;
if (confirmed) { if (confirmed) {
var thing = wp.route ? "Route" : "Waypoint";
deleteWaypoint(wp); deleteWaypoint(wp);
E.showAlert("Waypoint deleted: " + name).then(mainScreen); E.showAlert(thing + " deleted: " + name).then(mainScreen);
} else { } else {
mainScreen(); mainScreen();
} }
@ -281,7 +298,6 @@ Bangle.on('kill',()=>{
}); });
g.clear(); g.clear();
Bangle.setLCDBrightness(1);
Bangle.setGPSPower(1); Bangle.setGPSPower(1);
startTimers(); startTimers();
Bangle.on('GPS', onGPS); Bangle.on('GPS', onGPS);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 140 KiB

BIN
apps/wpmoto/editroute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@ -2,11 +2,11 @@
"id": "wpmoto", "id": "wpmoto",
"name": "Waypointer Moto", "name": "Waypointer Moto",
"shortName": "Waypointer Moto", "shortName": "Waypointer Moto",
"version": "0.01", "version": "0.02",
"description": "Waypoint-based motorcycle navigation aid", "description": "Waypoint-based motorcycle navigation aid",
"icon": "wpmoto.png", "icon": "wpmoto.png",
"tags": "tool,outdoors,gps", "tags": "tool,outdoors,gps",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-menu.png"},{"url":"screenshot-delete.png"}], "screenshots": [{"url":"screenshot.png"},{"url":"screenshot-menu.png"},{"url":"screenshot-delete.png"}],
"readme": "README.md", "readme": "README.md",
"interface": "wpmoto.html", "interface": "wpmoto.html",

BIN
apps/wpmoto/newroute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

@ -1,161 +1,238 @@
<html> <!doctype html>
<html lang="en">
<head> <head>
<meta charset="utf-8">
<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 rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/ol-geocoder@latest/dist/ol-geocoder.min.css" rel="stylesheet"> <link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css">
<style type="text/css">
html, body { height: 100% }
.flex-col { display:flex; flex-direction:column; height:100% }
#map { width:100%; height:100% }
/* https://stackoverflow.com/a/58686215 */
.arrow-icon {
width: 14px;
height: 14px;
}
.arrow-icon > div {
margin-left: -1px;
margin-top: -3px;
transform-origin: center center;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
</style>
</head> </head>
<body> <body>
<div class="flex-col">
<div id="statusarea">
<button id="download" class="btn btn-error">Reload</button> <button id="upload" class="btn btn-primary">Upload</button>
<span id="status"></span>
<span id="routestatus"></span>
</div>
<div style="flex: 1">
<div id="map"></div>
</div>
</div>
<h4>List of waypoints</h4> <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin="anonymous"></script>
<table class="table"> <script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>
<thead> <script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<tr> <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<th>Name</th>
<th>Lat.</th>
<th>Long.</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="waypoints">
</tbody>
</table>
<br>
<h4>Add a new waypoint</h4>
<form id="add_waypoint_form">
<div class="columns">
<div class="column col-3 col-xs-8">
<input class="form-input input-sm" type="text" id="add_waypoint_name" placeholder="Name">
</div>
<div class="column col-3 col-xs-8">
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_latitude" placeholder="Lat">
</div>
<div class="column col-3 col-xs-8">
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_longitude" placeholder="Long">
</div>
</div>
<div class="columns">
<div class="column col-3 col-xs-8">
<button id="add_waypoint_button" class="btn btn-primary btn-sm">Add Waypoint</button>
</div>
</div>
</form>
<br>
<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({ var map;
target: 'map', var waypoints = [];
layers: [ var mapmarkers = L.layerGroup();
new ol.layer.Tile({ var searchresult = L.layerGroup();
source: new ol.source.OSM() var dynamicarrow = L.layerGroup();
}) var editingroute = null;
], var lastroutepoint;
view: new ol.View({
center: ol.proj.fromLonLat([37.41, 8.82]),
zoom: 4
})
});
var geocoder = new Geocoder('nominatim', { /*** map ***/
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); map = L.map('map').setView([51.505, -0.09], 8);
$longitude.value = lonlat[0];
$latitude.value = lonlat[1]; L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
subdomains: ['a','b','c'],
}).addTo(map);
L.Control.geocoder({defaultMarkGeocode: false}).addTo(map).on('markgeocode', function(e) {
searchresult.clearLayers();
var bbox = e.geocode.bbox;
var poly = L.polygon([
bbox.getSouthEast(),
bbox.getNorthEast(),
bbox.getNorthWest(),
bbox.getSouthWest()
], {fill:false}).addTo(searchresult);
map.addLayer(searchresult);
map.fitBounds(poly.getBounds());
}); });
var waypoints = []
var $name = document.getElementById('add_waypoint_name')
var $form = document.getElementById('add_waypoint_form')
var $button = document.getElementById('add_waypoint_button')
var $latitude = document.getElementById('add_latitude')
var $longitude = document.getElementById('add_longitude')
var $list = document.getElementById('waypoints')
map.on('click', function(e) { map.on('click', function(e) {
var lonlat = ol.proj.toLonLat(e.coordinate); if (editingroute != null) {
$longitude.value = lonlat[0]; searchresult.clearLayers();
$latitude.value = lonlat[1]; addWaypoint(waypoints[editingroute].route, e.latlng.lat, e.latlng.lng, "");
});
function compare(a, b){
var x = a.name.toLowerCase();
var y = b.name.toLowerCase();
if (x=="none") {return -1};
if (y=="none") {return 1};
if (x < y) {return -1;}
if (x > y) {return 1;}
return 0;
}
$button.addEventListener('click', event => {
event.preventDefault()
var name = $name.value.trim()
if(!name) return;
var lat = parseFloat($latitude.value).toPrecision(8);
var lon = parseFloat($longitude.value).toPrecision(8);
waypoints.push({
name, lat,lon,
});
waypoints.sort(compare);
renderWaypoints()
$name.value = ''
$latitude.value = (0).toPrecision(8);
$longitude.value = (0).toPrecision(8);
});
function removeWaypoint(index){
$name.value = waypoints[index].name
if (waypoints[index].lat !== undefined && waypoints[index].lon !== undefined
&& !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)
renderWaypoints()
}
function renderWaypoints(){
$list.innerHTML = ''
waypoints.forEach((waypoint,index) => {
var $waypoint = document.createElement('tr')
if (index==0){
$waypoint.innerHTML = `<td>${waypoint.name}</td>`
} else if(waypoint.lat==undefined){
$waypoint.innerHTML = `<td>${waypoint.name}</td><td>------</td><td>-----</td><td><button class="btn btn-action btn-primary" onclick="removeWaypoint(${index})"><i class="icon icon-edit"></i></button></td>`
} else { } else {
$waypoint.innerHTML = `<td>${waypoint.name}</td><td>${waypoint.lat}</td><td>${waypoint.lon}</td><td><button class="btn btn-action btn-primary" onclick="removeWaypoint(${index})"><i class="icon icon-edit"></i></button></td>` swal({
icon: 'info',
text: "Enter a name for the waypoint:",
buttons: true,
content: 'input',
}).then((name) => {
if (name != null && name != "") {
searchresult.clearLayers();
addWaypoint(waypoints, e.latlng.lat, e.latlng.lng, name);
} }
$list.appendChild($waypoint) });
})
$name.focus()
} }
});
map.on('mousemove', function(e) {
if (editingroute == null) return;
let latlngs = [lastroutepoint, [e.latlng.lat, e.latlng.lng]];
dynamicarrow.clearLayers();
L.polyline(latlngs, { color:'black'}).addTo(dynamicarrow);
L.featureGroup(getArrows(latlngs, 'black', 2, map)).addTo(dynamicarrow)
map.addLayer(dynamicarrow);
});
clean();
renderAllWaypoints();
/*** status ***/
function clean() {
$('#status').html('<i class="icon icon-check"></i> No pending changes.');
routestatus();
}
function dirty() {
$('#status').html('<b><i class="icon icon-edit"></i> Changes have not been sent to the watch.</b>');
routestatus();
}
function routestatus() {
if (editingroute == null) {
$('#routestatus').html('');
dynamicarrow.clearLayers();
} else {
$('#routestatus').html('Editing route: ' + escapeHTML(waypoints[editingroute].name) + ' <button class="btn btn-sm btn-primary" onclick="closeRoute()">close route</button>');
}
}
/*** waypoints ***/
function addWaypoint(arr, lat, lon, name) {
arr.push({lat:lat, lon:lon, name:name});
renderAllWaypoints();
dirty();
}
function deleteWaypoint(arr, i) {
arr.splice(i, 1);
if (editingroute != null) {
// XXX: ugly: fix editingroute index
if (editingroute == i) editingroute = null;
else if (editingroute > i) editingroute--;
}
renderAllWaypoints();
dirty();
}
function renameWaypoint(arr, i) {
var name = prompt("Enter new name for the waypoint:", arr[i].name);
if (name == null || name == "" || name == arr[i].name)
return;
arr[i].name = name;
renderAllWaypoints();
dirty();
}
function renderWaypoints(wps, isroute, parentidx) {
var latlngs = [];
for (var i = 0; i < wps.length; i++) {
if (wps[i].route) {
renderWaypoints(wps[i].route, true, i);
continue;
}
if (wps[i].lat == null || wps[i].lon == null)
continue;
if (isroute) {
L.marker([wps[i].lat, wps[i].lon], {title: wps[i].name})
.bindPopup(`<h4><b>${wps[i].name}</b> <a class="btn btn-primary" href="javascript:renameWaypoint(waypoints[${parentidx}].route,${i})"><i class="icon icon-edit"></i></a> <a class="btn btn-error" href="javascript:deleteWaypoint(waypoints[${parentidx}].route, ${i})"><i class="icon icon-delete"></i></a></h4><button class="btn btn-sm btn-error" onclick="javascript:deleteEntireRoute(${parentidx})">Delete entire route</button>`)
.addTo(mapmarkers);
latlngs.push([wps[i].lat, wps[i].lon]);
lastroutepoint = [wps[i].lat, wps[i].lon];
} else {
L.marker([wps[i].lat, wps[i].lon], {title: wps[i].name})
.bindPopup(`<h4><b>${wps[i].name}</b> <a class="btn btn-primary" href="javascript:renameWaypoint(waypoints,${i})"><i class="icon icon-edit"></i></a> <a class="btn btn-error" href="javascript:deleteWaypoint(waypoints, ${i})"><i class="icon icon-delete"></i></a></h4><button class="btn btn-sm btn-primary" onclick="javascript:makeRoute(${i})">Make route</button>`)
.addTo(mapmarkers);
}
}
if (isroute) {
L.polyline(latlngs, { color:'black'}).addTo(mapmarkers);
L.featureGroup(getArrows(latlngs, 'black', 2, map)).addTo(mapmarkers)
}
}
function renderAllWaypoints() {
mapmarkers.clearLayers();
renderWaypoints(waypoints, false, 0);
map.addLayer(mapmarkers);
}
/*** routes ***/
function openRoute(i) {
editingroute = i;
console.log("edit route "+ i);
map.on('contextmenu', closeRoute);
renderAllWaypoints();
}
function makeRoute(i) {
waypoints[i].route = [{
name: waypoints[i].name,
lat: waypoints[i].lat,
lon: waypoints[i].lon,
}];
openRoute(i);
dirty();
}
function closeRoute() {
map.off('contextmenu', closeRoute);
editingroute = null;
renderAllWaypoints();
routestatus();
}
function deleteEntireRoute(i) {
swal({
icon: 'warning',
text: "Really delete entire route '" + waypoints[i].name + "'?",
buttons: true,
}).then((v) => {
console.log(v)
if (v) {
deleteWaypoint(waypoints, i);
}
});
}
/*** util ***/
// https://stackoverflow.com/a/22706073
function escapeHTML(str){
return new Option(str).innerHTML;
}
/*** Bangle.js ***/
function downloadJSONfile(fileid, callback) { function downloadJSONfile(fileid, callback) {
Puck.write(`\x10(function() { Puck.write(`\x10(function() {
@ -164,6 +241,7 @@
})()\n`, contents => { })()\n`, contents => {
var storedpts = JSON.parse(contents); var storedpts = JSON.parse(contents);
callback(storedpts); callback(storedpts);
clean();
}); });
} }
@ -173,26 +251,102 @@
Bluetooth.print("OK"); Bluetooth.print("OK");
})()\n`, ret => { })()\n`, ret => {
console.log("uploadFile", ret); console.log("uploadFile", ret);
if (ret == "OK")
clean();
}); });
} }
function gotStored(pts) { function gotStored(pts) {
waypoints = pts; waypoints = pts;
renderWaypoints();
var latlngs = waypoints.map(p => [p.lat, p.lon]);
var poly = L.polygon(latlngs);
map.fitBounds(poly.getBounds());
renderAllWaypoints();
} }
function onInit() { function onInit() {
downloadJSONfile("waypoints.json", gotStored); downloadJSONfile("waypoints.json", gotStored);
} }
document.getElementById("Download").addEventListener("click", function() { $('#download').on('click', function() {
downloadJSONfile("waypoints.json", gotStored); downloadJSONfile("waypoints.json", gotStored);
}); });
document.getElementById("Upload").addEventListener("click", function() { $('#upload').click(function() {
var data = JSON.stringify(waypoints); var data = JSON.stringify(waypoints);
uploadFile("waypoints.json",data); uploadFile("waypoints.json",data);
}); });
$('#statusarea').click(closeRoute);
/*** map arrows ***/
// https://stackoverflow.com/a/58686215
function getArrows(arrLatlngs, color, arrowCount, mapObj) {
if (typeof arrLatlngs === undefined || arrLatlngs == null ||
(!arrLatlngs.length) || arrLatlngs.length < 2)
return [];
if (typeof arrowCount === 'undefined' || arrowCount == null)
arrowCount = 1;
if (typeof color === 'undefined' || color == null)
color = '';
else
color = 'color:' + color;
var result = [];
for (var i = 1; i < arrLatlngs.length; i++) {
var icon = L.divIcon({ className: 'arrow-icon', bgPos: [5, 5], html: '<div style="' + color + ';transform: rotate(' + getAngle(arrLatlngs[i - 1], arrLatlngs[i], -1).toString() + 'deg)"></div>' });
for (var c = 1; c <= arrowCount; c++) {
result.push(L.marker(myMidPoint(arrLatlngs[i], arrLatlngs[i - 1], (c / (arrowCount + 1)), mapObj), { icon: icon }));
}
}
return result;
}
function getAngle(latLng1, latlng2, coef) {
var dy = latlng2[0] - latLng1[0];
var dx = Math.cos(Math.PI / 180 * latLng1[0]) * (latlng2[1] - latLng1[1]);
var ang = ((Math.atan2(dy, dx) / Math.PI) * 180 * coef);
return (ang).toFixed(2);
}
function myMidPoint(latlng1, latlng2, per, mapObj) {
if (!mapObj)
throw new Error('map is not defined');
var halfDist, segDist, dist, p1, p2, ratio,
points = [];
p1 = mapObj.project(new L.latLng(latlng1));
p2 = mapObj.project(new L.latLng(latlng2));
halfDist = distanceTo(p1, p2) * per;
if (halfDist === 0)
return mapObj.unproject(p1);
dist = distanceTo(p1, p2);
if (dist > halfDist) {
ratio = (dist - halfDist) / dist;
var res = mapObj.unproject(new Point(p2.x - ratio * (p2.x - p1.x), p2.y - ratio * (p2.y - p1.y)));
return [res.lat, res.lng];
}
}
function distanceTo(p1, p2) {
var x = p2.x - p1.x,
y = p2.y - p1.y;
return Math.sqrt(x * x + y * y);
}
function Point(x, y, round) {
this.x = (round ? Math.round(x) : x);
this.y = (round ? Math.round(y) : y);
}
</script> </script>
</body> </body>
</html>

View File

@ -51,8 +51,8 @@
<li class="tab-item" id="tab-myappscontainer"> <li class="tab-item" id="tab-myappscontainer">
<a href="javascript:showTab('myappscontainer')">My Apps</a> <a href="javascript:showTab('myappscontainer')">My Apps</a>
</li> </li>
<li class="tab-item" id="tab-aboutcontainer"> <li class="tab-item" id="tab-morecontainer">
<a href="javascript:showTab('aboutcontainer')">About</a> <a href="javascript:showTab('morecontainer')">More...</a>
</li> </li>
</ul> </ul>
@ -111,7 +111,7 @@
</div> </div>
</div> </div>
<div class="container apploader-tab" id="aboutcontainer" style="display:none"> <div class="container apploader-tab" id="morecontainer" style="display:none">
<div class="hero bg-gray"> <div class="hero bg-gray">
<div class="hero-body"> <div class="hero-body">
<a href="https://banglejs.com" target="_blank"><img src="img/banglejs-logo-mid.png" alt="Bangle.js"></a> <a href="https://banglejs.com" target="_blank"><img src="img/banglejs-logo-mid.png" alt="Bangle.js"></a>