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
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`).
## Waypoint editor
@ -68,27 +69,43 @@ This will load up the waypoint editor:
### Add a waypoint
Use the map to find your destination. Clicking on the map will
populate the latitude/longitude input boxes with the coordinates
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.
Click on the map to add a waypoint. You'll be prompted to give it
a name.
### Edit a waypoint
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.
Click on the map marker of the waypoint you wish to edit. You
can then click on the blue pencil icon to edit the name of the
waypoint. If you want to move the waypoint to a new location then
you need to delete it and re-add it.
### Delete a waypoint
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.
Click on the map marker of the waypoint you wish to delete. You
can then click on the red bin icon to delete the waypoint.
### 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
@ -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 delete waypoints from inside the app without needing the PC
* still uses the same `waypoints.json` file
* supports "routes" which automatically step from one waypoint to the next
## Gotchas
@ -144,8 +162,6 @@ turns white again.
## 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
- make the text & arrow more legible in direct sunlight
- integrate a charging connector into the handlebar mount

View File

@ -1,10 +1,15 @@
var loc = require("locale");
var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
var waypoints = require("Storage").readJSON("waypoints.json") || [];
var wp = waypoints[0];
if (wp == undefined) wp = {name:"NONE"};
var wp_bearing = 0;
var routeidx = 0;
var candraw = true;
const ROUTE_STEP = 50; // metres
const EPSILON = 1; // degrees
var direction = 0;
var dist = 0;
@ -15,138 +20,130 @@ var previous = {
wp_name: '',
course: 180,
selected: false,
routeidx: -1,
};
/*** Drawing ***/
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
var pal_br = new Uint16Array([0x0000,0xf800],0,1); // black, red
var pal_compass = pal_by;
var W = g.getWidth();
var H = g.getHeight();
// layout (XXX: this should probably use the Layout library instead)
var L = { // banglejs1
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"));
function flip1(x,y,palette) {
g.drawImage({width:160,height:160,bpp:1,buffer:buf.buffer, palette:palette},x,y);
function flip(y,h,palette) {
g.drawImage({width:240,height:h,bpp:1,buffer:buf.buffer, palette:palette},0,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 draw(force) {
if (!candraw) return;
previous.course = course;
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 course = direction;
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
if (force || previous.dst !== dst) {
previous.dst = dst;
previous.wp_name = wp.name;
previous.routeidx = routeidx;
buf.setColor(1);
buf.setFontAlign(-1, -1);
buf.setFont("Vector",40);
buf.setFont("Vector",L.text.largesize);
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);
buf.setFont("Vector", L.text.smallsize);
buf.drawString(wp.name, W, L.text.waypointy);
// 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) {
if (!candraw) return;
g.setColor(1,1,1);
drawN(force);
drawCompass(direction);
flip(L.text.bufy,L.text.bufh,pal_bw);
}
}
/*** Heading ***/
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() {
if (savedfix !== undefined && !isNaN(savedfix.course)) {
if (savedfix !== undefined && savedfix.satellites > 0 && !isNaN(savedfix.course)) {
Bangle.setCompassPower(0);
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;
var d = 0;
var m = Bangle.getCompass();
if (!isNaN(m.heading)) d = -m.heading;
heading = d;
}
direction = wp_bearing - heading;
if (direction < 0) direction += 360;
if (direction > 360) direction -= 360;
drawCompass(direction);
draw();
}
/*** Maths ***/
function radians(a) {
@ -218,23 +215,39 @@ function onGPS(fix) {
savedfix = fix;
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);
}
if (isNaN(dist)) dist = 0;
if (wp.route) {
wp_bearing = bearing(fix, wp.route[routeidx]);
} else {
wp_bearing = bearing(fix, wp);
}
if (isNaN(wp_bearing)) wp_bearing = 0;
drawN();
draw();
}
}
function startTimers() {
setInterval(function() {
Bangle.setLCDPower(1);
if (W==240) Bangle.setLCDPower(1); // keep banglejs1 display on
read_heading();
}, 500);
}, 250);
}
function addWaypointToMenu(menu, i) {
menu[waypoints[i].name] = function() {
menu[waypoints[i].name + (waypoints[i].route ? " (R)" : "")] = function() {
wp = waypoints[i];
mainScreen();
};
@ -243,7 +256,9 @@ function addWaypointToMenu(menu, i) {
function mainScreen() {
E.showMenu();
candraw = true;
drawAll(true);
g.setColor(0,0,0);
g.fillRect(0,0,W,H);
draw(true);
Bangle.setUI("updown", function(v) {
if (v === undefined) {
@ -262,11 +277,13 @@ function mainScreen() {
E.showMenu(menu);
} else {
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;
if (confirmed) {
var thing = wp.route ? "Route" : "Waypoint";
deleteWaypoint(wp);
E.showAlert("Waypoint deleted: " + name).then(mainScreen);
E.showAlert(thing + " deleted: " + name).then(mainScreen);
} else {
mainScreen();
}
@ -281,7 +298,6 @@ Bangle.on('kill',()=>{
});
g.clear();
Bangle.setLCDBrightness(1);
Bangle.setGPSPower(1);
startTimers();
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",
"name": "Waypointer Moto",
"shortName": "Waypointer Moto",
"version": "0.01",
"version": "0.02",
"description": "Waypoint-based motorcycle navigation aid",
"icon": "wpmoto.png",
"tags": "tool,outdoors,gps",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-menu.png"},{"url":"screenshot-delete.png"}],
"readme": "README.md",
"interface": "wpmoto.html",

BIN
apps/wpmoto/newroute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

@ -1,169 +1,247 @@
<html>
<head>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="../../css/spectre.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>
<body>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css">
<h4>List of waypoints</h4>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Lat.</th>
<th>Long.</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="waypoints">
<style type="text/css">
html, body { height: 100% }
.flex-col { display:flex; flex-direction:column; height:100% }
#map { width:100%; height:100% }
</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">
/* 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>
<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 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 style="flex: 1">
<div id="map"></div>
</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="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin="anonymous"></script>
<script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="../../core/lib/interface.js"></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 map;
var waypoints = [];
var mapmarkers = L.layerGroup();
var searchresult = L.layerGroup();
var dynamicarrow = L.layerGroup();
var editingroute = null;
var lastroutepoint;
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)
});
/*** map ***/
var lonlat = ol.proj.toLonLat(e.coordinate);
$longitude.value = lonlat[0];
$latitude.value = lonlat[1];
map = L.map('map').setView([51.505, -0.09], 8);
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) {
var lonlat = ol.proj.toLonLat(e.coordinate);
$longitude.value = lonlat[0];
$latitude.value = lonlat[1];
});
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>`
if (editingroute != null) {
searchresult.clearLayers();
addWaypoint(waypoints[editingroute].route, e.latlng.lat, e.latlng.lng, "");
} 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) {
Puck.write(`\x10(function() {
var pts = require("Storage").readJSON("${fileid}")||[{name:"NONE"}];
Bluetooth.print(JSON.stringify(pts));
})()\n`,contents=>{
})()\n`, contents => {
var storedpts = JSON.parse(contents);
callback(storedpts);
clean();
});
}
@ -171,28 +249,104 @@
Puck.write(`\x10(function() {
require("Storage").write("${fileid}",'${contents}');
Bluetooth.print("OK");
})()\n`,ret=>{
console.log("uploadFile",ret);
})()\n`, ret => {
console.log("uploadFile", ret);
if (ret == "OK")
clean();
});
}
function gotStored(pts){
function gotStored(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() {
downloadJSONfile("waypoints.json", gotStored);
}
document.getElementById("Download").addEventListener("click", function() {
$('#download').on('click', function() {
downloadJSONfile("waypoints.json", gotStored);
});
document.getElementById("Upload").addEventListener("click", function() {
$('#upload').click(function() {
var data = JSON.stringify(waypoints);
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>
</body>
</html>
</body>

View File

@ -51,8 +51,8 @@
<li class="tab-item" id="tab-myappscontainer">
<a href="javascript:showTab('myappscontainer')">My Apps</a>
</li>
<li class="tab-item" id="tab-aboutcontainer">
<a href="javascript:showTab('aboutcontainer')">About</a>
<li class="tab-item" id="tab-morecontainer">
<a href="javascript:showTab('morecontainer')">More...</a>
</li>
</ul>
@ -111,7 +111,7 @@
</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-body">
<a href="https://banglejs.com" target="_blank"><img src="img/banglejs-logo-mid.png" alt="Bangle.js"></a>