mirror of https://github.com/espruino/BangleApps
Merge pull request #1431 from jes/master
Waypointer Moto: Add routes, support for Bangle.js 2, better editor UIpull/1451/head
commit
fe6014a74c
|
@ -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);
|
||||
|
||||
// distance on left
|
||||
if (force || previous.dst !== dst) {
|
||||
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
|
||||
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;
|
||||
// waypoint name on right
|
||||
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);
|
||||
}
|
||||
|
||||
flip(L.text.bufy,L.text.bufh,pal_bw);
|
||||
}
|
||||
}
|
||||
|
||||
function drawAll(force) {
|
||||
if (!candraw) return;
|
||||
|
||||
g.setColor(1,1,1);
|
||||
drawN(force);
|
||||
drawCompass(direction);
|
||||
}
|
||||
|
||||
/*** 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){
|
||||
dist = distance(fix, wp);
|
||||
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;
|
||||
wp_bearing = bearing(fix, wp);
|
||||
|
||||
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,198 +1,352 @@
|
|||
<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">
|
||||
|
||||
</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">
|
||||
<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>
|
||||
<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 style="flex: 1">
|
||||
<div id="map"></div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<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 ***/
|
||||
|
||||
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 lonlat = ol.proj.toLonLat(e.coordinate);
|
||||
$longitude.value = lonlat[0];
|
||||
$latitude.value = lonlat[1];
|
||||
});
|
||||
map.on('click', function(e) {
|
||||
if (editingroute != null) {
|
||||
searchresult.clearLayers();
|
||||
addWaypoint(waypoints[editingroute].route, e.latlng.lat, e.latlng.lng, "");
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
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();
|
||||
|
||||
var waypoints = []
|
||||
/*** status ***/
|
||||
|
||||
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;
|
||||
function clean() {
|
||||
$('#status').html('<i class="icon icon-check"></i> No pending changes.');
|
||||
routestatus();
|
||||
}
|
||||
|
||||
$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);
|
||||
function dirty() {
|
||||
$('#status').html('<b><i class="icon icon-edit"></i> Changes have not been sent to the watch.</b>');
|
||||
routestatus();
|
||||
}
|
||||
|
||||
waypoints.push({
|
||||
name, lat,lon,
|
||||
});
|
||||
|
||||
waypoints.sort(compare);
|
||||
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>');
|
||||
}
|
||||
}
|
||||
|
||||
renderWaypoints()
|
||||
$name.value = ''
|
||||
$latitude.value = (0).toPrecision(8);
|
||||
$longitude.value = (0).toPrecision(8);
|
||||
});
|
||||
/*** waypoints ***/
|
||||
|
||||
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)
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
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 {
|
||||
$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>`
|
||||
}
|
||||
$list.appendChild($waypoint)
|
||||
})
|
||||
$name.focus()
|
||||
}
|
||||
/*** util ***/
|
||||
|
||||
function downloadJSONfile(fileid, callback) {
|
||||
Puck.write(`\x10(function() {
|
||||
var pts = require("Storage").readJSON("${fileid}")||[{name:"NONE"}];
|
||||
Bluetooth.print(JSON.stringify(pts));
|
||||
})()\n`,contents=>{
|
||||
var storedpts = JSON.parse(contents);
|
||||
callback(storedpts);
|
||||
// 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 => {
|
||||
var storedpts = JSON.parse(contents);
|
||||
callback(storedpts);
|
||||
clean();
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFile(fileid, contents) {
|
||||
Puck.write(`\x10(function() {
|
||||
require("Storage").write("${fileid}",'${contents}');
|
||||
Bluetooth.print("OK");
|
||||
})()\n`, ret => {
|
||||
console.log("uploadFile", ret);
|
||||
if (ret == "OK")
|
||||
clean();
|
||||
});
|
||||
}
|
||||
|
||||
function gotStored(pts) {
|
||||
waypoints = pts;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
$('#download').on('click', function() {
|
||||
downloadJSONfile("waypoints.json", gotStored);
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFile(fileid, contents) {
|
||||
Puck.write(`\x10(function() {
|
||||
require("Storage").write("${fileid}",'${contents}');
|
||||
Bluetooth.print("OK");
|
||||
})()\n`,ret=>{
|
||||
console.log("uploadFile",ret);
|
||||
$('#upload').click(function() {
|
||||
var data = JSON.stringify(waypoints);
|
||||
uploadFile("waypoints.json",data);
|
||||
});
|
||||
}
|
||||
|
||||
function gotStored(pts){
|
||||
waypoints = pts;
|
||||
renderWaypoints();
|
||||
}
|
||||
$('#statusarea').click(closeRoute);
|
||||
|
||||
function onInit() {
|
||||
downloadJSONfile("waypoints.json", gotStored);
|
||||
}
|
||||
/*** 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 [];
|
||||
|
||||
document.getElementById("Download").addEventListener("click", function() {
|
||||
downloadJSONfile("waypoints.json", gotStored);
|
||||
});
|
||||
if (typeof arrowCount === 'undefined' || arrowCount == null)
|
||||
arrowCount = 1;
|
||||
|
||||
document.getElementById("Upload").addEventListener("click", function() {
|
||||
var data = JSON.stringify(waypoints);
|
||||
uploadFile("waypoints.json",data);
|
||||
});
|
||||
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>
|
||||
|
|
Loading…
Reference in New Issue