mirror of https://github.com/espruino/BangleApps
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
parent
2f3a9c8c87
commit
f24ec9404d
|
@ -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
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
|
|
@ -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 |
Binary file not shown.
After Width: | Height: | Size: 140 KiB |
|
@ -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",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 125 KiB |
|
@ -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: '© <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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue