/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ var packages = []; if (getStorage("packages") != null) { packages = JSON.parse(getStorage("packages")); } /** * Count how many items are still undelivered for a location. * @param {type} location An item in the packages array. * @returns {Number} */ function getUndeliveredCount(location) { var undelivered = 0; for (var i = 0; i < location.items.length; i++) { if (!location.items[i].delivered) { undelivered++; } } return undelivered; } function getPackage(packageid) { for (var i = 0; i < packages.length; i++) { for (var j = 0; j < packages[i].items.length; j++) { if (packages[i].items[j].id == packageid) { return packages[i].items[j]; } } } } function getIconForType(type) { return SETTINGS.itemtypes[type].icon; } function getMapIconForItems(items) { var types = {}; var deliveredcount = 0; var type = "package"; for (var i = 0; i < items.length; i++) { // Don't consider delivered packages when determining icon, // only count them to check if everything is delivered if (items[i].delivered) { deliveredcount++; continue; } if (isNaN(types[items[i].type])) { types[items[i].type] = 0; } types[items[i].type]++; } if (deliveredcount == items.length) { return "check"; } item_types = 0; icon = "multiple-items"; // Count how many types we have, and set/overwrite the icon assuming we // only have that type. If we end up with multiple types, we return that // icon instead of a specific one. //console.log(types); for (var type in types) { //console.log(type); item_types++; if (types[type] == 1) { icon = SETTINGS.itemtypes[type].mapicon; } else { icon = SETTINGS.itemtypes[type].pluralmapicon; } //console.log(icon); } if (item_types > 1) { return "multiple-items"; } return icon; } function addPackage(address, latitude, longitude, type, callback, deadline) { var added = false; if (typeof type == 'undefined') { type = SETTINGS.itemtypes[0].id; } if (typeof deadline == 'undefined') { deadline = false; } if (typeof address == "object") { var extendedaddress = address; address = extendedaddress.address; } else { var extendedaddress = false; } // Extra precision makes the map stupider, // and doesn't really increase accuracy since four decimal places ~= 11m latitude = +(parseFloat("" + latitude).toFixed(4)); longitude = +(parseFloat("" + longitude).toFixed(4)); var packageID = uuidv4(); var coordsID = ""; for (var i = 0; i < packages.length; i++) { if (packages[i].coords[0] == latitude && packages[i].coords[1] == longitude) { coordsID = packages[i].id; packages[i].items.push({ extended: extendedaddress, address: address, delivered: false, type: type, deadline: deadline, id: packageID }); added = true; break; } } if (!added) { coordsID = uuidv4(); packages.push({ coords: [ latitude, longitude ], id: coordsID, address: address, items: [ { extended: extendedaddress, address: address, delivered: false, type: type, deadline: deadline, id: packageID } ] }); } setStorage("packages", JSON.stringify(packages)); appendActivityLog("Added", SETTINGS.itemtypes[type].name, address, "fas fa-truck-loading"); playSound("ok"); app.toast.show({ text: SETTINGS.itemtypes[type].name + ' Added!
' + address + "", position: "bottom", destroyOnClose: true, closeTimeout: 1000 * 3 }); if (map != null) { reloadMap(); } if (typeof callback == 'function') { callback({ coordsID: coordsID, packageID: packageID }); } addAutofillEntry(address); } /** * Import a second package list and merge it with the existing one. * @param {type} newlist * @return {number} The number of packages that were skipped because they already exist locally. */ function importPackageList(newlist) { skipped = 0; let count = 0; for (latlng in newlist) { var latitude = newlist[latlng].coords[0]; var longitude = newlist[latlng].coords[1]; latitude = +(parseFloat("" + latitude).toFixed(4)); longitude = +(parseFloat("" + longitude).toFixed(4)); for (pkg in newlist[latlng].items) { var added = false; for (var i = 0; i < packages.length; i++) { if (+(parseFloat("" + packages[i].coords[0]).toFixed(4)) == latitude && +(parseFloat("" + packages[i].coords[1]).toFixed(4)) == longitude) { var newpackage = newlist[latlng].items[pkg]; for (var j in packages[i].items) { if (packages[i].items[j].id == newpackage.id) { // This package already exists in the local database. added = true; skipped++; } } if (!added) { packages[i].items.push(package); count++; added = true; } break; } } if (!added) { packages.push(newlist[latlng]); } } } setStorage("packages", JSON.stringify(packages)); appendActivityLog("Imported List", count + " items added", "", "fas fa-file-download"); if (map != null) { reloadMap(); } return skipped; } function mapCalibrate(item, packagesentry) { // Determine if the delivery location isn't near the map pin if (userPosition.coords.accuracy < 20 && timeDiff(userPosition.updated) < 10) { // User location is accurate, check distance var distance = getDistance(packagesentry.coords[0], packagesentry.coords[1], userPosition.coords.latitude, userPosition.coords.longitude); var lat = userPosition.coords.latitude; var lon = userPosition.coords.longitude; if (distance > 100) { // Over 100 meters distance if (typeof item.extended == "object") { // we have all the info we need var fixmap = function (item, latitude, longitude, locationtype, pkgsentry) { $.ajax({ type: "POST", url: SETTINGS.mapfixapi, data: { number: item.extended.number, unit: item.extended.unit, street: item.extended.street, citystate: item.extended.citystate, zip: item.extended.zip, latitude: latitude, longitude: longitude, locationtype: locationtype }, success: function () { appendActivityLog("Map Calibrated", item.address, "Thanks for improving the map accuracy!
" + "Old: " + pkgsentry.coords[0] + ", " + pkgsentry.coords[1] + "
" + "New: " + latitude + ", " + longitude + "", "fas fa-map-marked-alt" ); }, error: function () { // try again in five minutes setTimeout(function () { fixmap(item, latitude, longitude, locationtype, pkgsentry); }, 1000 * 60 * 5); }, dataType: "json" }); }; app.dialog.create({ title: 'Map Calibration', text: "Your actual location doesn't match the map location for the " + SETTINGS.itemtypes[item.type].name + " at " + item.address + ". Where are you?", buttons: [ { text: 'Address', close: true }, { text: 'Mailbox/CBU', close: true }, { text: 'Parcel Locker', close: true }, { text: "Other/Cancel", close: true } ], verticalButtons: true, onClick: function (dialog, index) { switch (index) { case 0: fixmap(item, lat, lon, "address", packagesentry); break; case 1: fixmap(item, lat, lon, "mailbox", packagesentry); break; case 2: fixmap(item, lat, lon, "locker", packagesentry); break; default: return; } } }).open(); } } } } function markDelivered(id, delivered) { for (var i = 0; i < packages.length; i++) { for (var j = 0; j < packages[i].items.length; j++) { if (packages[i].items[j].id == id) { if (typeof delivered == 'undefined') { if (packages[i].items[j].delivered == false) { delivered = true; } else { delivered = false; } } packages[i].items[j].delivered = delivered; let gpslink = ""; if (userPosition.coords.accuracy < 40 && timeDiff(userPosition.updated) < 15) { var lat = parseFloat(userPosition.coords.latitude).toFixed(5); var lon = parseFloat(userPosition.coords.longitude).toFixed(5); gpslink = "
" + lat + ", " + lon + " (±" + getDisplayDistance(userPosition.coords.accuracy, false) + ")"; } if (delivered) { packages[i].items[j].deliverytimestamp = time(); appendActivityLog("Delivered", SETTINGS.itemtypes[packages[i].items[j].type].name, packages[i].items[j].address + gpslink, "far fa-check-circle"); mapCalibrate(packages[i].items[j], packages[i]); } else { packages[i].items[j].deliverytimestamp = null; appendActivityLog("Undelivered", SETTINGS.itemtypes[packages[i].items[j].type].name, packages[i].items[j].address, "fas fa-undo"); } setStorage("packages", JSON.stringify(packages)); return; // so we don't keep looping over the rest of the packages } } } } function confirmDeletePackage(package, callback) { app.dialog.confirm( "Delete " + SETTINGS.itemtypes[package.type].name.toLowerCase() + " at " + package.address + "?", "Confirm", function () { // delete deletePackage(package.id, callback); }, function () { // cancel } ); } function deletePackage(id, callback) { for (var i = 0; i < packages.length; i++) { for (var j = 0; j < packages[i].items.length; j++) { if (packages[i].items[j].id == id) { appendActivityLog("Deleted", SETTINGS.itemtypes[packages[i].items[j].type].name, packages[i].items[j].address, "fas fa-trash"); packages[i].items.splice(j, 1); if (packages[i].items.length == 0) { packages.splice(i, 1); } setStorage("packages", JSON.stringify(packages)); loadPackageList(); if (typeof callback == 'function') { callback(id); } return; } } } } function countRemainingPackages() { var undelivered = 0; for (var i = 0; i < packages.length; i++) { for (var j = 0; j < packages[i].items.length; j++) { if (packages[i].items[j].delivered != true) { undelivered++; } } } return undelivered; } function countPackages() { var count = 0; for (var i = 0; i < packages.length; i++) { for (var j = 0; j < packages[i].items.length; j++) { count++; } } return count; } function addPackageByAddress(number, unit, street, citystate, zip, type, callback) { var requestfinished = false; var searchingdialogopen = false; var deadline = false; var ajaxlookup = function () { var geocodecache = getStorage("geocode_cache"); if (geocodecache == null) { geocodecache = "{}"; setStorage("geocode_cache", "{}"); } geocodecache = JSON.parse(geocodecache); var cachekey = number + " || " + street + " || " + citystate + " || " + zip + " || " + SETTINGS.itemtypes[type].allowedlocationtypes; if (unit != '') { cachekey = number + " || " + unit + " || " + street + " || " + citystate + " || " + zip + " || " + SETTINGS.itemtypes[type].allowedlocationtypes; } var cacheitem = geocodecache[cachekey]; var timestamp = Math.floor(Date.now() / 1000); if (typeof cacheitem != 'undefined') { if (cacheitem.added + SETTINGS.geocodecacheexpiry < timestamp) { console.log("Info", "Removing expired geocode cache item " + cachekey); delete geocodecache[cachekey]; setStorage("geocode_cache", JSON.stringify(geocodecache)); } else { console.log("Info", "Using cached geocode result", cacheitem); addPackage({ address: cacheitem.address, number: number, unit: unit, street: street, citystate: citystate, zip: zip }, cacheitem.latitude, cacheitem.longitude, type, callback, deadline); return; } } $.ajax({ url: SETTINGS.geocodeapi, dataType: 'json', data: { number: number, unit: unit, street: street, citystate: citystate, zip: zip, type: SETTINGS.itemtypes[type].allowedlocationtypes }, timeout: 15 * 1000, success: function (resp) { if (searchingdialogopen) { app.dialog.close(); searchingdialogopen = false; } requestfinished = true; if (resp.status == "OK") { if (resp.accuracy.ok) { addPackage({ address: resp.address.street, number: number, unit: unit, street: street, citystate: citystate, zip: zip }, resp.coords[0], resp.coords[1], type, callback, deadline); geocodecache[cachekey] = { number: resp.address.number, unit: unit, address: resp.address.street, latitude: resp.coords[0], longitude: resp.coords[1], added: Math.floor(Date.now() / 1000) }; setStorage("geocode_cache", JSON.stringify(geocodecache)); } else { playSound("error"); app.dialog.confirm( "The address \"" + address + "\" couldn't be reliably located. Add it anyways?", "Accuracy Warning", function (ok) { if (resp.address.street == "") { addPackage({ address: address, number: number, unit: unit, street: street, citystate: citystate, zip: zip }, resp.coords[0], resp.coords[1], type, callback, deadline); } else { addPackage({ address: resp.address.street, number: number, unit: unit, street: street, citystate: citystate, zip: zip }, resp.coords[0], resp.coords[1], type, callback, deadline); } } ); } } else { playSound("error"); app.dialog.alert(resp.message, "Error"); } }, error: function (jqXHR, status, errorThrown) { if (searchingdialogopen) { app.dialog.close(); searchingdialogopen = false; } requestfinished = true; playSound("error"); app.dialog.alert("There was a network issue while finding the address. Please try adding the item again.", "Error"); } }); // Open a loading message if there's a delay finding the address setTimeout(function () { if (!requestfinished) { app.dialog.preloader("Searching for address..."); searchingdialogopen = true; } }, 750); } var prelookup = function () { if (type == "express") { if (getStorage("deadlinealarm_minutes") == null) { setStorage("deadlinealarm_minutes", 20); } var minutes = getStorage("deadlinealarm_minutes"); app.dialog.create({ title: 'Express Item', text: 'Set a reminder for ' + minutes + ' minutes before:', buttons: [ { text: '10:30 AM', close: true }, { text: '12:00 PM', close: true }, { text: '3:00 PM', close: true }, { text: '6:00 PM', close: true }, { text: "No reminder", color: "red", close: true } ], verticalButtons: true, onClick: function (dialog, index) { deadline = new Date(); switch (index) { case 0: deadline.setMinutes(30); deadline.setHours(10); break; case 1: deadline.setMinutes(00); deadline.setHours(12); break; case 2: deadline.setMinutes(00); deadline.setHours(12 + 3); break; case 3: deadline.setMinutes(00); deadline.setHours(12 + 6); break; default: deadline = false; break; } if (deadline != false) { deadline = deadline.getTime() / 1000; } ajaxlookup(); } }).open(); } else { ajaxlookup(); } } var deliverable = isDeliverable(number, street); if (deliverable.ok) { prelookup(); } else { app.dialog.confirm( "A route note says this address " + deliverable.reason + ". Add item anyways?", "Confirm", function () { prelookup(); }, function () { // cancel } ); } } function addPackageByBarcode(barcode, type, callback) { var requestfinished = false; var searchingdialogopen = false; var deadline = false; var ajaxlookup = function () { $.ajax({ url: SETTINGS.geocodebarcodeapi, dataType: 'json', data: { code: barcode, type: SETTINGS.itemtypes[type].allowedlocationtypes }, timeout: 15 * 1000, success: function (resp) { if (searchingdialogopen) { app.dialog.close(); searchingdialogopen = false; } requestfinished = true; if (resp.status == "OK") { if (resp.accuracy.ok) { addPackage(resp.address.street, resp.coords[0], resp.coords[1], type, callback, deadline); } else { playSound("error"); app.dialog.alert("The scanned address couldn't be reliably located.", "Error"); } } else { playSound("error"); app.dialog.alert(resp.message, "Error"); } }, error: function (jqXHR, status, errorThrown) { if (searchingdialogopen) { app.dialog.close(); searchingdialogopen = false; } requestfinished = true; playSound("error"); app.dialog.alert("There was a network issue while looking up the barcode. Please try again.", "Error"); } }); // Open a loading message if there's a delay finding the address setTimeout(function () { if (!requestfinished) { app.dialog.preloader("Looking up barcode..."); searchingdialogopen = true; } }, 750); } var prelookup = function () { if (type == "express") { if (getStorage("deadlinealarm_minutes") == null) { setStorage("deadlinealarm_minutes", 20); } var minutes = getStorage("deadlinealarm_minutes"); app.dialog.create({ title: 'Express Item', text: 'Set a reminder for ' + minutes + ' minutes before:', buttons: [ { text: '10:30 AM', close: true }, { text: '12:00 PM', close: true }, { text: '3:00 PM', close: true }, { text: "No reminder", color: "red", close: true } ], verticalButtons: true, onClick: function (dialog, index) { deadline = new Date(); switch (index) { case 0: deadline.setMinutes(30); deadline.setHours(10); break; case 1: deadline.setMinutes(00); deadline.setHours(12); break; case 2: deadline.setMinutes(00); deadline.setHours(12 + 3); break; case 3: default: deadline = false; break; } if (deadline != false) { deadline = deadline.getTime() / 1000; } ajaxlookup(); } }).open(); } else { ajaxlookup(); } } prelookup(); } function checkDeadlines() { if (getStorage("deadlinealarm_minutes") == null) { setStorage("deadlinealarm_minutes", 20); } var minutes = getStorage("deadlinealarm_minutes"); var currentTime = new Date().getTime() / 1000; var deadlineTime = currentTime + (minutes * 60); for (i in packages) { for (j in packages[i].items) { var item = packages[i].items[j]; if (typeof item.deadline != 'undefined' && item.deadline != false && item.delivered != true) { if ((typeof item.deadlinealarmed == 'undefined' || item.deadlinealarmed != true) && item.deadline <= deadlineTime) { playSound("alert"); app.dialog.alert( "Item at " + item.address + " needs to be delivered by " + timestampToTimeString(item.deadline) + " (" + Math.floor((item.deadline - currentTime) / 60) + " minutes from now).", "Delivery Alarm", function () { } ); packages[i].items[j].deadlinealarmed = true; setStorage("packages", JSON.stringify(packages)); } } } } } setInterval(checkDeadlines, 15 * 1000);