diff --git a/MMM-LocalTransport.js b/MMM-LocalTransport.js
index 1875d9f..930e0b5 100644
--- a/MMM-LocalTransport.js
+++ b/MMM-LocalTransport.js
@@ -3,296 +3,454 @@
* Module: MMM-LocalTransport
*
* By Christopher Fenner https://github.com/CFenner
- * style options by Lasse Wollatz
+ * style options and calendar extension by Lasse Wollatz https://github.com/GHLasse
* MIT Licensed.
*/
Module.register('MMM-LocalTransport', {
- defaults: {
- maximumEntries: 3,
- displayStationLength: 0,
- displayWalkType: 'short',
- displayArrival: true,
- maxWalkTime: 10,
- fade: true,
- fadePoint: 0.1,
- showColor: true,
- maxModuleWidth: 0,
- animationSpeed: 1,
- updateInterval: 5,
- language: config.language,
- units: config.units,
- timeFormat: config.timeFormat,
- mode: 'transit',
- traffic_model: 'best_guess',
- departure_time: 'now',
- alternatives: true,
- apiBase: 'https://maps.googleapis.com/',
- apiEndpoint: 'maps/api/directions/json',
- debug: false
- },
- start: function() {
- Log.info('Starting module: ' + this.name);
- this.loaded = false;
- this.url = this.config.apiBase + this.config.apiEndpoint + this.getParams();
- var d = new Date();
- this.lastupdate = d.getTime() - 2 * this.config.updateInterval * 60 * 1000;
- this.update();
- // refresh every 0.25 minutes
- setInterval(
- this.update.bind(this),
- 15 * 1000);
- },
- update: function() {
- //updateDOM
- var dn = new Date();
- if (dn.getTime() - this.lastupdate >= this.config.updateInterval * 60 * 1000){
- //perform main update
- //request routes from Google
+ defaults: {
+ maximumEntries: 3, //maximum number of Routes to display
+ displayStationLength: 0,
+ displayWalkType: 'short',
+ displayArrival: true, //display arrival time
+ displayAltWalk: false, //display info about how long walking would take
+ displayAltCycle: false, //display info about how long cycling would take
+ displayAltDrive: false, //display info about how long driving would take
+ displayAltTransit: false, //display info about how long public transport does take
+ maxWalkTime: 10, //maximum acceptable walking time between stations
+ fade: true, //apply fading effect
+ fadePoint: 0.1,
+ showColor: true, //display transport icons in colour
+ maxModuleWidth: 0,
+ animationSpeed: 1,
+ updateInterval: 5, //in min, how often to request new routes
+ language: config.language,
+ units: config.units,
+ timeFormat: config.timeFormat,
+ mode: 'transit',
+ traffic_model: 'best_guess',
+ departure_time: 'now',
+ alternatives: true,
+ getCalendarLocation: false, //read calendar notifications to display the route to the next event.
+ api_key: 'YOUR_API_KEY',
+ apiBase: 'https://maps.googleapis.com/',
+ apiEndpoint: 'maps/api/directions/json',
+ debug: false,
+ ignoreErrors: ["OK","OVER_QUERY_LIMIT","UNKNOWN_ERROR"], //any of: OK, NOT_FOUND, ZERO_RESULTS, MAX_WAYPOINTS_EXCEEDED, INVALID_REQUEST, OVER_QUERY_LIMIT, REQUEST_DENIED, UNKNOWN_ERROR
+ _laststop: '', //the variables with _ are for internal use only - should consider defining them elsewhere!
+ _headerDest: '',
+ _headerDestPlan: '',
+ _headerOrigPlan: '',
+ _destination: '', //actual destination requested from Google
+ test: ""
+ },
+ start: function() {
+ Log.info('Starting module: ' + this.name);
+ //default certain global variables
+ this.loaded = false;
+ this.ignoredError = false;
+ this.altTimeWalk = 'unknown';
+ this.altTimeCycle = 'unknown';
+ this.altTimeTransit = 'unknown';
+ this.altTimeDrive = 'unknown';
+ this.altSymbols = ['walk','cycle','rail','drive'];
+ this.altModes = ['walking','bicycling','transit','driving'];
+
+
+ if (this.config.api_key === 'YOUR_API_KEY'){
+ /*if there is no api key specified in the options of the module,
+ look it up in the main config-file settings */
+ this.config.api_key = config.apiKeys.google;
+ }
+ this.config._destination = this.config.destination;
+ this.url = this.config.apiBase + this.config.apiEndpoint + this.getParams();
+ var d = new Date();
+ this.lastupdate = d.getTime() - 2 * this.config.updateInterval * 60 * 1000;
+ this.update();
+ // refresh every 0.25 minutes so that the time left till departure is always correct without having to request an update from Google every time.
+ setInterval(
+ this.update.bind(this),
+ 15 * 1000);
+ },
+ update: function() {
+ //updateDOM
+ var dn = new Date();
+ if (dn.getTime() - this.lastupdate >= this.config.updateInterval * 60 * 1000){
+ //perform main update
+ this.doMainUpdate();
+ this.lastupdate = dn.getTime();
+ }else{
+ //perform minor update
+ //only update time
+ if (this.config.debug){
+ this.sendNotification("SHOW_ALERT", {timer: 3000, title: "LOCAL TRANSPORT", message: "normal update"});
+ }
+ //this.loaded = false;
+ this.updateDom(); //this.updateDom(this.config.animationSpeed * 1000)
+ }
+ },
+ sendRequest: function(requestName, mode) {
+ this.config.mode = mode;
this.sendSocketNotification(
- 'LOCAL_TRANSPORT_REQUEST', {
+ requestName, {
id: this.identifier,
url: this.config.apiBase + this.config.apiEndpoint + this.getParams()
}
);
- if (this.config.debug){
- this.sendNotification("SHOW_ALERT", { timer: 3000, title: "LOCAL TRANSPORT", message: "special update"});
+ Log.log("requested "+requestName);
+ this.config.mode = 'transit';
+ },
+ doMainUpdate: function() {
+ /*doMainUpdate
+ *requests routes from Google for public transport and any alternatives if applicable.
+ */
+ //request routes from Google
+ this.loaded = false;
+ this.sendRequest('LOCAL_TRANSPORT_REQUEST',this.config.mode);
+ //request walking time
+ if (this.config.displayAltWalk){
+ this.sendRequest('LOCAL_TRANSPORT_WALK_REQUEST','walking');
+ }
+ //request cycling time
+ if (this.config.displayAltCycle){
+ this.sendRequest('LOCAL_TRANSPORT_CYCLE_REQUEST','bicycling');
+ }
+ //request driving time
+ if (this.config.displayAltDrive){
+ this.sendRequest('LOCAL_TRANSPORT_DRIVE_REQUEST','driving');
}
- this.lastupdate = dn.getTime();
- }else{
- //perform minor update
- //only update time
if (this.config.debug){
- this.sendNotification("SHOW_ALERT", {timer: 3000, title: "LOCAL TRANSPORT", message: "normal update"});
+ this.sendNotification("SHOW_ALERT", { timer: 3000, title: "LOCAL TRANSPORT", message: "special update"});
}
- this.loaded = true;
- this.updateDom(); //this.updateDom(this.config.animationSpeed * 1000)
- }
- },
- getParams: function() {
- var params = '?';
- params += 'mode=' + this.config.mode;
- params += '&origin=' + this.config.origin;
- params += '&destination=' + this.config.destination;
- params += '&key=' + this.config.api_key;
- params += '&traffic_model=' + this.config.traffic_model;
- params += '&departure_time=now';
- params += '&alternatives=true';
- return params;
- },
- renderLeg: function(wrapper, leg){
- /* renderLeg
- * creates HTML element for one leg of a route
- */
- var depature = leg.departure_time.value * 1000;
- var arrival = leg.arrival_time.value * 1000;
- //var depadd = leg.start_address;
- var span = document.createElement("div");
- span.className = "small bright";
- span.innerHTML = moment(depature).locale(this.config.language).fromNow();
- // span.innerHTML += "from " + depadd;
- if (this.config.displayArrival && this.config.timeFormat === 24){
- span.innerHTML += " ("+this.translate("ARRIVAL")+": " + moment(arrival).format("H:mm") + ")";
- }else if(this.config.displayArrival){
- span.innerHTML += " ("+this.translate("ARRIVAL")+": " + moment(arrival).format("h:mm") + ")";
- }
- // span.innerHTML += this.translate('TRAVEL_TIME') + ": ";
- // span.innerHTML += moment.duration(moment(arrival).diff(depature, 'minutes'), 'minutes').humanize();
- wrapper.appendChild(span);
- },
- renderStep: function(wrapper, step){
- /* renderStep
- * creates HTML element for one step of a leg
- */
- if(step.travel_mode === "WALKING"){
- /*this step is not public transport but walking*/
- var duration = step.duration.value;
- if (duration >= (this.config.maxWalkTime*60)){
- /*if time of walking is longer than
- *specified, mark this route to be skipped*/
- wrapper.innerHTML = "too far";
- }else if(this.config.displayWalkType != 'none'){
- /*if walking and walking times should be
- *specified, add symbol and time*/
- var img = document.createElement("img");
- if(this.config.showColor){
- img.className = "symbol";
+ },
+ getParams: function() {
+ var params = '?';
+ params += 'mode=' + this.config.mode;
+ params += '&origin=' + this.config.origin;
+ params += '&destination=' + this.config._destination;
+ params += '&key=' + this.config.api_key;
+ params += '&traffic_model=' + this.config.traffic_model;
+ params += '&departure_time=now';
+ params += '&alternatives=true';
+ return params;
+ },
+ convertTime: function(seconds){
+ var ans = moment.duration(seconds, 'seconds').locale(this.config.language).humanize();
+ if(this.config.displayWalkType === 'short'){
+ ans = ans.replace(this.translate("MINUTE_PL"),this.translate("MINUTE_PS"));
+ ans = ans.replace(this.translate("MINUTE_SL"),this.translate("MINUTE_SS"));
+ ans = ans.replace(this.translate("SECOND_PL"),this.translate("SECOND_PS"));
+ }
+ return ans;
+ },
+ renderStep: function(wrapper, step){
+ /* renderStep
+ * creates HTML element for one step of a leg
+ */
+ if(step.travel_mode === "WALKING"){
+ /*this step is not public transport but walking*/
+ var duration = step.duration.value;
+ if (duration >= (this.config.maxWalkTime*60)){
+ /*if time of walking is longer than
+ *specified, mark this route to be skipped*/
+ wrapper.innerHTML = "too far";
+ }else if(this.config.displayWalkType != 'none'){
+ /*if walking and walking times should be
+ *specified, add symbol and time*/
+ wrapper.appendChild(getSymbol("walk",this.config.showColor));
+ var span = document.createElement("span");
+ span.innerHTML = this.convertTime(duration);
+ if (this.config._laststop !== ''){
+ /* walking step doesn't have a departure_stop set - maybe something else but can't find the documentation right now.
+ so in order to display the departure, we will just save the arrival of any transit step into a global variable and
+ display the previous arrival instead of the current departure location. That means we need to reset the global variable
+ to not cause interference between different routes and we need to skip the display for the first step if that is a walking
+ step (alternatively one could display the departure location specified by the user, but I prefer this option)
+ */
+ renderDeparture(span, this.config._laststop, this.translate("FROM"), this.config);
+ }
+ span.className = "xsmall dimmed";
+ wrapper.appendChild(span);
}else{
- img.className = "symbol bw";
+ /*skip walking*/
+ return;
}
- img.src = "http://maps.gstatic.com/mapfiles/transit/iw2/6/walk.png";
- //img.src = "/localtransport/walk.png"; //needs to be saved in localtransport/public/walk.png
- wrapper.appendChild(img)
- var span = document.createElement("span");
- span.innerHTML = moment.duration(duration, 'seconds').locale(this.config.language).humanize();
- if(this.config.displayWalkType === 'short'){
- span.innerHTML = span.innerHTML.replace(this.translate("MINUTE_PL"),this.translate("MINUTE_PS"));
- span.innerHTML = span.innerHTML.replace(this.translate("MINUTE_SL"),this.translate("MINUTE_SS"));
- span.innerHTML = span.innerHTML.replace(this.translate("SECOND_PL"),this.translate("SECOND_PS"));
+ this.config._laststop = '';
+ }else{
+ /*if this is a transit step*/
+ var details = step.transit_details;
+ if(details) {
+ /*add symbol of transport vehicle*/
+ var img = document.createElement("img");
+ if(this.config.showColor){
+ img.className = "symbol";
+ }else{
+ img.className = "symbol bw";
+ }
+ /* get symbol online*/
+ img.src = details.line.vehicle.local_icon || ("https:" + details.line.vehicle.icon);
+ /* can provide own symbols under /localtransport/public/*.png */
+ //img.src = "/localtransport/" + details.line.vehicle.name + ".png";
+ img.alt = "[" + details.line.vehicle.name +"]";
+ wrapper.appendChild(img);
+ /*add description*/
+ var span = document.createElement("span");
+ /* add line name*/
+ span.innerHTML = details.line.short_name || details.line.name;
+ renderDeparture(span, details.departure_stop.name, this.translate("FROM"), this.config);
+ if (this.config.debug){
+ /* add vehicle type for debug*/
+ span.innerHTML += " [" + details.line.vehicle.name +"]";
+ }
+ this.config._laststop = details.arrival_stop.name;
+ span.className = "xsmall dimmed";
+ wrapper.appendChild(span);
}
- span.className = "xsmall dimmed";
- wrapper.appendChild(span);
+ }
+ },
+
+ renderAlternativeTime: function(time){
+ var span = document.createElement("span");
+ if(time != "unknown"){
+ span.innerHTML = this.convertTime(time);
}else{
- /*skip walking*/
- return;
+ span.innerHTML = "n/a";
}
- }else{
- /*if this is a transit step*/
- var details = step.transit_details;
- if(details) {
- /*add symbol of transport vehicle*/
- var img = document.createElement("img");
- if(this.config.showColor){
- img.className = "symbol";
- }else{
- img.className = "symbol bw";
+ return span;
+ },
+
+ renderAlternatives: function(){
+ /*creates a
containing the duration for ALTERNATIVE transport methods*/
+ var li = document.createElement("li");
+ li.className = "small";
+ //intro
+ var span = document.createElement("span");
+ span.innerHTML = this.translate("ALT") + ": ";
+ li.appendChild(span);
+
+ /*add alternative times*/
+ var displayAlt = [this.config.displayAltWalk,this.config.displayAltCycle,this.config.displayAltTransit,this.config.displayAltDrive];
+ var timeAlt = [this.altTimeWalk,this.altTimeCycle,this.altTimeTransit,this.altTimeDrive];
+ for (i = 0; i < displayAlt.length; i++) {
+ if (displayAlt[i]){
+ li.appendChild(getSymbol(this.altSymbols[i],this.config.showColor));
+ var span = this.renderAlternativeTime(timeAlt[i]);
+ li.appendChild(span);
}
- /* get symbol online*/
- img.src = details.line.vehicle.local_icon || ("http:" + details.line.vehicle.icon);
- /* can provide own symbols under /localtransport/public/*.png */
- //img.src = "/localtransport/" + details.line.vehicle.name + ".png";
- img.alt = "[" + details.line.vehicle.name +"]";
- wrapper.appendChild(img);
- /*add description*/
- var span = document.createElement("span");
- /* add line name*/
- span.innerHTML = details.line.short_name || details.line.name;
- if (this.config.displayStationLength > 0){
- /* add departure stop (shortened)*/
- span.innerHTML += " ("+this.translate("FROM")+" " + this.shorten(details.departure_stop.name, this.config.displayStationLength) + ")";
- }else if (this.config.displayStationLength === 0){
- /* add departure stop*/
- span.innerHTML += " ("+this.translate("FROM")+" " + details.departure_stop.name + ")";
- }
- if (this.config.debug){
- /* add vehicle type for debug*/
- span.innerHTML += " [" + details.line.vehicle.name +"]";
- }
- span.className = "xsmall dimmed";
- wrapper.appendChild(span);
}
- }
- },
- socketNotificationReceived: function(notification, payload) {
- if (notification === 'LOCAL_TRANSPORT_RESPONSE' && payload.id === this.identifier) {
- Log.info('received' + notification);
+ return li;
+ },
+ receiveMain: function(notification, payload){
+ var errlst = this.config.ignoreErrors;
if(payload.data && payload.data.status === "OK"){
+ //API request returned a result -> we are happy
+ Log.log('received ' + notification);
this.info = payload.data;
this.loaded = true;
+ this.ignoredError = false;
+ this.config._headerDestPlan = shortenAddress(this.config._destination);
+ //this.altTimeTransit = this.receiveAlternative(notification, payload);
+ this.altTimeTransit = receiveAlternative(notification, payload, this.config.ignoreErrors);
+ this.updateDom(this.config.animationSpeed * 1000);
+ }else if(!payload.data){
+ //API request returned nothing
+ this.loaded = false;
+ this.ignoredError = false;
+ }else if(this.info && errlst.indexOf(payload.data.status) >= 0){
+ //API request returned an error but we have previous results and permission to ignore it
+ Log.info('received ' + notification + ' with status '+payload.data.status);
+ this.ignoredError = true;
+ this.loaded = true;
+ this.updateDom(this.config.animationSpeed * 1000);
+ }else{
+ //API request returned an error so we don't have any routes to display -> show error
+ Log.warn('received ' + notification + ' with status '+payload.data.status);
+ this.ignoredError = false;
+ this.info = payload.data;
+ this.loaded = false;
this.updateDom(this.config.animationSpeed * 1000);
}
- }
- },
- getStyles: function() {
- return ["localtransport.css"];
- },
- getScripts: function() {
- return ["moment.js"];
- },
- getTranslations: function() {
- return {
- de: "i18n/de.json",
- en: "i18n/en.json",
- sv: "i18n/sv.json",
- fr: "i18n/fr.json"
- };
- },
- getDom: function() {
- /* main function creating HTML code to display*/
- var wrapper = document.createElement("div");
- if (!this.loaded) {
- /*if not loaded, display message*/
- wrapper.innerHTML = this.translate("LOADING_CONNECTIONS");
- wrapper.className = "small dimmed";
- }else{
- /*create an unsorted list with each
- *route alternative being a new list item*/
- //var udt = document.createElement("div");
- //udt.innerHTML = moment().format("HH:mm:ss") + " (" + this.lastupdate + ")";
- //wrapper.appendChild(udt);
- var ul = document.createElement("ul");
- var Nrs = 0; //number of routes
- var routeArray = []; //array of all alternatives for later sorting
- for(var routeKey in this.info.routes) {
- /*each route describes a way to get from A to Z*/
- //if(Nrs >= this.config.maxAlternatives){
- // break;
- //}
- var route = this.info.routes[routeKey];
- var li = document.createElement("li");
- li.className = "small";
- var arrival = 0;
- if (this.config.maxModuleWidth > 0){
- li.style.width = this.config.maxModuleWidth + "px";
+ },
+ socketNotificationReceived: function(notification, payload) {
+ /*socketNotificationReceived
+ *handles notifications send by this module
+ */
+ if (payload.id === this.identifier){
+ switch(notification){
+ case 'LOCAL_TRANSPORT_RESPONSE':
+ //Received response on public transport routes (main one)
+ this.receiveMain(notification, payload);
+ break;
+ case 'LOCAL_TRANSPORT_WALK_RESPONSE':
+ //Received response on walking alternative
+ //this.altTimeWalk = this.receiveAlternative(notification, payload);
+ this.altTimeWalk = receiveAlternative(notification, payload, this.config.ignoreErrors);
+ this.updateDom();
+ break;
+ case 'LOCAL_TRANSPORT_CYCLE_RESPONSE':
+ //Received response on cycling alternative
+ //this.altTimeCycle = this.receiveAlternative(notification, payload);
+ this.altTimeCycle = receiveAlternative(notification, payload, this.config.ignoreErrors);
+ this.updateDom();
+ break;
+ case 'LOCAL_TRANSPORT_DRIVE_RESPONSE':
+ //Received response on driving alternative
+ //this.altTimeDrive = this.receiveAlternative(notification, payload);
+ this.altTimeDrive = receiveAlternative(notification, payload, this.config.ignoreErrors);
+ this.updateDom();
}
- for(var legKey in route.legs) {
- var leg = route.legs[legKey];
- arrival = leg.arrival_time.value;
- var tmpwrapper = document.createElement("text");
- for(var stepKey in leg.steps) {
- /*each leg consists of several steps
- *e.g. (1) walk from A to B, then
- (2) take the bus from B to C and then
- (3) walk from C to Z*/
- var step = leg.steps[stepKey];
- this.renderStep(tmpwrapper, step);
+ }
+ },
+ calendarReceived: function(notification, payload, sender) {
+ Log.info('received ' + notification);
+ var dn = new Date();
+ //var oneSecond = 1000; // 1,000 milliseconds
+ //var oneMinute = oneSecond * 60;
+ //var oneHour = oneMinute * 60;
+ var oneDay = 1000 * 60 * 60 * 24;
+
+ var value;
+ for (let x of payload) {
+ //go through the events, pick first one with a set location.
+ //also ignore running events
+ if(x.location !== '' && x.location !== false && x.startDate - dn >= 0){
+ value = x;
+ break;
+ }
+ }
+ //check if event is less than one day away
+ if(value.startDate - dn - oneDay <= 0){
+ //if so, then update the destination and request an update of the routes
+ this.config._destination = value.location;
+ this.doMainUpdate();
+ this.lastupdate = dn.getTime();
+ }else{
+ //if not, then make sure the default destination is set again.
+ this.config._destination = this.config.destination;
+ }
+ },
+ notificationReceived: function(notification, payload, sender) {
+ /*notificationReceived
+ *handles notifications send by other modules
+ */
+ if (notification === 'CALENDAR_EVENTS' && sender.name === 'calendar' && this.config.getCalendarLocation) {
+ //received calendar events AND user wants events to influence the travel destination.
+ this.calendarReceived(notification, payload, sender);
+ }
+ },
+ getHeader: function() {
+ var header = this.data.header;
+ //use %{destX} in the header definition for the module and it will be replaces with the destination as returned by Google
+ header = header.replace("%{destX}", this.config._headerDest);
+ //use %{dest} in the header definition for the module and it will be replaces with the destination as defined in the config/ calendar event
+ if(!this.ignoredError){
+ header = header.replace("%{dest}", this.config._headerDestPlan);
+ }else{ //in case the last request returned an error an we use the previous data, then better use the destination of the last request as well.
+ header = header.replace("%{dest}", this.config._headerDest);
+ }
+ //use %{orig} in the header definition for the module and it will be replaces with the origin as defined in the config
+ this.config._headerOrigPlan = shortenAddress(this.config.origin);
+ header = header.replace("%{orig}", this.config._headerOrigPlan);
+
+ return header;
+ },
+ getStyles: function() {
+ return ["localtransport.css"];
+ },
+ getScripts: function() {
+ return ["moment.js","helper.js"];
+ },
+ getTranslations: function() {
+ return {
+ en: "i18n/en.json",
+ de: "i18n/de.json",
+ sv: "i18n/sv.json",
+ fr: "i18n/fr.json"
+ };
+ },
+ getDom: function() {
+ /* main function creating HTML code to display*/
+ this.config._headerDest = this.config._headerDestPlan; //resetting _headerDest in case there was an error loading...
+ var wrapper = document.createElement("div");
+ if (!this.loaded && !this.info) {
+ /*if not loaded, display message*/
+ wrapper.innerHTML = this.translate("LOADING_CONNECTIONS");
+ wrapper.className = "small dimmed";
+ }else if (!this.loaded){
+ /*if not loaded, display message*/
+ wrapper.innerHTML = this.translate(this.info.status);
+ wrapper.className = "small dimmed";
+ }else{
+ /*we have routes -> render them*/
+ var routeArray = []; //array of all alternatives for later sorting
+ for(var routeKey in this.info.routes) {
+ /*each route describes a way to get from A to Z*/
+ var route = this.info.routes[routeKey];
+ var li = document.createElement("li");
+ li.className = "small";
+ var arrival = 0;
+ if (this.config.maxModuleWidth > 0){
+ li.style.width = this.config.maxModuleWidth + "px";
+ }
+ for(var legKey in route.legs) {
+ var leg = route.legs[legKey];
+ var tmpwrapper = document.createElement("text");
+ this.config._headerDest = shortenAddress(leg.end_address);
+ this.config._laststop = ''; //need to reset the _laststop in case the previous leg didn't end with a walking step.
+ try {
+ arrival = leg.arrival_time.value;
+ for(var stepKey in leg.steps) {
+ /*each leg consists of several steps
+ *e.g. (1) walk from A to B, then
+ (2) take the bus from B to C and then
+ (3) walk from C to Z*/
+ var step = leg.steps[stepKey];
+ this.renderStep(tmpwrapper, step);
+ //renderStep(tmpwrapper, step, this.translate("FROM"), config);
+ if (tmpwrapper.innerHTML === "too far"){
+ //walking distance was too long -> skip this option
+ break;
+ }
+ }
+ }
+ catch(err) {
+ tmpwrapper.innerHTML = "too far";
+ }
if (tmpwrapper.innerHTML === "too far"){
//walking distance was too long -> skip this option
+ li.innerHTML = "too far";
break;
}
+ //this.renderLeg(li, leg);
+ renderLeg(li, leg, this.translate("ARRIVAL"), this.config, );
+ li.appendChild(tmpwrapper);
}
- if (tmpwrapper.innerHTML === "too far"){
- //walking distance was too long -> skip this option
- li.innerHTML = "too far";
- break;
+ if (li.innerHTML !== "too far"){
+ routeArray.push({"arrival":arrival,"html":li});
}
- this.renderLeg(li, leg);
- li.appendChild(tmpwrapper);
- }
- if (li.innerHTML !== "too far"){
- routeArray.push({"arrival":arrival,"html":li});
- Nrs += 1;
}
- }
-
- /*sort the different alternative routes by arrival time*/
- routeArray.sort(function(a, b) {
- return parseFloat(a.arrival) - parseFloat(b.arrival);
- });
- /*only show the first few options as specified by "maximumEntries"*/
- routeArray = routeArray.slice(0, this.config.maximumEntries);
- /*create fade effect and append list items to the list*/
- var e = 0;
- Nrs = routeArray.length;
- for(var dataKey in routeArray) {
- var routeData = routeArray[dataKey];
- var routeHtml = routeData.html;
- // Create fade effect.
- if (this.config.fade && this.config.fadePoint < 1) {
- if (this.config.fadePoint < 0) {
- this.config.fadePoint = 0;
- }
- var startingPoint = Nrs * this.config.fadePoint;
- var steps = Nrs - startingPoint;
- if (e >= startingPoint) {
- var currentStep = e - startingPoint;
- routeHtml.style.opacity = 1 - (1 / steps * currentStep);
- }
+ /*sort the different alternative routes by arrival time*/
+ routeArray.sort(function(a, b) {
+ return parseFloat(a.arrival) - parseFloat(b.arrival);
+ });
+ /*only show the first few options as specified by "maximumEntries"*/
+ routeArray = routeArray.slice(0, this.config.maximumEntries);
+
+ /*add ALTERNATIVE transport notes*/
+ if (this.config.displayAltWalk || this.config.displayAltCycle || this.config.displayAltDrive){
+ var li = this.renderAlternatives();
+ routeArray.push({"arrival":'',"html":li});
}
- ul.appendChild(routeHtml);
- e += 1;
+
+ /*create an unsorted list with each
+ *route alternative being a new list item*/
+ //var ul = document.createElement("ul");
+
+ /*create fade effect and append list items to the list*/
+ var ul = renderFade(routeArray,this.config);
+ wrapper.appendChild(ul);
}
- wrapper.appendChild(ul);
- }
- return wrapper;
- },
- shorten: function(string, maxLength) {
- /*shorten
- *shortens a string to the number of characters specified*/
- if (string.length > maxLength) {
- return string.slice(0,maxLength) + "…";
+ return wrapper;
}
- return string;
- }
-});
+});
\ No newline at end of file
diff --git a/README.md b/README.md
index 5580ce4..6e414da 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,12 @@ Add module configuration to config.js.
},
```
+For the header, the following keys can be specified and will be replaced with the according values at runtime:
+use `%{orig}` in the header definition for the module and it will be replaces with the origin as defined in the config
+use `%{dest}` in the header definition for the module and it will be replaces with the destination as defined in the config/ calendar event
+use `%{destX}` in the header definition for the module and it will be replaces with the destination as returned by Google
+These options are particularly useful if the destination is changed according to the calendar. The `%{orig}` option may become useful in the future.
+
|Option|Description|
|---|---|
|`apiKey`|The API key, which can be obtained [here](https://developers.google.com/maps/documentation/directions/).
This value is **REQUIRED**|
@@ -49,8 +55,12 @@ Add module configuration to config.js.
|`maximumEntries`|Maximum number of routes to display. Less routes will be shown if less are returned by Google. Routes will be sorted by arrival time. This option should take an integer value between `1` and `5`.
**Default value:** `3`|
|`updateInterval`|How often does the content need to be fetched? (Minutes) Note that the module refreshes every 15 seconds to always display an accurate estimate when you need to leave.
**Default value:** `5`|
|`animationSpeed`|Speed of the update animation. (Seconds)
**Default value:** `1`|
+|`getCalendarLocation`|Boolean to specify whether to read calendar notifications send by the default calendar module to display the route to the next event. The event must be less than one day away and contain a location to be considered for this functionality.
**Default value:** `false`|
|`displayStationLength`|Number of characters of the departure station for each transport mode to display.
`0` means display all,
`-1` means don't show the departure station
**Default value:** `0`|
|`displayArrival`|Boolean if the arrival time should be shown in the header for each option.
**Default value:** `true`|
+|`displayAltWalk`|Boolean if a note should be shown with the time it would take to walk instead of using public transport.
**Default value:** `false`|
+|`displayAltCycle`|Boolean if a note should be shown with the time it would take to take a bicycle instead of using public transport.
**Default value:** `false`|
+|`displayAltDrive`|Boolean if a note should be shown with the time it would take to drive instead of using public transport.
**Default value:** `false`|
|`displayWalkType`|String how detailed the walking segments should be shown.
`'none'` means to not display anything,
`'short'` means to display the symbol, the time and the short version of the unit or
`'long'` means that a symbol, the time and the long string for the unit is displayed. This options is default if an invalid string is supplied.
**Default value:** `'short'`|
|`maxWalkTime`|Maximum time you are willing to walk between stations in minutes
**Default value:** `10`|
|`maxModuleWidth`|Maximum width of the module in pixel. Unlimited when set to `0`. This option can be used to make the module shorter in case of very long lines for directions.
**Default value:** `0`|
diff --git a/helper.js b/helper.js
new file mode 100644
index 0000000..cee44be
--- /dev/null
+++ b/helper.js
@@ -0,0 +1,194 @@
+/* small collection of functions that do basic things
+ * main purpose is to reduce the number of functions per file
+ */
+
+function getSymbol(symName,showColor){
+ var img = document.createElement("img");
+ if(showColor){
+ img.className = "symbol";
+ }else{
+ img.className = "symbol bw";
+ }
+ img.src = "https://maps.gstatic.com/mapfiles/transit/iw2/6/"+symName+".png";
+ //img.src = "/localtransport/"+symName+".png"; //needs to be saved in localtransport/public/walk.png
+ return img;
+}
+
+
+function shorten(string, maxLength) {
+ /*shorten
+ *shortens a string to the number of characters specified*/
+ if (string.length > maxLength) {
+ return string.slice(0,maxLength) + "…";
+ }
+ return string;
+}
+
+function shortenAddress(address) {
+ /*shortenAddress
+ *shortens a string to the first part of an address
+ *this assumes that the address string is split by ','
+ *useful since Google will require a very precise
+ *definition of an address while the user would usually
+ *know which city and country they are looking at*/
+ var temp = address.split(",");
+ return temp[0];
+}
+
+function receiveAlternative(notification, payload, ignoreErrors){
+ var ans = "unknown";
+ var errlst = ignoreErrors;
+ if(payload.data && payload.data.status === "OK"){
+ Log.log('received ' + notification);
+ //only interested in duration, first option should be the shortest one
+ var route = payload.data.routes[0];
+ var leg = route.legs[0];
+ ans = leg.duration.value;
+ }else if (errlst.indexOf(payload.data.status) < 0){
+ Log.warn('received '+notification+' with status '+payload.data.status);
+ }else{
+ Log.info('received '+notification+' with status '+payload.data.status);
+ }
+ return ans;
+}
+
+function renderLeg(wrapper, leg, arrivalWord, config){
+ /* renderLeg
+ * creates HTML element for one leg of a route
+ */
+ var depature = leg.departure_time.value * 1000;
+ var arrival = leg.arrival_time.value * 1000;
+ var span = document.createElement("div");
+ span.className = "small bright";
+ span.innerHTML = moment(depature).locale(config.language).fromNow();
+ if (config.displayArrival){
+ span.innerHTML += " ("+config.test+arrivalWord+": ";
+ if (config.timeFormat === 24){
+ span.innerHTML += moment(arrival).format("H:mm");
+ }else{
+ span.innerHTML += moment(arrival).format("h:mm");
+ }
+ span.innerHTML += ")";
+ }
+ wrapper.appendChild(span);
+}
+
+function renderDeparture(span, departureStop, fromWord, config){
+ if (config.displayStationLength > 0){
+ /* add departure stop (shortened)*/
+ span.innerHTML += " ("+fromWord+" " + shorten(departureStop, config.displayStationLength) + ")";
+ }else if (config.displayStationLength === 0){
+ /* add departure stop*/
+ span.innerHTML += " ("+fromWord+" " + config._laststop + ")";
+ }
+}
+
+function calcOpacity(Nrs,e,config){
+ if (config.fadePoint < 0) {
+ config.fadePoint = 0;
+ }
+ if (config.fade && config.fadePoint < 1) {
+ var startingPoint = Nrs * config.fadePoint;
+ var steps = Nrs - startingPoint;
+ if (e >= startingPoint) {
+ var currentStep = e - startingPoint;
+ return = 1 - (1 / steps * currentStep);
+ }
+ }else{
+ return 1;
+ }
+}
+
+function renderFade(routeArray,config){
+ /*create fade effect and append list items to the list*/
+ var ul = document.createElement("ul");
+ var e = 0;
+ var Nrs = routeArray.length;
+ for(var dataKey in routeArray) {
+ var routeData = routeArray[dataKey];
+ var routeHtml = routeData.html;
+ // Create fade effect.
+ routeHtml.style.opacity = calcOpacity(Nrs,e,config);
+ ul.appendChild(routeHtml);
+ e += 1;
+ }
+ return ul
+}
+
+//function renderStep(wrapper, step, fromWord, config){
+ // /* renderStep
+ // * creates HTML element for one step of a leg
+ // */
+ // if(step.travel_mode === "WALKING"){
+ // /*this step is not public transport but walking*/
+ // var duration = step.duration.value;
+ // if (duration >= (config.maxWalkTime*60)){
+ // /*if time of walking is longer than
+ // *specified, mark this route to be skipped*/
+ // wrapper.innerHTML = "too far";
+ // }else if(config.displayWalkType != 'none'){
+ // /*if walking and walking times should be
+ // *specified, add symbol and time*/
+ // wrapper.appendChild(getSymbol("walk",config.showColor));
+ // var span = document.createElement("span");
+ // span.innerHTML = this.convertTime(duration);
+ // if (config._laststop !== ''){
+ // /* walking step doesn't have a departure_stop set - maybe something else but can't find the documentation right now.
+ // so in order to display the departure, we will just save the arrival of any transit step into a global variable and
+ // display the previous arrival instead of the current departure location. That means we need to reset the global variable
+ // to not cause interference between different routes and we need to skip the display for the first step if that is a walking
+ // step (alternatively one could display the departure location specified by the user, but I prefer this option)
+ // */
+ // if (config.displayStationLength > 0){
+ // /* add departure stop (shortened)*/
+ // span.innerHTML += " ("+fromWord+" " + shorten(config._laststop, config.displayStationLength) + ")";
+ // }else if (config.displayStationLength === 0){
+ // /* add departure stop*/
+ // span.innerHTML += " ("+fromWord+" " + config._laststop + ")";
+ // }
+ // }
+ // span.className = "xsmall dimmed";
+ // wrapper.appendChild(span);
+ // }else{
+ // /*skip walking*/
+ // return;
+ // }
+ // config._laststop = '';
+ // }else{
+ // /*if this is a transit step*/
+ // var details = step.transit_details;
+ // if(details) {
+ // /*add symbol of transport vehicle*/
+ // var img = document.createElement("img");
+ // if(config.showColor){
+ // img.className = "symbol";
+ // }else{
+ // img.className = "symbol bw";
+ // }
+ // /* get symbol online*/
+ // img.src = details.line.vehicle.local_icon || ("https:" + details.line.vehicle.icon);
+ // /* can provide own symbols under /localtransport/public/*.png */
+ // //img.src = "/localtransport/" + details.line.vehicle.name + ".png";
+ // img.alt = "[" + details.line.vehicle.name +"]";
+ // wrapper.appendChild(img);
+ // /*add description*/
+ // var span = document.createElement("span");
+ // /* add line name*/
+ // span.innerHTML = details.line.short_name || details.line.name;
+ // if (config.displayStationLength > 0){
+ // /* add departure stop (shortened)*/
+ // span.innerHTML += " ("+fromWord+" " + shorten(details.departure_stop.name, config.displayStationLength) + ")";
+ // }else if (config.displayStationLength === 0){
+ // /* add departure stop*/
+ // span.innerHTML += " ("+fromWord+" " + details.departure_stop.name + ")";
+ // }
+ // if (config.debug){
+ // /* add vehicle type for debug*/
+ // span.innerHTML += " [" + details.line.vehicle.name +"]";
+ // }
+ // config._laststop = details.arrival_stop.name;
+ // span.className = "xsmall dimmed";
+ // wrapper.appendChild(span);
+ // }
+ // }
+// }
\ No newline at end of file
diff --git a/i18n/de.json b/i18n/de.json
index 583999d..c6dcb94 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -1,5 +1,12 @@
{
"LOADING_CONNECTIONS": "Lade Verbindungen ...",
+ "OK": "Ich kenne die Antwort aber ich sage sie dir nicht :-P Dies ist höchstwahrscheinlich ein bug.",
+ "NOT_FOUND": "Start- oder Endpunkt konntent nicht gefunden werden.",
+ "ZERO_RESULTS": "Keine Verbindung gefunden.",
+ "INVALID_REQUEST": "Es wurde eine ungültige Anfrage gemacht.",
+ "OVER_QUERY_LIMIT": "Sie haben zu viele Anfragen gestellt.",
+ "REQUEST_DENIED": "Google hat ihre Anfrage abgewiesen.",
+ "UNKNOWN_ERROR": "Irgendetwas ist falsch gelaufen.",
"ARRIVAL": "Ankunft",
"MINUTE_PL": "Minuten",
"MINUTE_SL": "Minute",
@@ -7,5 +14,6 @@
"MINUTE_PS": "min",
"MINUTE_SS": "min",
"SECOND_PS": "sek",
- "FROM": "von"
+ "FROM": "von",
+ "ALT": "Alternativen"
}
diff --git a/i18n/en.json b/i18n/en.json
index 7bbdbcb..d17b7a5 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1,5 +1,12 @@
{
"LOADING_CONNECTIONS": "Loading ...",
+ "OK": "I have a valid answer, but I don't want to show it to you :-P This is most likely a bug.",
+ "NOT_FOUND": "Either origin or destination couldn't be found.",
+ "ZERO_RESULTS": "No route found.",
+ "INVALID_REQUEST": "An invalid request was send.",
+ "OVER_QUERY_LIMIT": "You send too many requests.",
+ "REQUEST_DENIED": "Google denied your request.",
+ "UNKNOWN_ERROR": "Something went wrong. No one knows what.",
"ARRIVAL": "arriving",
"MINUTE_PL": "minutes",
"MINUTE_SL": "minute",
@@ -7,5 +14,6 @@
"MINUTE_PS": "min",
"MINUTE_SS": "min",
"SECOND_PS": "sec",
- "FROM": "from"
+ "FROM": "from",
+ "ALT": "alternatives"
}
diff --git a/i18n/sv.json b/i18n/sv.json
index 77b6227..a95482b 100644
--- a/i18n/sv.json
+++ b/i18n/sv.json
@@ -7,5 +7,6 @@
"MINUTE_PS": "min",
"MINUTE_SS": "min",
"SECOND_PS": "s",
- "FROM": "från"
+ "FROM": "från",
+ "ALT": "alternativ"
}
diff --git a/localtransport.css b/localtransport.css
index cf44997..9be3dd8 100644
--- a/localtransport.css
+++ b/localtransport.css
@@ -1,2 +1,33 @@
-.MMM-LocalTransport ul{margin:0;padding:0;list-style:none;text-align:left}.MMM-LocalTransport ul li{margin-bottom:.25em}.MMM-LocalTransport ul li img.bw{-webkit-filter:grayscale(100%)}.MMM-LocalTransport ul li img:first-child{margin-left:0}.MMM-LocalTransport ul li img{margin:0 .25em 0 .5em}.MMM-LocalTransport ul li div{margin-left:.5em}
+/*.localtransport ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ text-align: left;
+}
+.localtransport ul li {
+ margin-bottom: .25em;
+}
+.localtransport ul li img:first-child {
+ margin-left: 0;
+}
+.localtransport ul li img {
+ margin: 0 .25em 0 .5em;
+ max-height: 15px;
+ max-width: 15px;
+ height: 15px;
+ width: 15px;
+}
+.localtransport ul li div {
+ margin-left: .5em;
+}
+
+.localtransport img.symbol.bw {
+ -webkit-filter: grayscale(100%);
+}*/
+.MMM-LocalTransport ul{margin:0;padding:0;list-style:none;text-align:left}
+.MMM-LocalTransport ul li{margin-bottom:.25em}
+.MMM-LocalTransport ul li img.bw{-webkit-filter:grayscale(100%)}
+.MMM-LocalTransport ul li img:first-child{margin-left:0}
+.MMM-LocalTransport ul li img{margin:0 .25em 0 .5em; max-height: 15px; max-width: 15px; height: 15px; width: 15px;}
+.MMM-LocalTransport ul li div{margin-left:.5em}
/* This beautiful CSS-File has been crafted with LESS (lesscss.org) and compiled by simpLESS (wearekiss.com/simpless) */
diff --git a/localtransport.less b/localtransport.less
index f738329..fdedef1 100644
--- a/localtransport.less
+++ b/localtransport.less
@@ -16,6 +16,10 @@
}
img{
margin: 0 .25em 0 .5em;
+ max-height: 15px;
+ max-width: 15px;
+ height: 15px;
+ width: 15px;
}
div{
margin-left: .5em;
diff --git a/node_helper.js b/node_helper.js
index fa48ee8..712150d 100644
--- a/node_helper.js
+++ b/node_helper.js
@@ -11,21 +11,34 @@ module.exports = NodeHelper.create({
start: function () {
console.log(this.name + ' helper started ...');
},
- socketNotificationReceived: function(notification, payload) {
- //console.log(notification);
- if (notification === 'LOCAL_TRANSPORT_REQUEST') {
+ makeAPIrequest: function(payload,responseName){
var that = this;
request({
url: payload.url,
method: 'GET'
}, function(error, response, body) {
if (!error && response.statusCode == 200) {
- that.sendSocketNotification('LOCAL_TRANSPORT_RESPONSE', {
+ that.sendSocketNotification(responseName, {
id: payload.id,
data: JSON.parse(body)
});
}
});
+ },
+ socketNotificationReceived: function(notification, payload) {
+ //console.log(notification);
+
+ if (notification === 'LOCAL_TRANSPORT_REQUEST') {
+ this.makeAPIrequest(payload,'LOCAL_TRANSPORT_RESPONSE');
+ }
+ if (notification === 'LOCAL_TRANSPORT_WALK_REQUEST') {
+ this.makeAPIrequest(payload,'LOCAL_TRANSPORT_WALK_RESPONSE');
+ }
+ if (notification === 'LOCAL_TRANSPORT_CYCLE_REQUEST') {
+ this.makeAPIrequest(payload,'LOCAL_TRANSPORT_CYCLE_RESPONSE');
+ }
+ if (notification === 'LOCAL_TRANSPORT_DRIVE_REQUEST') {
+ this.makeAPIrequest(payload,'LOCAL_TRANSPORT_DRIVE_RESPONSE');
}
}
});