The shared location you tried to access was not found on the server. If this link worked before, the share might have expired.
+
+
+
+
+
Share expired
+
This location share has expired.
+
+
+
+
+
+
+
diff --git a/backend/main.js b/backend/main.js
new file mode 100644
index 0000000..e4c0978
--- /dev/null
+++ b/backend/main.js
@@ -0,0 +1,198 @@
+// This is the main script file for Hauk's web view client.
+
+// Create a Leaflet map.
+var map = L.map('map').setView([0, 0], DEFAULT_ZOOM);
+L.tileLayer(TILE_URI, {
+ attribution: ATTRIBUTION,
+ maxZoom: MAX_ZOOM
+}).addTo(map);
+
+var circleLayer = L.layerGroup().addTo(map);
+var markerLayer = L.layerGroup().addTo(map);
+
+// A list of points received from the server.
+var points = [];
+
+// The leaflet marker.
+var marker = null;
+var icon = null;
+var circle = null;
+
+// Retrieve the sharing link ID from the URL. E.g.
+// https://example.com/?ABCD-1234 --> "ABCD-1234"
+var id = location.href.substr(location.href.indexOf("?") + 1);
+if (id.indexOf("&") !== -1) id = id.substr(0, id.indexOf("&"));
+
+function getJSON(url, callback, invalid) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = function() {
+ if (this.readyState == 4 && this.status === 200) {
+ try {
+ var json = JSON.parse(this.responseText);
+ callback(json);
+ } catch (ex) {
+ console.log(ex);
+ invalid();
+ }
+ }
+ }
+ xhr.send();
+}
+
+document.getElementById("dismiss").addEventListener("click", function() {
+ document.getElementById("expired").style.display = "none";
+});
+
+// Attempt to fetch location data from the server once.
+getJSON("./api/fetch.php?id=" + id, function(data) {
+ document.getElementById("mapouter").style.visibility = "visible";
+
+ // The location data contains an interval. Schedule a task that fetches data
+ // once per interval time.
+ var interval = setInterval(function() {
+ // Stop the task if the share has expired.
+ if ((Date.now() / 1000) >= data.x) clearInterval(interval);
+
+ getJSON("./api/fetch.php?id=" + id, function(data) {
+ processUpdate(data);
+ }, function() {
+ clearInterval(interval);
+ document.getElementById("expired").style.display = "block";
+ });
+ }, data.i * 1000);
+ processUpdate(data);
+}, function() {
+ document.getElementById("notfound").style.display = "block";
+});
+
+// Parses the data returned from ./api/fetch.php and updates the map marker.
+function processUpdate(data) {
+ // Get the last location received.
+ var lastPoint = points.length > 0 ? points[points.length - 1] : null;
+
+ for (var i = 0; i < data.l.length; i++) {
+ var lat = data.l[i][0];
+ var lon = data.l[i][1];
+ var time = data.l[i][2];
+ var acc = data.l[i][3];
+ var spd = data.l[i][4];
+
+ // Check if the location should be added. Only add new location points
+ // if the point was not recorded before the last recorded point.
+ if (lastPoint === null || time > lastPoint.time) {
+ var line = null;
+ if (marker == null) {
+ // Add a marker to the map if it's not already there.
+ icon = L.divIcon({
+ html: '
0.0 ' + VELOCITY_UNIT.unit + '
',
+ iconAnchor: [33, 18]
+ });
+ marker = L.marker([lat, lon], {icon: icon, interactive: false}).addTo(markerLayer);
+ } else {
+ // If there is a marker, draw a line from its last location
+ // instead and move the marker.
+ line = L.polyline([marker.getLatLng(), [lat, lon]], {color: TRAIL_COLOR}).addTo(markerLayer);
+ marker.setLatLng([lat, lon]);
+ }
+ // Draw an accuracy circle if GPS accuracy was provided by the
+ // client.
+ if (acc !== null && circle == null) {
+ circle = L.circle([lat, lon], {radius: acc, fillColor: '#d80037', fillOpacity: 0.25, color: '#d80037', opacity: 0.5, interactive: false}).addTo(circleLayer);
+ } else if (circle !== null) {
+ circle.setLatLng([lat, lon]);
+ if (acc !== null) circle.setRadius(acc);
+ }
+ points.push({lat: lat, lon: lon, line: line, time: time, spd: spd, acc: acc});
+ lastPoint = points[points.length - 1];
+ }
+ }
+
+ var eVelocity = document.getElementById("velocity")
+ if (lastPoint !== null && lastPoint.spd !== null && eVelocity !== null) {
+ // Prefer client-provided speed if possible.
+ eVelocity.textContent = (lastPoint.spd * VELOCITY_UNIT.mpsMod).toFixed(1);
+ } else if (eVelocity !== null) {
+ // If the client didn't provide its speed, calculate it locally from its
+ // list of locations.
+ var dist = 0;
+ var time = 0;
+ var idx = points.length;
+
+ // Iterate over all locations backwards until we either reach our
+ // required VELOCITY_DELTA_TIME, or we run out of points.
+ while (idx > 2) {
+ idx--;
+ var pt1 = points[idx - 1];
+ var pt2 = points[idx];
+ var dTime = pt2.time - pt1.time;
+
+ // If the new time does not exceed the VELOCITY_DELTA_TIME, add the
+ // time and distance deltas to the appropriate sum for averaging;
+ // otherwise, break the loop and proceed to calculate.
+ if (time + dTime <= VELOCITY_DELTA_TIME) {
+ time += dTime;
+ dist += distance(pt1, pt2);
+ } else {
+ break;
+ }
+ }
+
+ // Update the UI with the velocity.
+ eVelocity.textContent = velocity(dist, time);
+ }
+
+
+ // Follow the marker.
+ if (lastPoint !== null) map.panTo([lastPoint.lat, lastPoint.lon]);
+
+ // Rotate the marker to the direction of movement.
+ var eArrow = document.getElementById("arrow");
+ if (eArrow !== null && points.length >= 2) {
+ var last = points.length - 1;
+ eArrow.style.transform = "rotate(" + angle(points[last - 1], points[last]) + "deg)";
+ }
+
+ // Prune the array of locations so it does not exceed our MAX_POINTS defined
+ // in the config.
+ if (points.length > MAX_POINTS) {
+ var remove = points.splice(0, points.length - MAX_POINTS);
+ for (var j = 0; j < remove.length; j++) if (remove[j].line !== null) map.removeLayer(remove[j].line);
+ }
+}
+
+// Calculates the distance between two points on a sphere using the Haversine
+// algorithm.
+function distance(from, to) {
+ var d2r = Math.PI / 180;
+ var fla = from.lat * d2r, tla = to.lat * d2r;
+ var flo = from.lon * d2r, tlo = to.lon * d2r;
+ var havLat = Math.sin((tla - fla) / 2); havLat *= havLat;
+ var havLon = Math.sin((tlo - flo) / 2); havLon *= havLon;
+ var hav = havLat + Math.cos(fla) * Math.cos(tla) * havLon;
+ var d = Math.asin(Math.sqrt(hav));
+ return d;
+}
+
+// Calculates a velocity using the velocity unit from the config.
+function velocity(distance, intv) {
+ if (intv == 0) return "0.0";
+ return (distance * VELOCITY_UNIT.havMod / intv).toFixed(1);
+}
+
+// Calculates the bearing between two points on a sphere in degrees.
+function angle(from, to) {
+ var d2r = Math.PI / 180;
+ var fromLat = from.lat * d2r, toLat = to.lat * d2r;
+ var fromLon = from.lon * d2r, toLon = to.lon * d2r;
+ /*
+ Calculation code by krishnar from
+ https://stackoverflow.com/a/52079217
+ */
+ var x = Math.cos(fromLat) * Math.sin(toLat)
+ - Math.sin(fromLat) * Math.cos(toLat) * Math.cos(toLon - fromLon);
+ var y = Math.sin(toLon - fromLon) * Math.cos(toLat);
+ var heading = Math.atan2(y, x) / d2r;
+
+ return (heading + 360) % 360;
+}
diff --git a/backend/style.css b/backend/style.css
new file mode 100644
index 0000000..2080f9b
--- /dev/null
+++ b/backend/style.css
@@ -0,0 +1,156 @@
+/* The main stylesheet for the Hauk web view interface. */
+
+* {
+ font-family: sans-serif;
+}
+
+.hidden {
+ display: none;
+}
+
+/* The map should cover the entire viewport. */
+#mapouter {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ /* Hide by default while we check whether or not the share exists. */
+ visibility: hidden;
+}
+
+#map {
+ width: 100%;
+ height: 100%;
+}
+
+/* Popup covers that should display on top of the map. */
+.cover {
+ position: absolute;
+ width: 80vmin;
+ height: 80vmin;
+ top: 0;
+ left: 50%;
+ z-index: 2000;
+ background-color: #fff;
+ text-align: center;
+ padding: 10vmin;
+ transform: translateX(-50%);
+}
+
+/* Hauk logo. */
+.cover > img {
+ width: 100%;
+}
+
+/* Popup header. */
+.cover > p.header {
+ font-size: 8vmin;
+ font-weight: bold;
+}
+
+/* Popup information. */
+.cover > p.body {
+ font-size: 4vmin;
+}
+
+/* Dialog window that contains a title, message and button. This is the outer
+ box, with a semitransparent black background to provide shading against the
+ map, which it renders on top of. */
+.dialog {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ z-index: 2000;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+/* The actual message dialog itself. */
+.dialog > div {
+ width: 300px;
+ max-width: 80vw;
+ background-color: white;
+ padding: 5px 20px;
+ position: relative;
+ top: 50%;
+ left: 50%;
+ transform: translateX(-50%) translateY(-50%);
+}
+
+.dialog p.header {
+ font-size: 1.2em;
+ font-weight: bold;
+}
+
+/* Ensure the button is big enough to be clicked on all devices. */
+.dialog input[type=button] {
+ font-size: 1em;
+}
+
+/* Display the Hauk logo in the bottom left corner of the map if the viewport is
+ wide enough to accomodate it. If it's so narrow that the Leaflet attribution
+ could be covered by it, display it in the bottom left corner instead, above
+ the attribution. */
+@media (max-width: 700px) {
+ #logo {
+ position: fixed;
+ bottom: 20px;
+ right: 5px;
+ z-index: 1000;
+ }
+}
+@media (min-width: 700.001px) {
+ #logo {
+ position: fixed;
+ bottom: 5px;
+ left: 10px;
+ z-index: 1000;
+ }
+}
+
+#logo div {
+ width: 73.33479px;
+ height: 28.853556px;
+ background: url(./assets/logo.svg) no-repeat;
+ background-size: cover;
+ margin: auto;
+}
+
+/* The outer marker div. */
+#marker {
+ width: 66px;
+ height: 62px;
+}
+
+/* The arrow within the marker div. */
+#arrow {
+ background: url(marker.svg) no-repeat;
+ background-size: cover;
+ width: 36px;
+ height: 36px;
+ margin: auto;
+}
+
+/* The velocity indicator on the marker div. */
+#marker p {
+ font-size: 0.9em;
+ background-color: rgba(0,0,0,0.5);
+ color: white;
+ width: 100%;
+ border-radius: 15px;
+ text-align: center;
+ padding: 2px 0;
+ line-height: 100%;
+ font-family: sans-serif;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: clip;
+}
+
+/* Hide the default white box in the top left corner of the marker div. */
+.leaflet-div-icon {
+ background: none;
+ border: none;
+}