var ds = ""; function dataURL(s) { return ds + s; } function htmlURL(s) { return s; } function error(sts) { var err = document.getElementById("error"); if(err) err.innerHTML = "Error "+sts; } // mk 1 caller has called dataURL // cls is the javascript class // must have constructor that takes json as only argument function loadJSON(url, cls, error) { var arr = []; var xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET", url, false); xmlhttp.send(); if (xmlhttp.status == 200) { // result is JSON array of ids var result = JSON.parse(xmlhttp.responseText); function process(value) { arr.push(new cls(value)); } result.forEach(process); return arr; } if(error) error(xmlhttp.status); return null; } function loadRawJSON(url,error) { var arr = []; var xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET", url, false); xmlhttp.send(); if (xmlhttp.status == 200) { var result = JSON.parse(xmlhttp.responseText); return result; } if(error) error(xmlhttp.status); return ""; } function getIgnition(status) { var sts; // inputs use negative logic 1 for off 0 for on if (status.length == 1) { // old format status is a single hex digit // (ignition on off is in 4th bit) sts = parseInt(status, 16) >> 3; } else { // new format status is a 2 or more hex digits // ignition on off is lowest bit sts = parseInt(status, 16) & 1; } // return 1 for on 0 for off if(sts == 1) { return 0; } return 1; } function getInputs(status) { var sts; // inputs use negative logic 1 for off 0 for on // 2 parts // move the inputs to the low order bits // invert logic back to normal if (status.length == 1) { // old format status is a single hex digit // inputs are in low order 3 bits (ignition on off is in 4th bit which we ignore sts = parseInt(status, 16) & 7; } else { // new format status is a 2 or more hex digits // ignition on off is lowest bit - all other bits are inputs // right shift 1 to discard ignition bit var sts = parseInt(status, 16) >> 1; } // turn back to normal logic by xoring with all bits set // mask bits depends on length of status // 4 bits per hex digit - but bit 0 not used // number of bits is 4n -1 3,7,11... // mask is (2**(4n)-1)-1 7,127,2047... sts ^= ((1 << (status.length *4)-1)-1); return sts; } // Classes and constuctors for above class Org { constructor(json) { this.oid = json.OrganisationID; this.name = json.Organisation_Name; } } // from machine class Vehicle { constructor(json) { this.mid = json.MachineID; this.oid = json.OwnerOrganisationRef; this.name = json.Model; // rest is temporary this.linecolor = '#ff00ff'; this.daytrips = []; // is an array of Trip this.trips = {}; // sequence -> trip lookup table this.alldaytrip; // transient this.currentLocation; // A Loc this.locs = []; this.events = []; } clearData() { this.daytrips = []; this.trips = {}; } setCurrent(loc) { this.currentLocation = loc; } getCurrent() { return this.currentLocation; } processLocData(loc, index, array) { let point = loc.point; var trip; if(index != 0) { trip = this.daytrips[this.daytrips.length -1]; } if(index == 0) // first point of first trip { this.alldaytrip = new Trip(this); this.alldaytrip.Title("All Day"); trip = new Trip(this); this.daytrips.push(trip); trip.Start(loc); this.alldaytrip.Start(loc); } else if(loc.seq != trip.seq) // new trip { // end previous trip trip.End(); // discard if only had 1 point if(trip.count == 1) this.daytrips.pop(); else //keep this.trips[trip.seq] = trip; // uncomment to include red markers in alldaytrip //alldaytrip.End(); // start a new trip (with same vehicle) trip = new Trip(this); this.daytrips.push(trip); trip.Start(loc); // if including red markers have green as well //alldaytrip.Start(loc); // and comment next line this.alldaytrip.Update(loc); } else { trip.Update(loc); this.alldaytrip.Update(loc); } if (index == array.length -1) { trip.End(); // discard if only had 1 point if(trip.count == 1) this.daytrips.pop(); else //keep this.trips[trip.seq] = trip; this.alldaytrip.End(); } } addAllEvents(ev, index, array) { this.alldaytrip.AddEvent(ev); // now add to the individual trip if found // we remove trips is vehicle didnt move // but there will still be events // they are only displayed in the all day trip var trip = this.trips[ev.seq]; if(trip) { //found trip.AddEvent(ev); } } createTrips() { var trip; this.locs.forEach(this.processLocData, this); if(this.events.length != 0) { this.events.forEach(this.addAllEvents, this); } this.daytrips.push(this.alldaytrip); } loadData(ctx) { // alert("Load Trip Data "+this.daytrips.length); if(this.daytrips.length == 0) // not already loaded { var url = dataURL("db?t=daytrips&vid="+ctx.vid()+"&day="+ctx.sqlday()); this.locs = loadJSON(url, Loc, ctx.setError); if(this.locs.length != 0) { var loc = this.locs[0]; // load events url = dataURL("db?t=daytripevents&vid="+loc.id+"&sn="+loc.sn+"&day="+ctx.sqlday()); this.events = loadJSON(url, Event, ctx.setError); // now process into trips this.createTrips(); // alert("Trips "+ this.daytrips.length); } } } } // a and b are javascript Date objects function dateDiffInDays(a, b) { const _MS_PER_DAY = 1000 * 60 * 60 * 24; // Discard the time and time-zone information. const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); return Math.floor((utc2 - utc1) / _MS_PER_DAY); } class NavContext { constructor(org) { this.orgid = org; this.orgName = this.loadOrgName(); this.init(); } loadOrgName() { var url = dataURL("db?t=orgname&id="+this.orgid); var result = loadJSON(url, Org, this.setError); return result[0].name; } init() { this.date = new Date(); // date we are displaying - today this.error = ""; this.vehicles = []; // 0 indexed array of Vehicles this.idvehicles = []; // vid indexed array of Vehicles this.vindex = 0; // currently selected vehicle this.vnames = []; //this.vid = 0; // from vehicles[vindex] this.loadVehicles(); } setError(sts) { this.error = "Failed "+sts; } clearError() { this.error = ""; } loadVehicleName(v) { this.vnames[v.mid] = v.name; this.idvehicles[v.mid] = v; } loadVehicles() { var url = dataURL("db?t=vehicles&id="+this.orgid); this.vehicles = loadJSON(url, Vehicle, this.setError); this.vehicles.forEach(this.loadVehicleName,this); } setCurrent(loc) { this.idvehicles[loc.id].setCurrent(loc); } loadCurrent() { var url = dataURL("db?t=last&id="+ctx.orgid); var lastlocs = loadJSON(url, Loc, this.setError); lastlocs.forEach(this.setCurrent,this); } vid() { return this.vehicles[this.vindex].mid; } vname() { if(this.vindex >= 0) return this.vehicles[this.vindex].name; alert("Index "+this.vindex); return "???"; } vehicle() { return this.vehicles[this.vindex]; } changeVehicle(index) { // if(index != this.vindex) { this.vindex = index; this.vehicle().loadData(this); // } } sqlday() { var today = new Date(); return dateDiffInDays(this.date,today); } localDate() { // ISO date is for UTC // so need to sutract timezone offset from date // to get local date let localdate = new Date(this.date.getTime() -(this.date.getTimezoneOffset() * 60*1000)); return localdate.toISOString().substring(0,10); } clear(vehicle) { vehicle.clearData(this); } clearVehicleData() { this.vehicles.forEach(this.clear); } setDate(d) { this.date = d; if(this.sqlday() < 0) { this.date = new Date(); } this.clearVehicleData(); this.changeVehicle(this.vindex); return this.sqlday(); } // set the sqlday for database access // and return the local timezone date in ISO format navDate(days) { var sqlday = this.sqlday(); let date = new Date(); if(sqlday > 0 || days < 0) { sqlday-= days; if (sqlday < 0) sqlday = 0; date.setDate(date.getDate()-sqlday); var newday = this.setDate(date); if(newday != sqlday) alert("SQLDAY="+sqlday+",newday="+newday); } return this.localDate(); } } // from device class IPAddress { constructor(json) { this.sn = json.navtrakka_SN; this.ip = json.navtrakka_IP; this.vid = json.navtrakka_MachineRef; this.oid = json.navtrakka_OrganisationRefl; } } class Loc { constructor(json) { this.id = json.vehicleid; this.sn = json.sn; this.seq = json.seq; this.status = json.status; this.speed = json.speed; this.ts = json.utctime; this.lat = json.lat; this.lon = json.lon; this.point = new ol.geom.Point([json.lon, json.lat]); } getIgnition() { var sts; // inputs use negative logic 1 for off 0 for on if (this.status.length == 1) { // old format status is a single hex digit // (ignition on off is in 4th bit) sts = parseInt(this.status, 16) >> 3; } else { // new format status is a 2 or more hex digits // ignition on off is lowest bit sts = parseInt(this.status, 16) & 1; } // return 1 for on 0 for off if(sts == 1) { return 0; } return 1; } getIgnitionString() { if(this.getIgnition()== 1) return "ignitionOn"; return "ignitionOff"; } } class Event { constructor(json) { this.id = json.vehicleid; this.sn = json.sn; this.seq = json.seq; this.status = json.status; this.ts = json.utctime; this.event = json.event; } } class StreetLoc { constructor(point,name,speedlimit) { this.point = point; this.name = name; this.speedlimit = speedlimit; } } // a track consists of Features // which can be repeated // start (geometry point) // line LineString // inputs (geometry point) // end (geometry point) var gpserror=12.0; // +/-meters function ColorFromSpeed(loc) { let speed = Number(loc.speed); let c = "#cc3232"; // over 110 if (speed < 110) c = "#db7b2b"; if (speed < 100) c = "#e7b416"; if (speed < 80) c = "#99c140"; if (speed < 60) c = "#2dc937"; return c; } class Trip { constructor(vehicle) { this.vehicle = vehicle; this.title = ""; this.start = 0; this.end = 0; this.seq = 0; this.track = []; this.eventhtml = ""; this.user = "Not Recorded"; this.count = 0; // number of points this.ucount = 0; this.snaptrack = []; this.snapradius = ""; this.osmr = ""; } Title(t) { this.title = t; } getTitle() { return this.title ? this.title : displayTime(this.start.ts) + " to "+displayTime(this.end.ts); } AddEvent(ev) { // add the headings if(this.eventhtml == "") { this.eventhtml = "
Event
Timestamp
\n"; } this.eventhtml += "
"+ev.event+"
"+ displayTime(ev.ts)+"
\n"; if(ev.event.startsWith("user")) { this.user = ev.event; } } // use existing track // so can have multiple start line end Start(loc) { this.start = loc; this.count++; this.end = loc; this.seq = loc.seq; this.track.push(new ol.Feature({ geometry: this.start.point, label: "start", loc: loc, name: this.vehicle.name })); this.ucount = 0; this.inputstatus = 0 this.inputstart = null; // check for on in first loc var sts = getInputs(loc.status); if(sts > 0) //input on { this.inputstatus = sts; this.inputstart = loc; } this.osmr += loc.lon+","+loc.lat; this.snapradius="&radiuses="+gpserror; } Update(loc,streetloc) { var streetname = ""; var speedlimit = ""; if(streetloc) { streetname = streetloc.name; speedlimit = streetloc.speedlimit; } // work around for database stuff up June 2025 // wrong vehicles were assigned // so when corrected tracks have 2 both correct and incorrect sn gps locations // ignore second sn if(loc.sn != this.start.sn) { return; } // if we were stopped (0 speed) // and we are still stopped // then dont move // ie change loc lat and long to same as end point if (this.end.speed == "0" && loc.speed == "0") { loc.lat = this.end.lat; loc.lon = this.end.lon; loc.point = this.end.point; } // if status has changed // and < 7 // something got turned on var prevsts = this.inputstatus; if(this.end.status != loc.status) { var sts = getInputs(loc.status); if((sts > 0) && (this.inputstatus == 0)) //input on { this.inputstatus = sts; this.inputstart = loc; // turn on line var ls = new ol.geom.LineString([this.end.point.getCoordinates(),loc.point.getCoordinates()]); if(deltaSeconds(this.end.ts, loc.ts) < 50) { // add a normal line //color: this.vehicle.linecolor, var iF = new ol.Feature({ geometry: ls, label: "line", vehicle: this.vehicle, street: streetname, linecolor: ColorFromSpeed(loc), speedlimit: speedlimit, style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: ColorFromSpeed(loc), width: 6 }) }), end: loc.ts, speed: loc.speed, name: "line" }); this.track.push(iF); this.ucount++; } else { // add a gap line var iF = new ol.Feature({ geometry: ls, label: "gap", name: "gap" }); this.track.push(iF); } this.count++; this.osmr += ";" + loc.lon+","+loc.lat; this.snapradius += ";"+gpserror; } else { if(this.inputstatus != 0) // input off { // we havent drawn any points since input on // draw catchup line from inputstart to here var ls = new ol.geom.LineString([this.inputstart.point.getCoordinates(),loc.point.getCoordinates()]); // add a normal line var iF = new ol.Feature({ geometry: ls, label: "line", vehicle: this.vehicle, street: streetname, speedlimit: speedlimit, linecolor: ColorFromSpeed(loc), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: this.vehicle.linecolor, width: 6 }) }), end: loc.ts, speed: loc.speed, name: "line" }); this.track.push(iF); this.ucount++; // end catchup line var inp = new ol.Feature({ geometry: loc.point, label: "inputs", start: this.inputstart.ts, end: loc.ts, status: this.inputstatus, name: this.vehicle.name }); this.track.push(inp); this.inputstatus = 0; } } } else // we need this else because of the catchup { // dont log any movement when input is on // so only track for on and off // this made gaps in the track because we put marker where it turns off if((this.inputstatus == 0) || (prevsts == 0)) { var ls = new ol.geom.LineString([this.end.point.getCoordinates(),loc.point.getCoordinates()]); if(deltaSeconds(this.end.ts, loc.ts) < 50) { // add a normal line var iF = new ol.Feature({ geometry: ls, label: "line", vehicle: this.vehicle, street: streetname, speedlimit: speedlimit, linecolor: ColorFromSpeed(loc), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: this.vehicle.linecolor, width: 6 }) }), end: loc.ts, speed: loc.speed, name: "line" }); this.track.push(iF); this.ucount++; } else { // add a gap line var iF = new ol.Feature({ geometry: ls, label: "gap", name: "gap" }); this.track.push(iF); } this.count++; this.osmr += ";" + loc.lon+","+loc.lat; this.snapradius += ";"+gpserror; } } this.end = loc; } End() { this.track.push(new ol.Feature({ geometry: this.end.point, label: "end", loc: this.end, name: this.vehicle.name })); } AppendSnap(prev,curr,gap) { var ls = new ol.geom.LineString([prev.getCoordinates(),curr.getCoordinates()]); // add a normal line var iF; if(gap != 1) { iF = new ol.Feature({ geometry: ls, label: "gap", name: "gap" }); } else { iF = new ol.Feature({ geometry: ls, label: "line", vehicle: this.vehicle, linecolor: ColorFromSpeed(loc), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: this.vehicle.linecolor, width: 6 }) }), name: "line" }); } this.snaptrack.push(iF); } } // query string support var urlParams = {}; function parseQuery() { var match, pl = /\+/g, // Regex for replacing addition symbol with a space search = /([^&=]+)=?([^&]*)/g, decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }, query = window.location.search.substring(1); var i = 0; while (match = search.exec(query)) { urlParams[decode(match[1])] = decode(match[2]); } } (window.onpopstate = parseQuery)(); function getParam(name, def) { return (typeof urlParams[name] !== 'undefined') ? urlParams['orgid'] : def; } // time functions // ********************************** // ********************************** // ********************************** // // all times from database are returned // in timezone of database server // // ********************************** // ********************************** // ********************************** // extract hh:mm:ss form timestamp function displayTime(ts) { return ts.substring(11,19); } // ts2 - ts1 function deltaSeconds(ts1, ts2) { // extract hh:mm:ss var t1 = displayTime(ts1); var t2 = displayTime(ts2); var sec1 = (parseInt(t1.substring(0,2), 10) * 3600) + (parseInt(t1.substring(3,5), 10) * 60) + (parseInt(t1.substring(6,8), 10)); var sec2 = (parseInt(t2.substring(0,2), 10) * 3600) + (parseInt(t2.substring(3,5), 10) * 60) + (parseInt(t2.substring(6,8), 10)); // if u2 < u1 day has ticked over - add a day // incomplete since dont use these // if(t2 < t1) // t2 += 86400; return (sec2 - sec1); } // Date Functions