2022-01-28 11:47:23 +00:00
|
|
|
<!doctype html>
|
|
|
|
<html lang="en">
|
|
|
|
<head>
|
|
|
|
<meta charset="utf-8">
|
2022-01-22 22:21:51 +00:00
|
|
|
<link rel="stylesheet" href="../../css/spectre.min.css">
|
|
|
|
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
2022-01-28 11:47:23 +00:00
|
|
|
<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">
|
|
|
|
|
|
|
|
<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>
|
2022-01-22 22:21:51 +00:00
|
|
|
</div>
|
2022-01-28 11:47:23 +00:00
|
|
|
<div style="flex: 1">
|
|
|
|
<div id="map"></div>
|
2022-01-22 22:21:51 +00:00
|
|
|
</div>
|
2022-01-28 11:47:23 +00:00
|
|
|
</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>
|
2022-01-22 22:21:51 +00:00
|
|
|
<script src="../../core/lib/interface.js"></script>
|
|
|
|
|
|
|
|
<script>
|
2022-01-28 11:47:23 +00:00
|
|
|
var map;
|
|
|
|
var waypoints = [];
|
|
|
|
var mapmarkers = L.layerGroup();
|
|
|
|
var searchresult = L.layerGroup();
|
|
|
|
var dynamicarrow = L.layerGroup();
|
|
|
|
var editingroute = null;
|
|
|
|
var lastroutepoint;
|
|
|
|
|
|
|
|
/*** 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());
|
2022-01-22 22:25:40 +00:00
|
|
|
});
|
|
|
|
|
2022-01-28 11:47:23 +00:00
|
|
|
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);
|
2022-01-22 22:21:51 +00:00
|
|
|
});
|
2022-01-28 11:47:23 +00:00
|
|
|
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 => {
|
|
|
|
var storedpts = JSON.parse(contents);
|
|
|
|
callback(storedpts);
|
|
|
|
clean();
|
2022-01-22 22:25:40 +00:00
|
|
|
});
|
|
|
|
}
|
2022-01-28 11:47:23 +00:00
|
|
|
|
|
|
|
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);
|
2022-01-22 22:21:51 +00:00
|
|
|
});
|
2022-01-28 11:47:23 +00:00
|
|
|
|
|
|
|
$('#upload').click(function() {
|
|
|
|
var data = JSON.stringify(waypoints);
|
|
|
|
uploadFile("waypoints.json",data);
|
2022-01-22 22:21:51 +00:00
|
|
|
});
|
|
|
|
|
2022-01-28 11:47:23 +00:00
|
|
|
$('#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');
|
2022-01-22 22:21:51 +00:00
|
|
|
|
2022-01-28 11:47:23 +00:00
|
|
|
var halfDist, segDist, dist, p1, p2, ratio,
|
|
|
|
points = [];
|
2022-01-22 22:21:51 +00:00
|
|
|
|
2022-01-28 11:47:23 +00:00
|
|
|
p1 = mapObj.project(new L.latLng(latlng1));
|
|
|
|
p2 = mapObj.project(new L.latLng(latlng2));
|
2022-01-22 22:21:51 +00:00
|
|
|
|
2022-01-28 11:47:23 +00:00
|
|
|
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);
|
|
|
|
}
|
2022-01-22 22:21:51 +00:00
|
|
|
</script>
|
2022-01-28 11:47:23 +00:00
|
|
|
</body>
|