BangleApps/apps/wpmoto/wpmoto.html

353 lines
13 KiB
HTML

<!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://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>
</div>
<div style="flex: 1">
<div id="map"></div>
</div>
</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;
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: '&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());
});
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();
/*** 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();
});
}
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);
});
$('#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>