diff --git a/transport/cyclehire.html b/transport/cyclehire.html
new file mode 100644
index 00000000..2a7769c8
--- /dev/null
+++ b/transport/cyclehire.html
@@ -0,0 +1,333 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/transport/cyclehire.js b/transport/cyclehire.js
new file mode 100644
index 00000000..0115151a
--- /dev/null
+++ b/transport/cyclehire.js
@@ -0,0 +1,334 @@
+/*
+ Copyright 2014 IBM Corp.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+module.exports = function(RED) {
+ "use strict";
+ var xml2js = require('xml2js');
+ var http = require("http");
+ var dataJSON;
+ var STATIONS_TO_RETURN = 10; //the number of nearest stations to be returned by a lat/lon request only.
+ var TIME_BETWEEN_REQUESTS = 300000; //the time in ms between requests to the server from the input node.
+
+ //function to pull the data from the API. Reformats it into a JSON which can more easily be used.
+ function pullData(callback){
+ var data = "";
+ http.get("http://www.tfl.gov.uk/tfl/syndication/feeds/cycle-hire/livecyclehireupdates.xml", function(res) {
+ res.on('data', function (chunk) {
+ data+= chunk;
+ });
+ res.on('end', function() {
+ if(res.statusCode == 200){
+ xml2js.parseString(data, function (err, result) {
+ dataJSON = JSON.parse(JSON.stringify(result));
+ callback();
+ });
+ } else {
+ dataJSON = {statuscode: res.statusCode};
+ console.log(res);
+ callback();
+ }
+ });
+ }).on('error', function(e) {
+ dataJSON = {error: e};
+ console.log(e);
+ callback();
+ });
+ }
+
+ //Accepts a station or array of stations and formats them to be output.
+ function stationFormat(station, callback){
+ var response;
+ if(Array.isArray(station)){
+ response = [];
+ for(var i=0; i= node.lon && node.lon >= -180){
+ if(90 >= node.lat && node.lat >= -90){
+ findLocation(node.lat, node.lon, function(response){
+ stationFormat(response, function(formattedStation){
+ msg.payload = formattedStation;
+ msg.location.lat = node.lat;
+ msg.location.lon = node.lon;
+ msg.description = "Barclays Cycle Hire information for the " + STATIONS_TO_RETURN + " closest stations to coordinates: " + node.lat + ", " + node.lon;
+ msg.title = "Current Barclays Cycle Hire station information";
+ callback();
+ });
+ });
+ } else {
+ node.error("Invalid latitude provided.");
+ callback();
+ }
+ } else {
+ node.error("Invalid longitude provided.");
+ callback();
+ }
+ }
+
+ function processHandler(node, msg, callback){
+ msg.payload = {};
+ msg.data = {};
+ msg.location = {};
+ msg.title = {};
+ msg.description = {};
+
+ if(node.station){
+ processStation(node, msg, function(){
+ callback();
+ });
+ } else if (node.stationid){
+ processStationID(node, msg, function(){
+ callback();
+ });
+ } else if (node.lat && node.lon) {
+ processCoordinates(node, msg, function(){
+ callback();
+ });
+ } else {
+ node.error("Invalid data provided.");
+ callback();
+ }
+ }
+
+ function CycleHire(n) {
+ RED.nodes.createNode(this, n);
+ var node = this;
+
+ this.on('input', function(msg) {
+ if(n.station){
+ node.station = n.station;
+ } else {
+ if(msg.payload && msg.payload.stationid){
+ node.stationid = msg.payload.stationid;
+ } else {
+ if(n.lat){
+ node.lat = n.lat;
+ } else if(msg.location && msg.location.lat) {
+ node.lat = msg.location.lat;
+ }
+
+ if(n.lon){
+ node.lon = n.lon;
+ } else if (msg.location && msg.location.lon) {
+ node.lon = msg.location.lon;
+ }
+ }
+ }
+
+ if(!dataJSON){
+ pullData(function(){
+ if(dataJSON.error){
+ node.error("Error connecting to the API: " + dataJSON.error);
+ } else {
+ processHandler(node, msg, function(){
+ node.send(msg);
+ });
+ }
+ });
+ } else {
+ processHandler(node, msg, function(){
+ node.send(msg);
+ });
+ }
+ });
+ }
+
+ function CycleHireInput(n){
+ RED.nodes.createNode(this, n);
+ var node = this;
+
+ this.repeat = TIME_BETWEEN_REQUESTS;
+ this.interval_id = null;
+ var previousdata = null;
+
+ this.interval_id = setInterval( function() {
+ node.emit("input",{});
+ }, this.repeat );
+
+ this.on('input', function(msg) {
+ if(n.station){
+ node.station = n.station;
+ } else {
+ if(n.lat){
+ node.lat = n.lat;
+ }
+ if(n.lon){
+ node.lon = n.lon;
+ }
+ }
+
+ if(!dataJSON){
+ pullData(function(){
+ if(dataJSON.error){
+ node.error("Error connecting to the API: " + dataJSON.error);
+ } else {
+ processHandler(node, msg, function(){
+ var msgString = JSON.stringify(msg.payload);
+ if(msgString !== previousdata){
+ previousdata = msgString;
+ node.send(msg);
+ }
+ });
+ }
+ });
+ } else {
+ processHandler(node, msg, function(){
+ var msgString = JSON.stringify(msg.payload);
+ if(msgString !== previousdata){
+ previousdata = msgString;
+ node.send(msg);
+ }
+ });
+ }
+ });
+
+ this.on("close", function() {
+ if (this.interval_id !== null) {
+ clearInterval(this.interval_id);
+ }
+ });
+ node.emit("input",{});
+ }
+
+ RED.httpAdmin.get('/cyclehire/data', function(req, res) {
+ pullData( function() {
+ res.send(dataJSON);
+ });
+
+ });
+
+ RED.nodes.registerType("tfl cyclehire",CycleHire);
+ RED.nodes.registerType("tfl cyclehire in",CycleHireInput);
+};
diff --git a/transport/icons/bicycle.png b/transport/icons/bicycle.png
new file mode 100644
index 00000000..829137df
Binary files /dev/null and b/transport/icons/bicycle.png differ