diff --git a/lovelace.js b/lovelace.js
index 0315d81..3694f2b 100644
--- a/lovelace.js
+++ b/lovelace.js
@@ -1,65 +1,103 @@
/**
* Module Dependencies
* */
- var express = require('express');
- var app = express();
- var http = require('http').Server(app);
- var path = require('path');
- var bodyParser = require('body-parser');
- var io = require('socket.io')(http);
- var rethinkdb = require("./rethinkDB.js");
- var mongoDB = require("./mongoDB.js");
- var pgrouting = require("./pgrouting.js");
+var express = require('express');
+var app = express();
+var http = require('http').Server(app);
+var path = require('path');
+var bodyParser = require('body-parser');
+var io = require('socket.io')(http);
+var rethinkdb = require("./rethinkDB.js");
+var mongoDB = require("./mongoDB.js");
+var pgrouting = require("./pgrouting.js");
+
/**
* View engine
* */
- app.set('views', path.join(__dirname, 'views'));
- app.set('view engine', 'jade');
-
- app.use(bodyParser.json()); //json as param support
- app.use(bodyParser.urlencoded({ extended: false })); //data as param support
-
- app.use(express.static(path.join(__dirname, 'public'))); //Make resources public
- app.use('/mapa', express.static('public')); //Make resources public at mapa/css/xxx
- app.use(express.static(path.join(__dirname, 'test'))); //Make test directory public... for... testing....
-
-
- /**
- *
- * Set rethinkDB and mongoDB... and pgrouting as a global resource.
- * Somebody please optimize this @_@.
- *
- * */
- app.use(function(req, res, next) {
- res.header("Access-Control-Allow-Origin", "*");
- res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
-
- req.rethinkdb = rethinkdb;
- req.mongoDB = mongoDB;
- req.pgrouting = pgrouting;
- req.io = io;
-
- next();
- });
+app.set('views', path.join(__dirname, 'views'));
+app.set('view engine', 'jade');
+
+app.use(bodyParser.json()); //json as param support
+app.use(bodyParser.urlencoded({ extended: false })); //data as param support
+
+app.use(express.static(path.join(__dirname, 'public'))); //Make resources public
+app.use('/map', express.static('public')); //Make resources public at mapa/css/xxx
+app.use('/cars', express.static('public')); //Make resources public at car/css/xxx
+app.use(express.static(path.join(__dirname, 'test'))); //Make test directory public... for... testing....
+
+
+
+/**
+ * Set rethinkDB and mongoDB... and pgrouting as a global resource.
+ * Someday i'm going to optimize this @_@...
+ * */
+app.use(function(req, res, next) {
+ res.header("Access-Control-Allow-Origin", "*");
+ res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
+
+ req.rethinkdb = rethinkdb;
+ req.mongoDB = mongoDB;
+ req.pgrouting = pgrouting;
+ req.io = io;
+
+ next();
+});
+
/**
- *
* Routers controllers
- *
* */
- var indexRouter = require("./routes/router.js");
- var mapaRouter = require("./routes/map.js");
- var carRouter = require("./routes/car.js");
- var tripsRouter = require("./routes/trips.js");
-
+var indexRouter = require("./routes/router.js");
+var mapaRouter = require("./routes/map.js");
+var carRouter = require("./routes/car.js");
+var carsRouter = require("./routes/cars.js");
+
+
+app.use("/", indexRouter);
+app.use("/map", mapaRouter);
+app.use("/car", carRouter);
+app.use("/cars", carsRouter);
- app.use("/", indexRouter);
- app.use("/map", mapaRouter);
- app.use("/car", carRouter);
- app.use("/trips", tripsRouter);
+
+
+/**
+ * Socket handlers
+ * */
+io.on('connection', function(socket){
+
+ socket.on("carsData", function(){
+ mongoDB.getCollections(function(collections){
+ for(var i = 0; i < collections.length; i++){
+ findAndEmit(collections[i].name, socket);
+ }
+ });
+ });
+
+ socket.on("carFeatures", function(idCar) {
+ mongoDB.getCollectionData({},idCar, function(err, data) {
+ if(!err){
+ socket.emit("carFeatures", data);
+ }
+ });
+ });
+});
+
+
+function findAndEmit(idCar, socket){
+ mongoDB.getCollectionData({collectionProperties: true}, idCar, function(err,data){
+ if( !err ){
+ var transferObject = {
+ idCar: idCar,
+ data: data[0]
+ };
+
+ socket.emit("carData", transferObject);
+ }
+ });
+}
/**
diff --git a/mongoDB.js b/mongoDB.js
index 8f7175a..59a737e 100644
--- a/mongoDB.js
+++ b/mongoDB.js
@@ -9,8 +9,7 @@ var mongodb = require('mongodb');
* */
var HOST = "107.170.232.222";
var PORT = 27017;
-var DB = "lovelace_test";
-var COLLECTION = "cars";
+var DB = "lovelace";
/**
@@ -36,6 +35,8 @@ var insert = function(idCar, data, callback){
* Updates the collection properties of the car: new_collection.properties.idCar
* */
var updateCollectionProperties = function(collection, last_trip, callback){
+ delete last_trip.isochrone;
+
var server = new mongodb.Server(HOST, PORT);
var db = new mongodb.Db(DB, server);
@@ -84,13 +85,17 @@ var getCollections = function(callback){
db.listCollections().toArray(function(err, collectionsNames){
db.close();
- callback(err, collectionsNames);
+ if(err){
+ console.log("Error getting the collection names", err);
+ }else{
+ callback(collectionsNames);
+ }
});
});
};
-var getCollectionData = function(collection, callback){
+var getCollectionData = function(filter, collection, callback){
var server = new mongodb.Server(HOST, PORT);
var db = new mongodb.Db(DB, server);
@@ -99,7 +104,7 @@ var getCollectionData = function(collection, callback){
console.log("Error ", err);
}
- client.collection(collection).find().toArray(function(err, result){
+ client.collection(collection).find(filter).toArray(function(err, result){
db.close();
callback(err,result);
});
diff --git a/public/css/car.css b/public/css/car.css
new file mode 100644
index 0000000..c7975ff
--- /dev/null
+++ b/public/css/car.css
@@ -0,0 +1,32 @@
+svg {
+ font: 10px sans-serif;
+}
+
+.background path {
+ fill: none;
+ stroke: #ddd;
+ shape-rendering: crispEdges;
+}
+
+.foreground path {
+ fill: none;
+ stroke: steelblue;
+}
+
+.brush .extent {
+ fill-opacity: .3;
+ stroke: #fff;
+ shape-rendering: crispEdges;
+}
+
+.axis line,
+.axis path {
+ fill: none;
+ stroke: #000;
+ shape-rendering: crispEdges;
+}
+
+.axis text {
+ text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
+ cursor: move;
+}
\ No newline at end of file
diff --git a/public/js/d3/d3.parcoords.css b/public/js/d3/d3.parcoords.css
new file mode 100644
index 0000000..956603c
--- /dev/null
+++ b/public/js/d3/d3.parcoords.css
@@ -0,0 +1,48 @@
+.parcoords > svg, .parcoords > canvas {
+ font: 14px sans-serif;
+ position: absolute;
+}
+.parcoords > canvas {
+ pointer-events: none;
+}
+
+.parcoords text.label {
+ cursor: default;
+}
+
+.parcoords rect.background {
+ fill: transparent;
+}
+.parcoords rect.background:hover {
+ fill: rgba(120,120,120,0.2);
+}
+.parcoords .resize rect {
+ fill: rgba(0,0,0,0.1);
+}
+.parcoords rect.extent {
+ fill: rgba(255,255,255,0.25);
+ stroke: rgba(0,0,0,0.6);
+}
+.parcoords .axis line, .parcoords .axis path {
+ fill: none;
+ stroke: #222;
+ shape-rendering: crispEdges;
+}
+.parcoords canvas {
+ opacity: 1;
+ -moz-transition: opacity 0.3s;
+ -webkit-transition: opacity 0.3s;
+ -o-transition: opacity 0.3s;
+}
+.parcoords canvas.faded {
+ opacity: 0.25;
+}
+.parcoords {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ background-color: white;
+}
\ No newline at end of file
diff --git a/public/js/d3/d3.parcoords.js b/public/js/d3/d3.parcoords.js
new file mode 100644
index 0000000..1644f6a
--- /dev/null
+++ b/public/js/d3/d3.parcoords.js
@@ -0,0 +1,2315 @@
+d3.parcoords = function(config) {
+ var __ = {
+ data: [],
+ highlighted: [],
+ dimensions: {},
+ dimensionTitleRotation: 0,
+ brushed: false,
+ brushedColor: null,
+ alphaOnBrushed: 0.0,
+ mode: "default",
+ rate: 20,
+ width: 600,
+ height: 300,
+ margin: { top: 24, right: 0, bottom: 12, left: 0 },
+ nullValueSeparator: "undefined", // set to "top" or "bottom"
+ nullValueSeparatorPadding: { top: 8, right: 0, bottom: 8, left: 0 },
+ color: "#069",
+ composite: "source-over",
+ alpha: 0.7,
+ bundlingStrength: 0.5,
+ bundleDimension: null,
+ smoothness: 0.0,
+ showControlPoints: false,
+ hideAxis : []
+ };
+
+ extend(__, config);
+
+ if (config && config.dimensionTitles) {
+ console.warn("dimensionTitles passed in config is deprecated. Add title to dimension object.");
+ d3.entries(config.dimensionTitles).forEach(function(d) {
+ if (__.dimensions[d.key]) {
+ __.dimensions[d.key].title = __.dimensions[d.key].title ? __.dimensions[d.key].title : d.value;
+ } else {
+ __.dimensions[d.key] = {
+ title: d.value
+ };
+ }
+ });
+ }
+var pc = function(selection) {
+ selection = pc.selection = d3.select(selection);
+
+ __.width = selection[0][0].clientWidth;
+ __.height = selection[0][0].clientHeight;
+
+ // canvas data layers
+ ["marks", "foreground", "brushed", "highlight"].forEach(function(layer) {
+ canvas[layer] = selection
+ .append("canvas")
+ .attr("class", layer)[0][0];
+ ctx[layer] = canvas[layer].getContext("2d");
+ });
+
+ // svg tick and brush layers
+ pc.svg = selection
+ .append("svg")
+ .attr("width", __.width)
+ .attr("height", __.height)
+ .append("svg:g")
+ .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+ return pc;
+};
+var events = d3.dispatch.apply(this,["render", "resize", "highlight", "brush", "brushend", "axesreorder"].concat(d3.keys(__))),
+ w = function() { return __.width - __.margin.right - __.margin.left; },
+ h = function() { return __.height - __.margin.top - __.margin.bottom; },
+ flags = {
+ brushable: false,
+ reorderable: false,
+ axes: false,
+ interactive: false,
+ debug: false
+ },
+ xscale = d3.scale.ordinal(),
+ dragging = {},
+ line = d3.svg.line(),
+ axis = d3.svg.axis().orient("left").ticks(5),
+ g, // groups for axes, brushes
+ ctx = {},
+ canvas = {},
+ clusterCentroids = [];
+
+// side effects for setters
+var side_effects = d3.dispatch.apply(this,d3.keys(__))
+ .on("composite", function(d) {
+ ctx.foreground.globalCompositeOperation = d.value;
+ ctx.brushed.globalCompositeOperation = d.value;
+ })
+ .on("alpha", function(d) {
+ ctx.foreground.globalAlpha = d.value;
+ ctx.brushed.globalAlpha = d.value;
+ })
+ .on("brushedColor", function (d) {
+ ctx.brushed.strokeStyle = d.value;
+ })
+ .on("width", function(d) { pc.resize(); })
+ .on("height", function(d) { pc.resize(); })
+ .on("margin", function(d) { pc.resize(); })
+ .on("rate", function(d) {
+ brushedQueue.rate(d.value);
+ foregroundQueue.rate(d.value);
+ })
+ .on("dimensions", function(d) {
+ __.dimensions = pc.applyDimensionDefaults(d3.keys(d.value));
+ xscale.domain(pc.getOrderedDimensionKeys());
+ pc.sortDimensions();
+ if (flags.interactive){pc.render().updateAxes();}
+ })
+ .on("bundleDimension", function(d) {
+ if (!d3.keys(__.dimensions).length) pc.detectDimensions();
+ pc.autoscale();
+ if (typeof d.value === "number") {
+ if (d.value < d3.keys(__.dimensions).length) {
+ __.bundleDimension = __.dimensions[d.value];
+ } else if (d.value < __.hideAxis.length) {
+ __.bundleDimension = __.hideAxis[d.value];
+ }
+ } else {
+ __.bundleDimension = d.value;
+ }
+
+ __.clusterCentroids = compute_cluster_centroids(__.bundleDimension);
+ if (flags.interactive){pc.render();}
+ })
+ .on("hideAxis", function(d) {
+ pc.dimensions(pc.applyDimensionDefaults());
+ pc.dimensions(without(__.dimensions, d.value));
+ });
+
+// expose the state of the chart
+pc.state = __;
+pc.flags = flags;
+
+// create getter/setters
+getset(pc, __, events);
+
+// expose events
+d3.rebind(pc, events, "on");
+
+// getter/setter with event firing
+function getset(obj,state,events) {
+ d3.keys(state).forEach(function(key) {
+ obj[key] = function(x) {
+ if (!arguments.length) {
+ return state[key];
+ }
+ if (key === 'dimensions' && Object.prototype.toString.call(x) === '[object Array]') {
+ console.warn("pc.dimensions([]) is deprecated, use pc.dimensions({})");
+ x = pc.applyDimensionDefaults(x);
+ }
+ var old = state[key];
+ state[key] = x;
+ side_effects[key].call(pc,{"value": x, "previous": old});
+ events[key].call(pc,{"value": x, "previous": old});
+ return obj;
+ };
+ });
+};
+
+function extend(target, source) {
+ for (key in source) {
+ target[key] = source[key];
+ }
+ return target;
+};
+
+function without(arr, items) {
+ items.forEach(function (el) {
+ delete arr[el];
+ });
+ return arr;
+};
+/** adjusts an axis' default range [h()+1, 1] if a NullValueSeparator is set */
+function getRange() {
+ if (__.nullValueSeparator=="bottom") {
+ return [h()+1-__.nullValueSeparatorPadding.bottom-__.nullValueSeparatorPadding.top, 1];
+ } else if (__.nullValueSeparator=="top") {
+ return [h()+1, 1+__.nullValueSeparatorPadding.bottom+__.nullValueSeparatorPadding.top];
+ }
+ return [h()+1, 1];
+};
+
+pc.autoscale = function() {
+ // yscale
+ var defaultScales = {
+ "date": function(k) {
+ var extent = d3.extent(__.data, function(d) {
+ return d[k] ? d[k].getTime() : null;
+ });
+
+ // special case if single value
+ if (extent[0] === extent[1]) {
+ return d3.scale.ordinal()
+ .domain([extent[0]])
+ .rangePoints(getRange());
+ }
+
+ return d3.time.scale()
+ .domain(extent)
+ .range(getRange());
+ },
+ "number": function(k) {
+ var extent = d3.extent(__.data, function(d) { return +d[k]; });
+
+ // special case if single value
+ if (extent[0] === extent[1]) {
+ return d3.scale.ordinal()
+ .domain([extent[0]])
+ .rangePoints(getRange());
+ }
+
+ return d3.scale.linear()
+ .domain(extent)
+ .range(getRange());
+ },
+ "string": function(k) {
+ var counts = {},
+ domain = [];
+
+ // Let's get the count for each value so that we can sort the domain based
+ // on the number of items for each value.
+ __.data.map(function(p) {
+ if (p[k] === undefined && __.nullValueSeparator!== "undefined"){
+ return; // null values will be drawn beyond the horizontal null value separator!
+ }
+ if (counts[p[k]] === undefined) {
+ counts[p[k]] = 1;
+ } else {
+ counts[p[k]] = counts[p[k]] + 1;
+ }
+ });
+
+ domain = Object.getOwnPropertyNames(counts).sort(function(a, b) {
+ return counts[a] - counts[b];
+ });
+
+ return d3.scale.ordinal()
+ .domain(domain)
+ .rangePoints(getRange());
+ }
+ };
+
+ d3.keys(__.dimensions).forEach(function(k) {
+ if (!__.dimensions[k].yscale){
+ __.dimensions[k].yscale = defaultScales[__.dimensions[k].type](k);
+ }
+ });
+
+ // xscale
+ xscale.rangePoints([0, w()], 1);
+
+ // canvas sizes
+ pc.selection.selectAll("canvas")
+ .style("margin-top", __.margin.top + "px")
+ .style("margin-left", __.margin.left + "px")
+ .attr("width", w()+2)
+ .attr("height", h()+2);
+
+ // default styles, needs to be set when canvas width changes
+ ctx.foreground.strokeStyle = __.color;
+ ctx.foreground.lineWidth = 1.4;
+ ctx.foreground.globalCompositeOperation = __.composite;
+ ctx.foreground.globalAlpha = __.alpha;
+ ctx.brushed.strokeStyle = __.brushedColor;
+ ctx.brushed.lineWidth = 1.4;
+ ctx.brushed.globalCompositeOperation = __.composite;
+ ctx.brushed.globalAlpha = __.alpha;
+ ctx.highlight.lineWidth = 3;
+
+ return this;
+};
+
+pc.scale = function(d, domain) {
+ __.dimensions[d].yscale.domain(domain);
+
+ return this;
+};
+
+pc.flip = function(d) {
+ //__.dimensions[d].yscale.domain().reverse(); // does not work
+ __.dimensions[d].yscale.domain(__.dimensions[d].yscale.domain().reverse()); // works
+
+ return this;
+};
+
+pc.commonScale = function(global, type) {
+ var t = type || "number";
+ if (typeof global === 'undefined') {
+ global = true;
+ }
+
+ // try to autodetect dimensions and create scales
+ if (!d3.keys(__.dimensions).length) {
+ pc.detectDimensions()
+ }
+ pc.autoscale();
+
+ // scales of the same type
+ var scales = d3.keys(__.dimensions).filter(function(p) {
+ return __.dimensions[p].type == t;
+ });
+
+ if (global) {
+ var extent = d3.extent(scales.map(function(d,i) {
+ return __.dimensions[d].yscale.domain();
+ }).reduce(function(a,b) {
+ return a.concat(b);
+ }));
+
+ scales.forEach(function(d) {
+ __.dimensions[d].yscale.domain(extent);
+ });
+
+ } else {
+ scales.forEach(function(d) {
+ __.dimensions[d].yscale.domain(d3.extent(__.data, function(d) { return +d[k]; }));
+ });
+ }
+
+ // update centroids
+ if (__.bundleDimension !== null) {
+ pc.bundleDimension(__.bundleDimension);
+ }
+
+ return this;
+};
+pc.detectDimensions = function() {
+ pc.dimensions(pc.applyDimensionDefaults());
+ return this;
+};
+
+pc.applyDimensionDefaults = function(dims) {
+ var types = pc.detectDimensionTypes(__.data);
+ dims = dims ? dims : d3.keys(types);
+ var newDims = {};
+ var currIndex = 0;
+ dims.forEach(function(k) {
+ newDims[k] = __.dimensions[k] ? __.dimensions[k] : {};
+ //Set up defaults
+ newDims[k].orient= newDims[k].orient ? newDims[k].orient : 'left';
+ newDims[k].ticks= newDims[k].ticks ? newDims[k].ticks : 5;
+ newDims[k].innerTickSize= newDims[k].innerTickSize ? newDims[k].innerTickSize : 6;
+ newDims[k].outerTickSize= newDims[k].outerTickSize ? newDims[k].outerTickSize : 0;
+ newDims[k].tickPadding= newDims[k].tickPadding ? newDims[k].tickPadding : 3;
+ newDims[k].type= newDims[k].type ? newDims[k].type : types[k];
+
+ newDims[k].index = newDims[k].index ? newDims[k].index : currIndex;
+ currIndex++;
+ });
+ return newDims;
+};
+
+pc.getOrderedDimensionKeys = function(){
+ return d3.keys(__.dimensions).sort(function(x, y){
+ return d3.ascending(__.dimensions[x].index, __.dimensions[y].index);
+ });
+};
+
+// a better "typeof" from this post: http://stackoverflow.com/questions/7390426/better-way-to-get-type-of-a-javascript-variable
+pc.toType = function(v) {
+ return ({}).toString.call(v).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
+};
+
+// try to coerce to number before returning type
+pc.toTypeCoerceNumbers = function(v) {
+ if ((parseFloat(v) == v) && (v != null)) {
+ return "number";
+}
+ return pc.toType(v);
+};
+
+// attempt to determine types of each dimension based on first row of data
+pc.detectDimensionTypes = function(data) {
+ var types = {};
+ d3.keys(data[0])
+ .forEach(function(col) {
+ types[isNaN(Number(col)) ? col : parseInt(col)] = pc.toTypeCoerceNumbers(data[0][col]);
+ });
+ return types;
+};
+pc.render = function() {
+ // try to autodetect dimensions and create scales
+ if (!d3.keys(__.dimensions).length) {
+ pc.detectDimensions()
+ }
+ pc.autoscale();
+
+ pc.render[__.mode]();
+
+ events.render.call(this);
+ return this;
+};
+
+pc.renderBrushed = function() {
+ if (!d3.keys(__.dimensions).length) pc.detectDimensions();
+
+ pc.renderBrushed[__.mode]();
+
+ events.render.call(this);
+ return this;
+};
+
+function isBrushed() {
+ if (__.brushed && __.brushed.length !== __.data.length)
+ return true;
+
+ var object = brush.currentMode().brushState();
+
+ for (var key in object) {
+ if (object.hasOwnProperty(key)) {
+ return true;
+ }
+ }
+ return false;
+};
+
+pc.render.default = function() {
+ pc.clear('foreground');
+ pc.clear('highlight');
+
+ pc.renderBrushed.default();
+
+ __.data.forEach(path_foreground);
+};
+
+var foregroundQueue = d3.renderQueue(path_foreground)
+ .rate(50)
+ .clear(function() {
+ pc.clear('foreground');
+ pc.clear('highlight');
+ });
+
+pc.render.queue = function() {
+ pc.renderBrushed.queue();
+
+ foregroundQueue(__.data);
+};
+
+pc.renderBrushed.default = function() {
+ pc.clear('brushed');
+
+ if (isBrushed()) {
+ __.brushed.forEach(path_brushed);
+ }
+};
+
+var brushedQueue = d3.renderQueue(path_brushed)
+ .rate(50)
+ .clear(function() {
+ pc.clear('brushed');
+ });
+
+pc.renderBrushed.queue = function() {
+ if (isBrushed()) {
+ brushedQueue(__.brushed);
+ } else {
+ brushedQueue([]); // This is needed to clear the currently brushed items
+ }
+};function compute_cluster_centroids(d) {
+
+ var clusterCentroids = d3.map();
+ var clusterCounts = d3.map();
+ // determine clusterCounts
+ __.data.forEach(function(row) {
+ var scaled = __.dimensions[d].yscale(row[d]);
+ if (!clusterCounts.has(scaled)) {
+ clusterCounts.set(scaled, 0);
+ }
+ var count = clusterCounts.get(scaled);
+ clusterCounts.set(scaled, count + 1);
+ });
+
+ __.data.forEach(function(row) {
+ d3.keys(__.dimensions).map(function(p, i) {
+ var scaled = __.dimensions[d].yscale(row[d]);
+ if (!clusterCentroids.has(scaled)) {
+ var map = d3.map();
+ clusterCentroids.set(scaled, map);
+ }
+ if (!clusterCentroids.get(scaled).has(p)) {
+ clusterCentroids.get(scaled).set(p, 0);
+ }
+ var value = clusterCentroids.get(scaled).get(p);
+ value += __.dimensions[p].yscale(row[p]) / clusterCounts.get(scaled);
+ clusterCentroids.get(scaled).set(p, value);
+ });
+ });
+
+ return clusterCentroids;
+
+}
+
+function compute_centroids(row) {
+ var centroids = [];
+
+ var p = d3.keys(__.dimensions);
+ var cols = p.length;
+ var a = 0.5; // center between axes
+ for (var i = 0; i < cols; ++i) {
+ // centroids on 'real' axes
+ var x = position(p[i]);
+ var y = __.dimensions[p[i]].yscale(row[p[i]]);
+ centroids.push($V([x, y]));
+
+ // centroids on 'virtual' axes
+ if (i < cols - 1) {
+ var cx = x + a * (position(p[i+1]) - x);
+ var cy = y + a * (__.dimensions[p[i+1]].yscale(row[p[i+1]]) - y);
+ if (__.bundleDimension !== null) {
+ var leftCentroid = __.clusterCentroids.get(__.dimensions[__.bundleDimension].yscale(row[__.bundleDimension])).get(p[i]);
+ var rightCentroid = __.clusterCentroids.get(__.dimensions[__.bundleDimension].yscale(row[__.bundleDimension])).get(p[i+1]);
+ var centroid = 0.5 * (leftCentroid + rightCentroid);
+ cy = centroid + (1 - __.bundlingStrength) * (cy - centroid);
+ }
+ centroids.push($V([cx, cy]));
+ }
+ }
+
+ return centroids;
+}
+
+function compute_control_points(centroids) {
+
+ var cols = centroids.length;
+ var a = __.smoothness;
+ var cps = [];
+
+ cps.push(centroids[0]);
+ cps.push($V([centroids[0].e(1) + a*2*(centroids[1].e(1)-centroids[0].e(1)), centroids[0].e(2)]));
+ for (var col = 1; col < cols - 1; ++col) {
+ var mid = centroids[col];
+ var left = centroids[col - 1];
+ var right = centroids[col + 1];
+
+ var diff = left.subtract(right);
+ cps.push(mid.add(diff.x(a)));
+ cps.push(mid);
+ cps.push(mid.subtract(diff.x(a)));
+ }
+ cps.push($V([centroids[cols-1].e(1) + a*2*(centroids[cols-2].e(1)-centroids[cols-1].e(1)), centroids[cols-1].e(2)]));
+ cps.push(centroids[cols - 1]);
+
+ return cps;
+
+};pc.shadows = function() {
+ flags.shadows = true;
+ pc.alphaOnBrushed(0.1);
+ pc.render();
+ return this;
+};
+
+// draw dots with radius r on the axis line where data intersects
+pc.axisDots = function(r) {
+ var r = r || 0.1;
+ var ctx = pc.ctx.marks;
+ var startAngle = 0;
+ var endAngle = 2 * Math.PI;
+ ctx.globalAlpha = d3.min([ 1 / Math.pow(__.data.length, 1 / 2), 1 ]);
+ __.data.forEach(function(d) {
+ d3.entries(__.dimensions).forEach(function(p, i) {
+ ctx.beginPath();
+ ctx.arc(position(p), __.dimensions[p.key].yscale(d[p]), r, startAngle, endAngle);
+ ctx.stroke();
+ ctx.fill();
+ });
+ });
+ return this;
+};
+
+// draw single cubic bezier curve
+function single_curve(d, ctx) {
+
+ var centroids = compute_centroids(d);
+ var cps = compute_control_points(centroids);
+
+ ctx.moveTo(cps[0].e(1), cps[0].e(2));
+ for (var i = 1; i < cps.length; i += 3) {
+ if (__.showControlPoints) {
+ for (var j = 0; j < 3; j++) {
+ ctx.fillRect(cps[i+j].e(1), cps[i+j].e(2), 2, 2);
+ }
+ }
+ ctx.bezierCurveTo(cps[i].e(1), cps[i].e(2), cps[i+1].e(1), cps[i+1].e(2), cps[i+2].e(1), cps[i+2].e(2));
+ }
+};
+
+// draw single polyline
+function color_path(d, ctx) {
+ ctx.beginPath();
+ if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) {
+ single_curve(d, ctx);
+ } else {
+ single_path(d, ctx);
+ }
+ ctx.stroke();
+};
+
+// draw many polylines of the same color
+function paths(data, ctx) {
+ ctx.clearRect(-1, -1, w() + 2, h() + 2);
+ ctx.beginPath();
+ data.forEach(function(d) {
+ if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) {
+ single_curve(d, ctx);
+ } else {
+ single_path(d, ctx);
+ }
+ });
+ ctx.stroke();
+};
+
+// returns the y-position just beyond the separating null value line
+function getNullPosition() {
+ if (__.nullValueSeparator=="bottom") {
+ return h()+1;
+ } else if (__.nullValueSeparator=="top") {
+ return 1;
+ } else {
+ console.log("A value is NULL, but nullValueSeparator is not set; set it to 'bottom' or 'top'.");
+ }
+ return h()+1;
+};
+
+function single_path(d, ctx) {
+ d3.entries(__.dimensions).forEach(function(p, i) { //p isn't really p
+ if (i == 0) {
+ ctx.moveTo(position(p.key), typeof d[p.key] =='undefined' ? getNullPosition() : __.dimensions[p.key].yscale(d[p.key]));
+ } else {
+ ctx.lineTo(position(p.key), typeof d[p.key] =='undefined' ? getNullPosition() : __.dimensions[p.key].yscale(d[p.key]));
+ }
+ });
+};
+
+function path_brushed(d, i) {
+ if (__.brushedColor !== null) {
+ ctx.brushed.strokeStyle = d3.functor(__.brushedColor)(d, i);
+ } else {
+ ctx.brushed.strokeStyle = d3.functor(__.color)(d, i);
+ }
+ return color_path(d, ctx.brushed)
+};
+
+function path_foreground(d, i) {
+ ctx.foreground.strokeStyle = d3.functor(__.color)(d, i);
+ return color_path(d, ctx.foreground);
+};
+
+function path_highlight(d, i) {
+ ctx.highlight.strokeStyle = d3.functor(__.color)(d, i);
+ return color_path(d, ctx.highlight);
+};
+pc.clear = function(layer) {
+ ctx[layer].clearRect(0, 0, w() + 2, h() + 2);
+
+ // This will make sure that the foreground items are transparent
+ // without the need for changing the opacity style of the foreground canvas
+ // as this would stop the css styling from working
+ if(layer === "brushed" && isBrushed()) {
+ ctx.brushed.fillStyle = pc.selection.style("background-color");
+ ctx.brushed.globalAlpha = 1 - __.alphaOnBrushed;
+ ctx.brushed.fillRect(0, 0, w() + 2, h() + 2);
+ ctx.brushed.globalAlpha = __.alpha;
+ }
+ return this;
+};
+d3.rebind(pc, axis, "ticks", "orient", "tickValues", "tickSubdivide", "tickSize", "tickPadding", "tickFormat");
+
+function flipAxisAndUpdatePCP(dimension) {
+ var g = pc.svg.selectAll(".dimension");
+
+ pc.flip(dimension);
+
+ d3.select(this.parentElement)
+ .transition()
+ .duration(1100)
+ .call(axis.scale(__.dimensions[dimension].yscale));
+
+ pc.render();
+}
+
+function rotateLabels() {
+ var delta = d3.event.deltaY;
+ delta = delta < 0 ? -5 : delta;
+ delta = delta > 0 ? 5 : delta;
+
+ __.dimensionTitleRotation += delta;
+ pc.svg.selectAll("text.label")
+ .attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")");
+ d3.event.preventDefault();
+}
+
+function dimensionLabels(d) {
+ return __.dimensions[d].title ? __.dimensions[d].title : d; // dimension display names
+}
+
+pc.createAxes = function() {
+ if (g) pc.removeAxes();
+
+ // Add a group element for each dimension.
+ g = pc.svg.selectAll(".dimension")
+ .data(pc.getOrderedDimensionKeys(), function(d) {
+ return d;
+ })
+ .enter().append("svg:g")
+ .attr("class", "dimension")
+ .attr("transform", function(d) {
+ return "translate(" + xscale(d) + ")";
+ });
+
+ // Add an axis and title.
+ g.append("svg:g")
+ .attr("class", "axis")
+ .attr("transform", "translate(0,0)")
+ .each(function(d) { d3.select(this).call( pc.applyAxisConfig(axis, __.dimensions[d]) )
+ })
+ .append("svg:text")
+ .attr({
+ "text-anchor": "middle",
+ "y": 0,
+ "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")",
+ "x": 0,
+ "class": "label"
+ })
+ .text(dimensionLabels)
+ .on("dblclick", flipAxisAndUpdatePCP)
+ .on("wheel", rotateLabels);
+
+ if (__.nullValueSeparator=="top") {
+ pc.svg.append("line")
+ .attr("x1", 0)
+ .attr("y1", 1+__.nullValueSeparatorPadding.top)
+ .attr("x2", w())
+ .attr("y2", 1+__.nullValueSeparatorPadding.top)
+ .attr("stroke-width", 1)
+ .attr("stroke", "#777")
+ .attr("fill", "none")
+ .attr("shape-rendering", "crispEdges");
+ } else if (__.nullValueSeparator=="bottom") {
+ pc.svg.append("line")
+ .attr("x1", 0)
+ .attr("y1", h()+1-__.nullValueSeparatorPadding.bottom)
+ .attr("x2", w())
+ .attr("y2", h()+1-__.nullValueSeparatorPadding.bottom)
+ .attr("stroke-width", 1)
+ .attr("stroke", "#777")
+ .attr("fill", "none")
+ .attr("shape-rendering", "crispEdges");
+ }
+
+ flags.axes= true;
+ return this;
+};
+
+pc.removeAxes = function() {
+ g.remove();
+ return this;
+};
+
+pc.updateAxes = function() {
+ var g_data = pc.svg.selectAll(".dimension").data(pc.getOrderedDimensionKeys());
+
+ // Enter
+ g_data.enter().append("svg:g")
+ .attr("class", "dimension")
+ .attr("transform", function(p) { return "translate(" + position(p) + ")"; })
+ .style("opacity", 0)
+ .append("svg:g")
+ .attr("class", "axis")
+ .attr("transform", "translate(0,0)")
+ .each(function(d) { d3.select(this).call( pc.applyAxisConfig(axis, __.dimensions[d]) )
+ })
+ .append("svg:text")
+ .attr({
+ "text-anchor": "middle",
+ "y": 0,
+ "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")",
+ "x": 0,
+ "class": "label"
+ })
+ .text(dimensionLabels)
+ .on("dblclick", flipAxisAndUpdatePCP)
+ .on("wheel", rotateLabels);
+
+ // Update
+ g_data.attr("opacity", 0);
+ g_data.select(".axis")
+ .transition()
+ .duration(1100)
+ .each(function(d) { d3.select(this).call( pc.applyAxisConfig(axis, __.dimensions[d]) )
+ });
+ g_data.select(".label")
+ .transition()
+ .duration(1100)
+ .text(dimensionLabels)
+ .attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")");
+
+ // Exit
+ g_data.exit().remove();
+
+ g = pc.svg.selectAll(".dimension");
+ g.transition().duration(1100)
+ .attr("transform", function(p) { return "translate(" + position(p) + ")"; })
+ .style("opacity", 1);
+
+ pc.svg.selectAll(".axis")
+ .transition()
+ .duration(1100)
+ .each(function(d) { d3.select(this).call( pc.applyAxisConfig(axis, __.dimensions[d]) );
+ });
+
+ if (flags.brushable) pc.brushable();
+ if (flags.reorderable) pc.reorderable();
+ if (pc.brushMode() !== "None") {
+ var mode = pc.brushMode();
+ pc.brushMode("None");
+ pc.brushMode(mode);
+ }
+ return this;
+};
+
+pc.applyAxisConfig = function(axis, dimension) {
+ return axis.scale(dimension.yscale)
+ .orient(dimension.orient)
+ .ticks(dimension.ticks)
+ .tickValues(dimension.tickValues)
+ .innerTickSize(dimension.innerTickSize)
+ .outerTickSize(dimension.outerTickSize)
+ .tickPadding(dimension.tickPadding)
+ .tickFormat(dimension.tickFormat)
+};
+
+// Jason Davies, http://bl.ocks.org/1341281
+pc.reorderable = function() {
+ if (!g) pc.createAxes();
+
+ g.style("cursor", "move")
+ .call(d3.behavior.drag()
+ .on("dragstart", function(d) {
+ dragging[d] = this.__origin__ = xscale(d);
+ })
+ .on("drag", function(d) {
+ dragging[d] = Math.min(w(), Math.max(0, this.__origin__ += d3.event.dx));
+ pc.sortDimensions();
+ xscale.domain(pc.getOrderedDimensionKeys());
+ pc.render();
+ g.attr("transform", function(d) {
+ return "translate(" + position(d) + ")";
+ });
+ })
+ .on("dragend", function(d) {
+ // Let's see if the order has changed and send out an event if so.
+ var i = 0,
+ j = __.dimensions[d].index,
+ elem = this,
+ parent = this.parentElement;
+
+ while((elem = elem.previousElementSibling) != null) ++i;
+ if (i !== j) {
+ events.axesreorder.call(pc, pc.getOrderedDimensionKeys());
+ // We now also want to reorder the actual dom elements that represent
+ // the axes. That is, the g.dimension elements. If we don't do this,
+ // we get a weird and confusing transition when updateAxes is called.
+ // This is due to the fact that, initially the nth g.dimension element
+ // represents the nth axis. However, after a manual reordering,
+ // without reordering the dom elements, the nth dom elements no longer
+ // necessarily represents the nth axis.
+ //
+ // i is the original index of the dom element
+ // j is the new index of the dom element
+ if (i > j) { // Element moved left
+ parent.insertBefore(this, parent.children[j - 1]);
+ } else { // Element moved right
+ if ((j + 1) < parent.children.length) {
+ parent.insertBefore(this, parent.children[j + 1]);
+ } else {
+ parent.appendChild(this);
+ }
+ }
+ }
+
+ delete this.__origin__;
+ delete dragging[d];
+ d3.select(this).transition().attr("transform", "translate(" + xscale(d) + ")");
+ pc.render();
+ }));
+ flags.reorderable = true;
+ return this;
+};
+
+// Reorder dimensions, such that the highest value (visually) is on the left and
+// the lowest on the right. Visual values are determined by the data values in
+// the given row.
+pc.reorder = function(rowdata) {
+ var firstDim = pc.getOrderedDimensionKeys()[0];
+
+ pc.sortDimensionsByRowData(rowdata);
+ // NOTE: this is relatively cheap given that:
+ // number of dimensions < number of data items
+ // Thus we check equality of order to prevent rerendering when this is the case.
+ var reordered = false;
+ reordered = firstDim !== pc.getOrderedDimensionKeys()[0];
+
+ if (reordered) {
+ xscale.domain(pc.getOrderedDimensionKeys());
+ var highlighted = __.highlighted.slice(0);
+ pc.unhighlight();
+
+ g.transition()
+ .duration(1500)
+ .attr("transform", function(d) {
+ return "translate(" + xscale(d) + ")";
+ });
+ pc.render();
+
+ // pc.highlight() does not check whether highlighted is length zero, so we do that here.
+ if (highlighted.length !== 0) {
+ pc.highlight(highlighted);
+ }
+ }
+}
+
+pc.sortDimensionsByRowData = function(rowdata) {
+ var copy = __.dimensions;
+ var positionSortedKeys = d3.keys(__.dimensions).sort(function(a, b) {
+ var pixelDifference = __.dimensions[a].yscale(rowdata[a]) - __.dimensions[b].yscale(rowdata[b]);
+
+ // Array.sort is not necessarily stable, this means that if pixelDifference is zero
+ // the ordering of dimensions might change unexpectedly. This is solved by sorting on
+ // variable name in that case.
+ if (pixelDifference === 0) {
+ return a.localeCompare(b);
+ } // else
+ return pixelDifference;
+ });
+ __.dimensions = {};
+ positionSortedKeys.forEach(function(p, i){
+ __.dimensions[p] = copy[p];
+ __.dimensions[p].index = i;
+ });
+}
+
+pc.sortDimensions = function() {
+ var copy = __.dimensions;
+ var positionSortedKeys = d3.keys(__.dimensions).sort(function(a, b) {
+ return position(a) - position(b);
+ });
+ __.dimensions = {};
+ positionSortedKeys.forEach(function(p, i){
+ __.dimensions[p] = copy[p];
+ __.dimensions[p].index = i;
+ })
+};
+
+// pairs of adjacent dimensions
+pc.adjacent_pairs = function(arr) {
+ var ret = [];
+ for (var i = 0; i < arr.length-1; i++) {
+ ret.push([arr[i],arr[i+1]]);
+ };
+ return ret;
+};
+
+var brush = {
+ modes: {
+ "None": {
+ install: function(pc) {}, // Nothing to be done.
+ uninstall: function(pc) {}, // Nothing to be done.
+ selected: function() { return []; }, // Nothing to return
+ brushState: function() { return {}; }
+ }
+ },
+ mode: "None",
+ predicate: "AND",
+ currentMode: function() {
+ return this.modes[this.mode];
+ }
+};
+
+// This function can be used for 'live' updates of brushes. That is, during the
+// specification of a brush, this method can be called to update the view.
+//
+// @param newSelection - The new set of data items that is currently contained
+// by the brushes
+function brushUpdated(newSelection) {
+ __.brushed = newSelection;
+ events.brush.call(pc,__.brushed);
+ pc.renderBrushed();
+}
+
+function brushPredicate(predicate) {
+ if (!arguments.length) { return brush.predicate; }
+
+ predicate = String(predicate).toUpperCase();
+ if (predicate !== "AND" && predicate !== "OR") {
+ throw "Invalid predicate " + predicate;
+ }
+
+ brush.predicate = predicate;
+ __.brushed = brush.currentMode().selected();
+ pc.renderBrushed();
+ return pc;
+}
+
+pc.brushModes = function() {
+ return Object.getOwnPropertyNames(brush.modes);
+};
+
+pc.brushMode = function(mode) {
+ if (arguments.length === 0) {
+ return brush.mode;
+ }
+
+ if (pc.brushModes().indexOf(mode) === -1) {
+ throw "pc.brushmode: Unsupported brush mode: " + mode;
+ }
+
+ // Make sure that we don't trigger unnecessary events by checking if the mode
+ // actually changes.
+ if (mode !== brush.mode) {
+ // When changing brush modes, the first thing we need to do is clearing any
+ // brushes from the current mode, if any.
+ if (brush.mode !== "None") {
+ pc.brushReset();
+ }
+
+ // Next, we need to 'uninstall' the current brushMode.
+ brush.modes[brush.mode].uninstall(pc);
+ // Finally, we can install the requested one.
+ brush.mode = mode;
+ brush.modes[brush.mode].install();
+ if (mode === "None") {
+ delete pc.brushPredicate;
+ } else {
+ pc.brushPredicate = brushPredicate;
+ }
+ }
+
+ return pc;
+};
+
+// brush mode: 1D-Axes
+
+(function() {
+ var brushes = {};
+
+ function is_brushed(p) {
+ return !brushes[p].empty();
+ }
+
+ // data within extents
+ function selected() {
+ var actives = d3.keys(__.dimensions).filter(is_brushed),
+ extents = actives.map(function(p) { return brushes[p].extent(); });
+
+ // We don't want to return the full data set when there are no axes brushed.
+ // Actually, when there are no axes brushed, by definition, no items are
+ // selected. So, let's avoid the filtering and just return false.
+ //if (actives.length === 0) return false;
+
+ // Resolves broken examples for now. They expect to get the full dataset back from empty brushes
+ if (actives.length === 0) return __.data;
+
+ // test if within range
+ var within = {
+ "date": function(d,p,dimension) {
+ if (typeof __.dimensions[p].yscale.rangePoints === "function") { // if it is ordinal
+ return extents[dimension][0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= extents[dimension][1]
+ } else {
+ return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
+ }
+ },
+ "number": function(d,p,dimension) {
+ if (typeof __.dimensions[p].yscale.rangePoints === "function") { // if it is ordinal
+ return extents[dimension][0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= extents[dimension][1]
+ } else {
+ return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
+ }
+ },
+ "string": function(d,p,dimension) {
+ return extents[dimension][0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= extents[dimension][1]
+ }
+ };
+
+ return __.data
+ .filter(function(d) {
+ switch(brush.predicate) {
+ case "AND":
+ return actives.every(function(p, dimension) {
+ return within[__.dimensions[p].type](d,p,dimension);
+ });
+ case "OR":
+ return actives.some(function(p, dimension) {
+ return within[__.dimensions[p].type](d,p,dimension);
+ });
+ default:
+ throw "Unknown brush predicate " + __.brushPredicate;
+ }
+ });
+ };
+
+ function brushExtents(extents) {
+ if(typeof(extents) === 'undefined')
+ {
+ var extents = {};
+ d3.keys(__.dimensions).forEach(function(d) {
+ var brush = brushes[d];
+ if (brush !== undefined && !brush.empty()) {
+ var extent = brush.extent();
+ extent.sort(d3.ascending);
+ extents[d] = extent;
+ }
+ });
+ return extents;
+ }
+ else
+ {
+ //first get all the brush selections
+ var brushSelections = {};
+ g.selectAll('.brush')
+ .each(function(d) {
+ brushSelections[d] = d3.select(this);
+
+ });
+
+ // loop over each dimension and update appropriately (if it was passed in through extents)
+ d3.keys(__.dimensions).forEach(function(d) {
+ if (extents[d] === undefined){
+ return;
+ }
+
+ var brush = brushes[d];
+ if (brush !== undefined) {
+ //update the extent
+ brush.extent(extents[d]);
+
+ //redraw the brush
+ brush(brushSelections[d]);
+
+ //fire some events
+ brush.event(brushSelections[d]);
+ }
+ });
+
+ //redraw the chart
+ pc.renderBrushed();
+ }
+ }
+
+ function brushFor(axis) {
+ var brush = d3.svg.brush();
+
+ brush
+ .y(__.dimensions[axis].yscale)
+ .on("brushstart", function() {
+ if(d3.event.sourceEvent !== null) {
+ d3.event.sourceEvent.stopPropagation();
+ }
+ })
+ .on("brush", function() {
+ brushUpdated(selected());
+ })
+ .on("brushend", function() {
+ events.brushend.call(pc, __.brushed);
+ });
+
+ brushes[axis] = brush;
+ return brush;
+ };
+ function brushReset(dimension) {
+ __.brushed = false;
+ if (g) {
+ g.selectAll('.brush')
+ .each(function(d) {
+ d3.select(this).call(
+ brushes[d].clear()
+ );
+ });
+ pc.renderBrushed();
+ }
+ return this;
+ };
+
+ function install() {
+ if (!g) pc.createAxes();
+
+ // Add and store a brush for each axis.
+ g.append("svg:g")
+ .attr("class", "brush")
+ .each(function(d) {
+ d3.select(this).call(brushFor(d));
+ })
+ .selectAll("rect")
+ .style("visibility", null)
+ .attr("x", -15)
+ .attr("width", 30);
+
+ pc.brushExtents = brushExtents;
+ pc.brushReset = brushReset;
+ return pc;
+ };
+
+ brush.modes["1D-axes"] = {
+ install: install,
+ uninstall: function() {
+ g.selectAll(".brush").remove();
+ brushes = {};
+ delete pc.brushExtents;
+ delete pc.brushReset;
+ },
+ selected: selected,
+ brushState: brushExtents
+ }
+})();
+// brush mode: 2D-strums
+// bl.ocks.org/syntagmatic/5441022
+
+(function() {
+ var strums = {},
+ strumRect;
+
+ function drawStrum(strum, activePoint) {
+ var svg = pc.selection.select("svg").select("g#strums"),
+ id = strum.dims.i,
+ points = [strum.p1, strum.p2],
+ line = svg.selectAll("line#strum-" + id).data([strum]),
+ circles = svg.selectAll("circle#strum-" + id).data(points),
+ drag = d3.behavior.drag();
+
+ line.enter()
+ .append("line")
+ .attr("id", "strum-" + id)
+ .attr("class", "strum");
+
+ line
+ .attr("x1", function(d) {
+ return d.p1[0]; })
+ .attr("y1", function(d) {
+ return d.p1[1]; })
+ .attr("x2", function(d) {
+ return d.p2[0]; })
+ .attr("y2", function(d) {
+ return d.p2[1]; })
+ .attr("stroke", "black")
+ .attr("stroke-width", 2);
+
+ drag
+ .on("drag", function(d, i) {
+ var ev = d3.event;
+ i = i + 1;
+ strum["p" + i][0] = Math.min(Math.max(strum.minX + 1, ev.x), strum.maxX);
+ strum["p" + i][1] = Math.min(Math.max(strum.minY, ev.y), strum.maxY);
+ drawStrum(strum, i - 1);
+ })
+ .on("dragend", onDragEnd());
+
+ circles.enter()
+ .append("circle")
+ .attr("id", "strum-" + id)
+ .attr("class", "strum");
+
+ circles
+ .attr("cx", function(d) { return d[0]; })
+ .attr("cy", function(d) { return d[1]; })
+ .attr("r", 5)
+ .style("opacity", function(d, i) {
+ return (activePoint !== undefined && i === activePoint) ? 0.8 : 0;
+ })
+ .on("mouseover", function() {
+ d3.select(this).style("opacity", 0.8);
+ })
+ .on("mouseout", function() {
+ d3.select(this).style("opacity", 0);
+ })
+ .call(drag);
+ }
+
+ function dimensionsForPoint(p) {
+ var dims = { i: -1, left: undefined, right: undefined };
+ d3.keys(__.dimensions).some(function(dim, i) {
+ if (xscale(dim) < p[0]) {
+ var next = d3.keys(__.dimensions)[pc.getOrderedDimensionKeys().indexOf(dim)+1];
+ dims.i = i;
+ dims.left = dim;
+ dims.right = next;
+ return false;
+ }
+ return true;
+ });
+
+ if (dims.left === undefined) {
+ // Event on the left side of the first axis.
+ dims.i = 0;
+ dims.left = pc.getOrderedDimensionKeys()[0];
+ dims.right = pc.getOrderedDimensionKeys()[1];
+ } else if (dims.right === undefined) {
+ // Event on the right side of the last axis
+ dims.i = d3.keys(__.dimensions).length - 1;
+ dims.right = dims.left;
+ dims.left = pc.getOrderedDimensionKeys()[d3.keys(__.dimensions).length - 2];
+ }
+
+ return dims;
+ }
+
+ function onDragStart() {
+ // First we need to determine between which two axes the sturm was started.
+ // This will determine the freedom of movement, because a strum can
+ // logically only happen between two axes, so no movement outside these axes
+ // should be allowed.
+ return function() {
+ var p = d3.mouse(strumRect[0][0]),
+ dims,
+ strum;
+
+ p[0] = p[0] - __.margin.left;
+ p[1] = p[1] - __.margin.top;
+
+ dims = dimensionsForPoint(p),
+ strum = {
+ p1: p,
+ dims: dims,
+ minX: xscale(dims.left),
+ maxX: xscale(dims.right),
+ minY: 0,
+ maxY: h()
+ };
+
+ strums[dims.i] = strum;
+ strums.active = dims.i;
+
+ // Make sure that the point is within the bounds
+ strum.p1[0] = Math.min(Math.max(strum.minX, p[0]), strum.maxX);
+ strum.p2 = strum.p1.slice();
+ };
+ }
+
+ function onDrag() {
+ return function() {
+ var ev = d3.event,
+ strum = strums[strums.active];
+
+ // Make sure that the point is within the bounds
+ strum.p2[0] = Math.min(Math.max(strum.minX + 1, ev.x - __.margin.left), strum.maxX);
+ strum.p2[1] = Math.min(Math.max(strum.minY, ev.y - __.margin.top), strum.maxY);
+ drawStrum(strum, 1);
+ };
+ }
+
+ function containmentTest(strum, width) {
+ var p1 = [strum.p1[0] - strum.minX, strum.p1[1] - strum.minX],
+ p2 = [strum.p2[0] - strum.minX, strum.p2[1] - strum.minX],
+ m1 = 1 - width / p1[0],
+ b1 = p1[1] * (1 - m1),
+ m2 = 1 - width / p2[0],
+ b2 = p2[1] * (1 - m2);
+
+ // test if point falls between lines
+ return function(p) {
+ var x = p[0],
+ y = p[1],
+ y1 = m1 * x + b1,
+ y2 = m2 * x + b2;
+
+ if (y > Math.min(y1, y2) && y < Math.max(y1, y2)) {
+ return true;
+ }
+
+ return false;
+ };
+ }
+
+ function selected() {
+ var ids = Object.getOwnPropertyNames(strums),
+ brushed = __.data;
+
+ // Get the ids of the currently active strums.
+ ids = ids.filter(function(d) {
+ return !isNaN(d);
+ });
+
+ function crossesStrum(d, id) {
+ var strum = strums[id],
+ test = containmentTest(strum, strums.width(id)),
+ d1 = strum.dims.left,
+ d2 = strum.dims.right,
+ y1 = __.dimensions[d1].yscale,
+ y2 = __.dimensions[d2].yscale,
+ point = [y1(d[d1]) - strum.minX, y2(d[d2]) - strum.minX];
+ return test(point);
+ }
+
+ if (ids.length === 0) { return brushed; }
+
+ return brushed.filter(function(d) {
+ switch(brush.predicate) {
+ case "AND":
+ return ids.every(function(id) { return crossesStrum(d, id); });
+ case "OR":
+ return ids.some(function(id) { return crossesStrum(d, id); });
+ default:
+ throw "Unknown brush predicate " + __.brushPredicate;
+ }
+ });
+ }
+
+ function removeStrum() {
+ var strum = strums[strums.active],
+ svg = pc.selection.select("svg").select("g#strums");
+
+ delete strums[strums.active];
+ strums.active = undefined;
+ svg.selectAll("line#strum-" + strum.dims.i).remove();
+ svg.selectAll("circle#strum-" + strum.dims.i).remove();
+ }
+
+ function onDragEnd() {
+ return function() {
+ var brushed = __.data,
+ strum = strums[strums.active];
+
+ // Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
+ // considered a drag without move. So we have to deal with that case
+ if (strum && strum.p1[0] === strum.p2[0] && strum.p1[1] === strum.p2[1]) {
+ removeStrum(strums);
+ }
+
+ brushed = selected(strums);
+ strums.active = undefined;
+ __.brushed = brushed;
+ pc.renderBrushed();
+ events.brushend.call(pc, __.brushed);
+ };
+ }
+
+ function brushReset(strums) {
+ return function() {
+ var ids = Object.getOwnPropertyNames(strums).filter(function(d) {
+ return !isNaN(d);
+ });
+
+ ids.forEach(function(d) {
+ strums.active = d;
+ removeStrum(strums);
+ });
+ onDragEnd(strums)();
+ };
+ }
+
+ function install() {
+ var drag = d3.behavior.drag();
+
+ // Map of current strums. Strums are stored per segment of the PC. A segment,
+ // being the area between two axes. The left most area is indexed at 0.
+ strums.active = undefined;
+ // Returns the width of the PC segment where currently a strum is being
+ // placed. NOTE: even though they are evenly spaced in our current
+ // implementation, we keep for when non-even spaced segments are supported as
+ // well.
+ strums.width = function(id) {
+ var strum = strums[id];
+
+ if (strum === undefined) {
+ return undefined;
+ }
+
+ return strum.maxX - strum.minX;
+ };
+
+ pc.on("axesreorder.strums", function() {
+ var ids = Object.getOwnPropertyNames(strums).filter(function(d) {
+ return !isNaN(d);
+ });
+
+ // Checks if the first dimension is directly left of the second dimension.
+ function consecutive(first, second) {
+ var length = d3.keys(__.dimensions).length;
+ return d3.keys(__.dimensions).some(function(d, i) {
+ return (d === first)
+ ? i + i < length && __.dimensions[i + 1] === second
+ : false;
+ });
+ }
+
+ if (ids.length > 0) { // We have some strums, which might need to be removed.
+ ids.forEach(function(d) {
+ var dims = strums[d].dims;
+ strums.active = d;
+ // If the two dimensions of the current strum are not next to each other
+ // any more, than we'll need to remove the strum. Otherwise we keep it.
+ if (!consecutive(dims.left, dims.right)) {
+ removeStrum(strums);
+ }
+ });
+ onDragEnd(strums)();
+ }
+ });
+
+ // Add a new svg group in which we draw the strums.
+ pc.selection.select("svg").append("g")
+ .attr("id", "strums")
+ .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+ // Install the required brushReset function
+ pc.brushReset = brushReset(strums);
+
+ drag
+ .on("dragstart", onDragStart(strums))
+ .on("drag", onDrag(strums))
+ .on("dragend", onDragEnd(strums));
+
+ // NOTE: The styling needs to be done here and not in the css. This is because
+ // for 1D brushing, the canvas layers should not listen to
+ // pointer-events.
+ strumRect = pc.selection.select("svg").insert("rect", "g#strums")
+ .attr("id", "strum-events")
+ .attr("x", __.margin.left)
+ .attr("y", __.margin.top)
+ .attr("width", w())
+ .attr("height", h() + 2)
+ .style("opacity", 0)
+ .call(drag);
+ }
+
+ brush.modes["2D-strums"] = {
+ install: install,
+ uninstall: function() {
+ pc.selection.select("svg").select("g#strums").remove();
+ pc.selection.select("svg").select("rect#strum-events").remove();
+ pc.on("axesreorder.strums", undefined);
+ delete pc.brushReset;
+
+ strumRect = undefined;
+ },
+ selected: selected,
+ brushState: function () { return strums; }
+ };
+
+}());
+
+// brush mode: 1D-Axes with multiple extents
+// requires d3.svg.multibrush
+
+(function() {
+ if (typeof d3.svg.multibrush !== 'function') {
+ return;
+ }
+ var brushes = {};
+
+ function is_brushed(p) {
+ return !brushes[p].empty();
+ }
+
+ // data within extents
+ function selected() {
+ var actives = d3.keys(__.dimensions).filter(is_brushed),
+ extents = actives.map(function(p) { return brushes[p].extent(); });
+
+ // We don't want to return the full data set when there are no axes brushed.
+ // Actually, when there are no axes brushed, by definition, no items are
+ // selected. So, let's avoid the filtering and just return false.
+ //if (actives.length === 0) return false;
+
+ // Resolves broken examples for now. They expect to get the full dataset back from empty brushes
+ if (actives.length === 0) return __.data;
+
+ // test if within range
+ var within = {
+ "date": function(d,p,dimension,b) {
+ if (typeof __.dimensions[p].yscale.rangePoints === "function") { // if it is ordinal
+ return b[0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= b[1]
+ } else {
+ return b[0] <= d[p] && d[p] <= b[1]
+ }
+ },
+ "number": function(d,p,dimension,b) {
+ if (typeof __.dimensions[p].yscale.rangePoints === "function") { // if it is ordinal
+ return b[0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= b[1]
+ } else {
+ return b[0] <= d[p] && d[p] <= b[1]
+ }
+ },
+ "string": function(d,p,dimension,b) {
+ return b[0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= b[1]
+ }
+ };
+
+ return __.data
+ .filter(function(d) {
+ switch(brush.predicate) {
+ case "AND":
+ return actives.every(function(p, dimension) {
+ return extents[dimension].some(function(b) {
+ return within[__.dimensions[p].type](d,p,dimension,b);
+ });
+ });
+ case "OR":
+ return actives.some(function(p, dimension) {
+ return extents[dimension].some(function(b) {
+ return within[__.dimensions[p].type](d,p,dimension,b);
+ });
+ });
+ default:
+ throw "Unknown brush predicate " + __.brushPredicate;
+ }
+ });
+ };
+
+ function brushExtents() {
+ var extents = {};
+ d3.keys(__.dimensions).forEach(function(d) {
+ var brush = brushes[d];
+ if (brush !== undefined && !brush.empty()) {
+ var extent = brush.extent();
+ extents[d] = extent;
+ }
+ });
+ return extents;
+ }
+
+ function brushFor(axis) {
+ var brush = d3.svg.multibrush();
+
+ brush
+ .y(__.dimensions[axis].yscale)
+ .on("brushstart", function() {
+ if(d3.event.sourceEvent !== null) {
+ d3.event.sourceEvent.stopPropagation();
+ }
+ })
+ .on("brush", function() {
+ brushUpdated(selected());
+ })
+ .on("brushend", function() {
+ // d3.svg.multibrush clears extents just before calling 'brushend'
+ // so we have to update here again.
+ // This fixes issue #103 for now, but should be changed in d3.svg.multibrush
+ // to avoid unnecessary computation.
+ brushUpdated(selected());
+ events.brushend.call(pc, __.brushed);
+ })
+ .extentAdaption(function(selection) {
+ selection
+ .style("visibility", null)
+ .attr("x", -15)
+ .attr("width", 30);
+ })
+ .resizeAdaption(function(selection) {
+ selection
+ .selectAll("rect")
+ .attr("x", -15)
+ .attr("width", 30);
+ });
+
+ brushes[axis] = brush;
+ return brush;
+ }
+
+ function brushReset(dimension) {
+ __.brushed = false;
+ if (g) {
+ g.selectAll('.brush')
+ .each(function(d) {
+ d3.select(this).call(
+ brushes[d].clear()
+ );
+ });
+ pc.renderBrushed();
+ }
+ return this;
+ };
+
+ function install() {
+ if (!g) pc.createAxes();
+
+ // Add and store a brush for each axis.
+ g.append("svg:g")
+ .attr("class", "brush")
+ .each(function(d) {
+ d3.select(this).call(brushFor(d));
+ })
+ .selectAll("rect")
+ .style("visibility", null)
+ .attr("x", -15)
+ .attr("width", 30);
+
+ pc.brushExtents = brushExtents;
+ pc.brushReset = brushReset;
+ return pc;
+ }
+
+ brush.modes["1D-axes-multi"] = {
+ install: install,
+ uninstall: function() {
+ g.selectAll(".brush").remove();
+ brushes = {};
+ delete pc.brushExtents;
+ delete pc.brushReset;
+ },
+ selected: selected,
+ brushState: brushExtents
+ }
+})();
+// brush mode: angular
+// code based on 2D.strums.js
+
+(function() {
+ var arcs = {},
+ strumRect;
+
+ function drawStrum(arc, activePoint) {
+ var svg = pc.selection.select("svg").select("g#arcs"),
+ id = arc.dims.i,
+ points = [arc.p2, arc.p3],
+ line = svg.selectAll("line#arc-" + id).data([{p1:arc.p1,p2:arc.p2},{p1:arc.p1,p2:arc.p3}]),
+ circles = svg.selectAll("circle#arc-" + id).data(points),
+ drag = d3.behavior.drag(),
+ path = svg.selectAll("path#arc-" + id).data([arc]);
+
+ path.enter()
+ .append("path")
+ .attr("id", "arc-" + id)
+ .attr("class", "arc")
+ .style("fill", "orange")
+ .style("opacity", 0.5);
+
+ path
+ .attr("d", arc.arc)
+ .attr("transform", "translate(" + arc.p1[0] + "," + arc.p1[1] + ")");
+
+ line.enter()
+ .append("line")
+ .attr("id", "arc-" + id)
+ .attr("class", "arc");
+
+ line
+ .attr("x1", function(d) { return d.p1[0]; })
+ .attr("y1", function(d) { return d.p1[1]; })
+ .attr("x2", function(d) { return d.p2[0]; })
+ .attr("y2", function(d) { return d.p2[1]; })
+ .attr("stroke", "black")
+ .attr("stroke-width", 2);
+
+ drag
+ .on("drag", function(d, i) {
+ var ev = d3.event,
+ angle = 0;
+
+ i = i + 2;
+
+ arc["p" + i][0] = Math.min(Math.max(arc.minX + 1, ev.x), arc.maxX);
+ arc["p" + i][1] = Math.min(Math.max(arc.minY, ev.y), arc.maxY);
+
+ angle = i === 3 ? arcs.startAngle(id) : arcs.endAngle(id);
+
+ if ((arc.startAngle < Math.PI && arc.endAngle < Math.PI && angle < Math.PI) ||
+ (arc.startAngle >= Math.PI && arc.endAngle >= Math.PI && angle >= Math.PI)) {
+
+ if (i === 2) {
+ arc.endAngle = angle;
+ arc.arc.endAngle(angle);
+ } else if (i === 3) {
+ arc.startAngle = angle;
+ arc.arc.startAngle(angle);
+ }
+
+ }
+
+ drawStrum(arc, i - 2);
+ })
+ .on("dragend", onDragEnd());
+
+ circles.enter()
+ .append("circle")
+ .attr("id", "arc-" + id)
+ .attr("class", "arc");
+
+ circles
+ .attr("cx", function(d) { return d[0]; })
+ .attr("cy", function(d) { return d[1]; })
+ .attr("r", 5)
+ .style("opacity", function(d, i) {
+ return (activePoint !== undefined && i === activePoint) ? 0.8 : 0;
+ })
+ .on("mouseover", function() {
+ d3.select(this).style("opacity", 0.8);
+ })
+ .on("mouseout", function() {
+ d3.select(this).style("opacity", 0);
+ })
+ .call(drag);
+ }
+
+ function dimensionsForPoint(p) {
+ var dims = { i: -1, left: undefined, right: undefined };
+ d3.keys(__.dimensions).some(function(dim, i) {
+ if (xscale(dim) < p[0]) {
+ var next = d3.keys(__.dimensions)[pc.getOrderedDimensionKeys().indexOf(dim)+1];
+ dims.i = i;
+ dims.left = dim;
+ dims.right = next;
+ return false;
+ }
+ return true;
+ });
+
+ if (dims.left === undefined) {
+ // Event on the left side of the first axis.
+ dims.i = 0;
+ dims.left = pc.getOrderedDimensionKeys()[0];
+ dims.right = pc.getOrderedDimensionKeys()[1];
+ } else if (dims.right === undefined) {
+ // Event on the right side of the last axis
+ dims.i = d3.keys(__.dimensions).length - 1;
+ dims.right = dims.left;
+ dims.left = pc.getOrderedDimensionKeys()[d3.keys(__.dimensions).length - 2];
+ }
+
+ return dims;
+ }
+
+ function onDragStart() {
+ // First we need to determine between which two axes the arc was started.
+ // This will determine the freedom of movement, because a arc can
+ // logically only happen between two axes, so no movement outside these axes
+ // should be allowed.
+ return function() {
+ var p = d3.mouse(strumRect[0][0]),
+ dims,
+ arc;
+
+ p[0] = p[0] - __.margin.left;
+ p[1] = p[1] - __.margin.top;
+
+ dims = dimensionsForPoint(p),
+ arc = {
+ p1: p,
+ dims: dims,
+ minX: xscale(dims.left),
+ maxX: xscale(dims.right),
+ minY: 0,
+ maxY: h(),
+ startAngle: undefined,
+ endAngle: undefined,
+ arc: d3.svg.arc().innerRadius(0)
+ };
+
+ arcs[dims.i] = arc;
+ arcs.active = dims.i;
+
+ // Make sure that the point is within the bounds
+ arc.p1[0] = Math.min(Math.max(arc.minX, p[0]), arc.maxX);
+ arc.p2 = arc.p1.slice();
+ arc.p3 = arc.p1.slice();
+ };
+ }
+
+ function onDrag() {
+ return function() {
+ var ev = d3.event,
+ arc = arcs[arcs.active];
+
+ // Make sure that the point is within the bounds
+ arc.p2[0] = Math.min(Math.max(arc.minX + 1, ev.x - __.margin.left), arc.maxX);
+ arc.p2[1] = Math.min(Math.max(arc.minY, ev.y - __.margin.top), arc.maxY);
+ arc.p3 = arc.p2.slice();
+// console.log(arcs.angle(arcs.active));
+// console.log(signedAngle(arcs.unsignedAngle(arcs.active)));
+ drawStrum(arc, 1);
+ };
+ }
+
+ // some helper functions
+ function hypothenuse(a, b) {
+ return Math.sqrt(a*a + b*b);
+ }
+
+ var rad = (function() {
+ var c = Math.PI / 180;
+ return function(angle) {
+ return angle * c;
+ };
+ })();
+
+ var deg = (function() {
+ var c = 180 / Math.PI;
+ return function(angle) {
+ return angle * c;
+ };
+ })();
+
+ // [0, 2*PI] -> [-PI/2, PI/2]
+ var signedAngle = function(angle) {
+ var ret = angle;
+ if (angle > Math.PI) {
+ ret = angle - 1.5 * Math.PI;
+ ret = angle - 1.5 * Math.PI;
+ } else {
+ ret = angle - 0.5 * Math.PI;
+ ret = angle - 0.5 * Math.PI;
+ }
+ return -ret;
+ }
+
+ /**
+ * angles are stored in radians from in [0, 2*PI], where 0 in 12 o'clock.
+ * However, one can only select lines from 0 to PI, so we compute the
+ * 'signed' angle, where 0 is the horizontal line (3 o'clock), and +/- PI/2
+ * are 12 and 6 o'clock respectively.
+ */
+ function containmentTest(arc) {
+ var startAngle = signedAngle(arc.startAngle);
+ var endAngle = signedAngle(arc.endAngle);
+
+ if (startAngle > endAngle) {
+ var tmp = startAngle;
+ startAngle = endAngle;
+ endAngle = tmp;
+ }
+
+ // test if segment angle is contained in angle interval
+ return function(a) {
+
+ if (a >= startAngle && a <= endAngle) {
+ return true;
+ }
+
+ return false;
+ };
+ }
+
+ function selected() {
+ var ids = Object.getOwnPropertyNames(arcs),
+ brushed = __.data;
+
+ // Get the ids of the currently active arcs.
+ ids = ids.filter(function(d) {
+ return !isNaN(d);
+ });
+
+ function crossesStrum(d, id) {
+ var arc = arcs[id],
+ test = containmentTest(arc),
+ d1 = arc.dims.left,
+ d2 = arc.dims.right,
+ y1 = __.dimensions[d1].yscale,
+ y2 = __.dimensions[d2].yscale,
+ a = arcs.width(id),
+ b = y1(d[d1]) - y2(d[d2]),
+ c = hypothenuse(a, b),
+ angle = Math.asin(b/c); // rad in [-PI/2, PI/2]
+ return test(angle);
+ }
+
+ if (ids.length === 0) { return brushed; }
+
+ return brushed.filter(function(d) {
+ switch(brush.predicate) {
+ case "AND":
+ return ids.every(function(id) { return crossesStrum(d, id); });
+ case "OR":
+ return ids.some(function(id) { return crossesStrum(d, id); });
+ default:
+ throw "Unknown brush predicate " + __.brushPredicate;
+ }
+ });
+ }
+
+ function removeStrum() {
+ var arc = arcs[arcs.active],
+ svg = pc.selection.select("svg").select("g#arcs");
+
+ delete arcs[arcs.active];
+ arcs.active = undefined;
+ svg.selectAll("line#arc-" + arc.dims.i).remove();
+ svg.selectAll("circle#arc-" + arc.dims.i).remove();
+ svg.selectAll("path#arc-" + arc.dims.i).remove();
+ }
+
+ function onDragEnd() {
+ return function() {
+ var brushed = __.data,
+ arc = arcs[arcs.active];
+
+ // Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
+ // considered a drag without move. So we have to deal with that case
+ if (arc && arc.p1[0] === arc.p2[0] && arc.p1[1] === arc.p2[1]) {
+ removeStrum(arcs);
+ }
+
+ if (arc) {
+ var angle = arcs.startAngle(arcs.active);
+
+ arc.startAngle = angle;
+ arc.endAngle = angle;
+ arc.arc
+ .outerRadius(arcs.length(arcs.active))
+ .startAngle(angle)
+ .endAngle(angle);
+ }
+
+
+ brushed = selected(arcs);
+ arcs.active = undefined;
+ __.brushed = brushed;
+ pc.renderBrushed();
+ events.brushend.call(pc, __.brushed);
+ };
+ }
+
+ function brushReset(arcs) {
+ return function() {
+ var ids = Object.getOwnPropertyNames(arcs).filter(function(d) {
+ return !isNaN(d);
+ });
+
+ ids.forEach(function(d) {
+ arcs.active = d;
+ removeStrum(arcs);
+ });
+ onDragEnd(arcs)();
+ };
+ }
+
+ function install() {
+ var drag = d3.behavior.drag();
+
+ // Map of current arcs. arcs are stored per segment of the PC. A segment,
+ // being the area between two axes. The left most area is indexed at 0.
+ arcs.active = undefined;
+ // Returns the width of the PC segment where currently a arc is being
+ // placed. NOTE: even though they are evenly spaced in our current
+ // implementation, we keep for when non-even spaced segments are supported as
+ // well.
+ arcs.width = function(id) {
+ var arc = arcs[id];
+
+ if (arc === undefined) {
+ return undefined;
+ }
+
+ return arc.maxX - arc.minX;
+ };
+
+ // returns angles in [-PI/2, PI/2]
+ angle = function(p1, p2) {
+ var a = p1[0] - p2[0],
+ b = p1[1] - p2[1],
+ c = hypothenuse(a, b);
+
+ return Math.asin(b/c);
+ }
+
+ // returns angles in [0, 2 * PI]
+ arcs.endAngle = function(id) {
+ var arc = arcs[id];
+ if (arc === undefined) {
+ return undefined;
+ }
+ var sAngle = angle(arc.p1, arc.p2),
+ uAngle = -sAngle + Math.PI / 2;
+
+ if (arc.p1[0] > arc.p2[0]) {
+ uAngle = 2 * Math.PI - uAngle;
+ }
+
+ return uAngle;
+ }
+
+ arcs.startAngle = function(id) {
+ var arc = arcs[id];
+ if (arc === undefined) {
+ return undefined;
+ }
+
+ var sAngle = angle(arc.p1, arc.p3),
+ uAngle = -sAngle + Math.PI / 2;
+
+ if (arc.p1[0] > arc.p3[0]) {
+ uAngle = 2 * Math.PI - uAngle;
+ }
+
+ return uAngle;
+ }
+
+ arcs.length = function(id) {
+ var arc = arcs[id];
+
+ if (arc === undefined) {
+ return undefined;
+ }
+
+ var a = arc.p1[0] - arc.p2[0],
+ b = arc.p1[1] - arc.p2[1],
+ c = hypothenuse(a, b);
+
+ return(c);
+ }
+
+ pc.on("axesreorder.arcs", function() {
+ var ids = Object.getOwnPropertyNames(arcs).filter(function(d) {
+ return !isNaN(d);
+ });
+
+ // Checks if the first dimension is directly left of the second dimension.
+ function consecutive(first, second) {
+ var length = d3.keys(__.dimensions).length;
+ return d3.keys(__.dimensions).some(function(d, i) {
+ return (d === first)
+ ? i + i < length && __.dimensions[i + 1] === second
+ : false;
+ });
+ }
+
+ if (ids.length > 0) { // We have some arcs, which might need to be removed.
+ ids.forEach(function(d) {
+ var dims = arcs[d].dims;
+ arcs.active = d;
+ // If the two dimensions of the current arc are not next to each other
+ // any more, than we'll need to remove the arc. Otherwise we keep it.
+ if (!consecutive(dims.left, dims.right)) {
+ removeStrum(arcs);
+ }
+ });
+ onDragEnd(arcs)();
+ }
+ });
+
+ // Add a new svg group in which we draw the arcs.
+ pc.selection.select("svg").append("g")
+ .attr("id", "arcs")
+ .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+ // Install the required brushReset function
+ pc.brushReset = brushReset(arcs);
+
+ drag
+ .on("dragstart", onDragStart(arcs))
+ .on("drag", onDrag(arcs))
+ .on("dragend", onDragEnd(arcs));
+
+ // NOTE: The styling needs to be done here and not in the css. This is because
+ // for 1D brushing, the canvas layers should not listen to
+ // pointer-events.
+ strumRect = pc.selection.select("svg").insert("rect", "g#arcs")
+ .attr("id", "arc-events")
+ .attr("x", __.margin.left)
+ .attr("y", __.margin.top)
+ .attr("width", w())
+ .attr("height", h() + 2)
+ .style("opacity", 0)
+ .call(drag);
+ }
+
+ brush.modes["angular"] = {
+ install: install,
+ uninstall: function() {
+ pc.selection.select("svg").select("g#arcs").remove();
+ pc.selection.select("svg").select("rect#arc-events").remove();
+ pc.on("axesreorder.arcs", undefined);
+ delete pc.brushReset;
+
+ strumRect = undefined;
+ },
+ selected: selected,
+ brushState: function () { return arcs; }
+ };
+
+}());
+
+pc.interactive = function() {
+ flags.interactive = true;
+ return this;
+};
+
+// expose a few objects
+pc.xscale = xscale;
+pc.ctx = ctx;
+pc.canvas = canvas;
+pc.g = function() { return g; };
+
+// rescale for height, width and margins
+// TODO currently assumes chart is brushable, and destroys old brushes
+pc.resize = function() {
+ // selection size
+ pc.selection.select("svg")
+ .attr("width", __.width)
+ .attr("height", __.height)
+ pc.svg.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+ // FIXME: the current brush state should pass through
+ if (flags.brushable) pc.brushReset();
+
+ // scales
+ pc.autoscale();
+
+ // axes, destroys old brushes.
+ if (g) pc.createAxes();
+ if (flags.brushable) pc.brushable();
+ if (flags.reorderable) pc.reorderable();
+
+ events.resize.call(this, {width: __.width, height: __.height, margin: __.margin});
+ return this;
+};
+
+// highlight an array of data
+pc.highlight = function(data) {
+ if (arguments.length === 0) {
+ return __.highlighted;
+ }
+
+ __.highlighted = data;
+ pc.clear("highlight");
+ d3.selectAll([canvas.foreground, canvas.brushed]).classed("faded", true);
+ data.forEach(path_highlight);
+ events.highlight.call(this, data);
+ return this;
+};
+
+// clear highlighting
+pc.unhighlight = function() {
+ __.highlighted = [];
+ pc.clear("highlight");
+ d3.selectAll([canvas.foreground, canvas.brushed]).classed("faded", false);
+ return this;
+};
+
+// calculate 2d intersection of line a->b with line c->d
+// points are objects with x and y properties
+pc.intersection = function(a, b, c, d) {
+ return {
+ x: ((a.x * b.y - a.y * b.x) * (c.x - d.x) - (a.x - b.x) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
+ y: ((a.x * b.y - a.y * b.x) * (c.y - d.y) - (a.y - b.y) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x))
+ };
+};
+
+function position(d) {
+ if (xscale.range().length === 0) {
+ xscale.rangePoints([0, w()], 1);
+ }
+ var v = dragging[d];
+ return v == null ? xscale(d) : v;
+}
+pc.version = "0.7.0";
+ // this descriptive text should live with other introspective methods
+ pc.toString = function() { return "Parallel Coordinates: " + d3.keys(__.dimensions).length + " dimensions (" + d3.keys(__.data[0]).length + " total) , " + __.data.length + " rows"; };
+
+ return pc;
+};
+
+d3.renderQueue = (function(func) {
+ var _queue = [], // data to be rendered
+ _rate = 10, // number of calls per frame
+ _clear = function() {}, // clearing function
+ _i = 0; // current iteration
+
+ var rq = function(data) {
+ if (data) rq.data(data);
+ rq.invalidate();
+ _clear();
+ rq.render();
+ };
+
+ rq.render = function() {
+ _i = 0;
+ var valid = true;
+ rq.invalidate = function() { valid = false; };
+
+ function doFrame() {
+ if (!valid) return true;
+ if (_i > _queue.length) return true;
+
+ // Typical d3 behavior is to pass a data item *and* its index. As the
+ // render queue splits the original data set, we'll have to be slightly
+ // more carefull about passing the correct index with the data item.
+ var end = Math.min(_i + _rate, _queue.length);
+ for (var i = _i; i < end; i++) {
+ func(_queue[i], i);
+ }
+ _i += _rate;
+ }
+
+ d3.timer(doFrame);
+ };
+
+ rq.data = function(data) {
+ rq.invalidate();
+ _queue = data.slice(0);
+ return rq;
+ };
+
+ rq.rate = function(value) {
+ if (!arguments.length) return _rate;
+ _rate = value;
+ return rq;
+ };
+
+ rq.remaining = function() {
+ return _queue.length - _i;
+ };
+
+ // clear the canvas
+ rq.clear = function(func) {
+ if (!arguments.length) {
+ _clear();
+ return rq;
+ }
+ _clear = func;
+ return rq;
+ };
+
+ rq.invalidate = function() {};
+
+ return rq;
+});
\ No newline at end of file
diff --git a/public/js/graphsCar.js b/public/js/graphsCar.js
new file mode 100644
index 0000000..2363c5b
--- /dev/null
+++ b/public/js/graphsCar.js
@@ -0,0 +1,13 @@
+window.parallelBar = function(features){
+ d3.parcoords()("#graphContainer").data(features)
+ .composite("darken")
+ .color(window.getRandomColor())
+ .brushMode("1D-axes") // enable brushing
+ .interactive()
+ .createAxes().reorderable()
+ .render();
+};
+
+/*
+ global d3
+*/
\ No newline at end of file
diff --git a/public/js/map.js b/public/js/map.js
index b0cecc5..dbc753d 100644
--- a/public/js/map.js
+++ b/public/js/map.js
@@ -14,9 +14,11 @@ var map;
/*
global $
*/
-$(function () {
- map = buildMap("map_realtime");
-});
+window.startMap = function(idCar){
+ $("#"+idCar).css("width","100%");
+ $("#"+idCar).css("height","600px");
+ map = buildMap(idCar);
+};
/*
diff --git a/public/js/mapCar.js b/public/js/mapCar.js
new file mode 100644
index 0000000..6657e92
--- /dev/null
+++ b/public/js/mapCar.js
@@ -0,0 +1,128 @@
+var mapbox = {
+ idMapStreet: "imleo.o2ppnpfk",
+ idMapBlack: "imleo.o439ljf3",
+ token: "pk.eyJ1IjoiaW1sZW8iLCJhIjoiT0tfdlBSVSJ9.Qqzb4uGRSDRSGqZlV6koGg"
+};
+
+var controller = null;
+var map;
+
+
+/*
+ global $
+*/
+window.startMap = function(idCar, height){
+ $("#"+idCar).css("width","100%");
+ $("#"+idCar).css("height",height);
+ map = buildMap(idCar);
+};
+
+
+/*
+*
+* MAP
+*
+*/
+function buildMap(id_map) {
+ /*global L*/
+ var layerStreet = L.tileLayer('https://api.tiles.mapbox.com/v4/' + mapbox.idMapStreet + '/{z}/{x}/{y}.png?access_token=' + mapbox.token, {
+ maxZoom: 18,
+ minZoom: 3,
+ attribution: 'PwC México',
+ id: mapbox.idMapStreet,
+ accessToken: mapbox.token
+ });
+
+
+ var layerBlack = L.tileLayer('https://api.tiles.mapbox.com/v4/' + mapbox.idMapBlack + '/{z}/{x}/{y}.png?access_token=' + mapbox.token, {
+ maxZoom: 18,
+ minZoom: 3,
+ attribution: 'PwC México',
+ id: mapbox.idMapBlack,
+ accessToken: mapbox.token
+ });
+
+
+ var map = L.map(id_map, {
+ /*Options*/
+ layers: [layerStreet, layerBlack]
+ }).setView([19.4284700, -99.1276600], 12);
+
+
+ /*
+ Layer Controller
+ */
+ var baseMaps = {
+ "Streets": layerStreet,
+ "Black": layerBlack
+ };
+
+ var overlayMaps = {};
+
+
+ controller = L.control.layers(baseMaps, overlayMaps);
+ controller.addTo(map);
+
+
+ return map;
+}
+
+
+window.addLayer = function(features){
+ var routeLayer = L.layerGroup().addTo(map);
+ controller.addOverlay(routeLayer, features[0].properties.route);
+
+ var style = buildStyle();
+
+ for(var i = 0; i < features.length; i++){
+ var geoJSON = features[i];
+ var coordinates = geoJSON.geometry.coordinates;
+
+ var layer = L.circleMarker(
+ L.latLng(coordinates[1], coordinates[0]),
+ style
+ );
+
+ routeLayer.addLayer(layer);
+ layer.bindPopup(buildPopup(geoJSON));
+ }
+};
+
+
+function buildPopup(geojson){
+ var properties = geojson.properties;
+ var coordinates = geojson.geometry.coordinates;
+
+ //var html = "idCar: " + properties.idCar + "
";
+ var html = "";
+
+ for(var i = 0; i < properties.data.length; i++){
+ var data = properties.data[i];
+ html += "" + data[1] + ": " + data[2] + "
";
+ }
+
+
+ var popup = L.popup()
+ .setLatLng( coordinates )
+ .setContent(html);
+
+ return popup;
+}
+
+
+function buildStyle() {
+ var colorStroke = "#FFFFFF";
+
+ var aux = {
+ stroke: true,
+ color: colorStroke,
+ weight: 2,
+ fill: true,
+ fillColor: window.getRandomColor(),
+ opacity:1,
+ fillOpacity: 1,
+ radius: 6
+ };
+
+ return aux;
+}
diff --git a/public/js/socketMap.js b/public/js/socketMap.js
new file mode 100644
index 0000000..c56aed9
--- /dev/null
+++ b/public/js/socketMap.js
@@ -0,0 +1,13 @@
+/*global io*/
+var socket = io();
+
+socket.on('update_realtime', function (data) {
+ window.addFeature(data);
+});
+
+
+socket.on('delete_realtime', function (idCar) {
+ setTimeout(function(){
+ window.deleteLayer(idCar);
+ }, 1500);
+});
\ No newline at end of file
diff --git a/public/js/socketTrips.js b/public/js/socketTrips.js
new file mode 100644
index 0000000..cde26df
--- /dev/null
+++ b/public/js/socketTrips.js
@@ -0,0 +1,16 @@
+/*global io*/
+window.socket = io();
+/*
+socket.on('update_realtime', function (data) {
+
+});
+
+
+socket.on('delete_realtime', function (idCar) {
+
+});*/
+
+
+window.socket.on('carData', function(data){
+ window.addCarPanel(data);
+});
\ No newline at end of file
diff --git a/public/js/trips.js b/public/js/trips.js
index e69de29..1e678f9 100644
--- a/public/js/trips.js
+++ b/public/js/trips.js
@@ -0,0 +1,20 @@
+window.addCarPanel = function(data){
+ var html = '
' + data.idCar + '
';
+ html += '';
+ html += 'Distancia recorrida: ' + data.data.distance + ' (m)
';
+ html += 'Tiempo total: ' + data.data.total_time + ' (s)
';
+ html += 'Tiempo detenido (rpm=0): ' + data.data.stop_time + ' (s)
';
+ html += '
';
+
+ $("#panelContainer").append(html);
+};
+
+$(function(){
+ window.socket.emit("carsData");
+});
+
+
+
+/*
+ global $
+*/
\ No newline at end of file
diff --git a/rethinkDB.js b/rethinkDB.js
index 9778ea6..02de58f 100644
--- a/rethinkDB.js
+++ b/rethinkDB.js
@@ -197,12 +197,15 @@ function buildProperties(features, last_point){
for( var i = 0; i < features.length - 1; i++ ){
properties.distance += gju.pointDistance(features[i].geometry, features[i + 1].geometry);
- if(features[i].properties.data["rpm"] === 0)
- properties.stop_time++;
+ for(var j = 0; j < features[i].properties.data.length; j++){
+ if(features[i].properties.data[j][0] === 'speed' && features[i].properties.data[j][2] === 0){
+ properties.stop_time++;
+ break;
+ }
+ }
}
- properties.stop_time = (properties.stop_time - 1) * 5;
-
+ properties.stop_time = properties.stop_time * 5; // s
return properties;
}
diff --git a/routes/car.js b/routes/car.js
index 56d6ccf..8ac34e7 100644
--- a/routes/car.js
+++ b/routes/car.js
@@ -5,9 +5,12 @@ var router = express.Router();
router.all('*', function (req, res, next) {
var geojson = JSON.parse(req.body.data);
-
var coordinates = geojson.geometry.coordinates;
+ for(var i = 0; i < geojson.properties.data.length; i++){
+ geojson.properties.data[i][2] = parseFloat(geojson.properties.data[i][2]);
+ }
+
req.pgrouting.snapTogrid(coordinates[0], coordinates[1], function(err, result){
if(err){
console.log("Error snaping to grid", err);
diff --git a/routes/cars.js b/routes/cars.js
new file mode 100644
index 0000000..8b95836
--- /dev/null
+++ b/routes/cars.js
@@ -0,0 +1,17 @@
+
+var express = require('express');
+var router = express.Router();
+
+
+router.get('/', function (req, res, next) {
+ res.render('carsGlobal');
+});
+
+router.get('/:idCar', function (req, res, next) {
+ res.render('car',{
+ idCar: req.params.idCar
+ });
+});
+
+
+module.exports = router;
diff --git a/test/aveo_coords.js b/test/aveo_coords.js
index 6d6513c..1003ab4 100644
--- a/test/aveo_coords.js
+++ b/test/aveo_coords.js
@@ -1,32 +1 @@
-window.aveo_coords = [
- [-99.166058,19.442627],
- [-99.16595,19.442819],
- [-99.165881,19.44292],
- [-99.165795,19.443067],
- [-99.165714,19.443244]/*,
- [-99.165607,19.443451],
- [-99.165355,19.444387],
- [-99.165232,19.444969],
- [-99.16513,19.445429],
- [-99.165065,19.446183],
- [-99.16499,19.446537],
- [-99.164942,19.446754],
- [-99.164894,19.446937],
- [-99.164851,19.447154],
- [-99.164647,19.448049],
- [-99.164561,19.448424],
- [-99.164432,19.449071],
- [-99.16425,19.44981],
- [-99.164228,19.45069],
- [-99.164218,19.451641],
- [-99.1641,19.452905],
- [-99.164025,19.453482],
- [-99.163783,19.454347],
- [-99.163713,19.454949],
- [-99.163697,19.455141],
- [-99.163622,19.455687],
- [-99.163188,19.455702],
- [-99.162732,19.455622],
- [-99.162383,19.455571],
- [-99.162002,19.455536]*/
-];
\ No newline at end of file
+window.features = [{"geometry":{"coordinates":[-99.1801416666667,19.4297366666667],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","11.372549"],["air_intake_temp","air intake temp","62.0"],["speed","speed","34"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","80.0"],["engine_load","engine load","13.333333"],["engine_rpm","engine rpm","2674"],["intake_manifold_pressure","intake manifold pressure","18"],["engine_coolant_temp","engine coolant temp","87.0"]],"date":1461970260,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1799416666667,19.42845],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","18.82353"],["air_intake_temp","air intake temp","62.0"],["speed","speed","22"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","73.72549"],["engine_load","engine load","39.60784"],["engine_rpm","engine rpm","1737"],["intake_manifold_pressure","intake manifold pressure","49"],["engine_coolant_temp","engine coolant temp","87.0"]],"date":1461970280,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1808933333333,19.4311983333333],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","23.137255"],["air_intake_temp","air intake temp","62.0"],["speed","speed","18"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","62.35294"],["engine_load","engine load","52.54902"],["engine_rpm","engine rpm","1398"],["intake_manifold_pressure","intake manifold pressure","66"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970240,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1805466666667,19.430555],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","11.372549"],["air_intake_temp","air intake temp","62.0"],["speed","speed","33"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","76.86275"],["engine_load","engine load","16.862745"],["engine_rpm","engine rpm","2605"],["intake_manifold_pressure","intake manifold pressure","20"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970250,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1800733333334,19.42811],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","16.470589"],["air_intake_temp","air intake temp","62.0"],["speed","speed","25"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","72.15686"],["engine_load","engine load","16.078432"],["engine_rpm","engine rpm","1884"],["intake_manifold_pressure","intake manifold pressure","33"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970285,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1799116666667,19.42882],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","13.333333"],["air_intake_temp","air intake temp","62.0"],["speed","speed","23"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","73.72549"],["engine_load","engine load","29.019608"],["engine_rpm","engine rpm","1830"],["intake_manifold_pressure","intake manifold pressure","28"],["engine_coolant_temp","engine coolant temp","87.0"]],"date":1461970275,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1845679,19.4313704],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","11.372549"],["air_intake_temp","air intake temp","61.0"],["speed","speed","0"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","51.764706"],["engine_load","engine load","29.411764"],["engine_rpm","engine rpm","835"],["intake_manifold_pressure","intake manifold pressure","39"],["engine_coolant_temp","engine coolant temp","85.0"]],"date":1461970205,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1845679,19.4313704],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","18.431372"],["air_intake_temp","air intake temp","62.0"],["speed","speed","0"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","52.941177"],["engine_load","engine load","30.19608"],["engine_rpm","engine rpm","795"],["intake_manifold_pressure","intake manifold pressure","59"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970220,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1845679,19.4313704],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","16.078432"],["air_intake_temp","air intake temp","62.0"],["speed","speed","8"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","52.941177"],["engine_load","engine load","42.7451"],["engine_rpm","engine rpm","1265"],["intake_manifold_pressure","intake manifold pressure","45"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970225,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1801416666667,19.4297366666667],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","15.686275"],["air_intake_temp","air intake temp","62.0"],["speed","speed","30"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","74.117645"],["engine_load","engine load","23.529411"],["engine_rpm","engine rpm","2318"],["intake_manifold_pressure","intake manifold pressure","29"],["engine_coolant_temp","engine coolant temp","87.0"]],"date":1461970265,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1800733333334,19.42811],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","28.235294"],["air_intake_temp","air intake temp","62.0"],["speed","speed","6"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","58.431374"],["engine_load","engine load","44.705883"],["engine_rpm","engine rpm","1300"],["intake_manifold_pressure","intake manifold pressure","75"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970290,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1803383333333,19.43011],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","17.647058"],["air_intake_temp","air intake temp","62.0"],["speed","speed","35"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","70.588234"],["engine_load","engine load","32.156864"],["engine_rpm","engine rpm","2692"],["intake_manifold_pressure","intake manifold pressure","36"],["engine_coolant_temp","engine coolant temp","87.0"]],"date":1461970255,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1803683333333,19.4279933333333],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","32.156864"],["air_intake_temp","air intake temp","62.0"],["speed","speed","25"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","58.431374"],["engine_load","engine load","61.568626"],["engine_rpm","engine rpm","2062"],["intake_manifold_pressure","intake manifold pressure","70"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970295,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1845679,19.4313704],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","11.372549"],["air_intake_temp","air intake temp","61.0"],["speed","speed","0"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","52.941177"],["engine_load","engine load","30.19608"],["pids_41_60","pids 41 60","08080000"],["engine_rpm","engine rpm","835"],["intake_manifold_pressure","intake manifold pressure","39"],["engine_coolant_temp","engine coolant temp","85.0"]],"date":1461970195,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1845679,19.4313704],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","11.372549"],["air_intake_temp","air intake temp","61.0"],["speed","speed","0"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","52.941177"],["engine_load","engine load","30.588236"],["engine_rpm","engine rpm","831"],["intake_manifold_pressure","intake manifold pressure","40"],["engine_coolant_temp","engine coolant temp","85.0"]],"date":1461970200,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1845679,19.4313704],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","10.980392"],["air_intake_temp","air intake temp","61.0"],["speed","speed","0"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","53.333332"],["engine_load","engine load","30.588236"],["engine_rpm","engine rpm","829"],["intake_manifold_pressure","intake manifold pressure","40"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970215,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.18068,19.4309683333333],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","17.647058"],["air_intake_temp","air intake temp","62.0"],["speed","speed","30"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","76.07843"],["engine_load","engine load","32.941177"],["engine_rpm","engine rpm","2318"],["intake_manifold_pressure","intake manifold pressure","36"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970245,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1845679,19.4313704],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","13.333333"],["air_intake_temp","air intake temp","61.0"],["speed","speed","19"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","61.17647"],["engine_load","engine load","39.60784"],["engine_rpm","engine rpm","1529"],["intake_manifold_pressure","intake manifold pressure","33"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970230,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.180035,19.4292383333333],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","14.117647"],["air_intake_temp","air intake temp","62.0"],["speed","speed","25"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","58.039215"],["engine_load","engine load","16.078432"],["engine_rpm","engine rpm","1880"],["intake_manifold_pressure","intake manifold pressure","27"],["engine_coolant_temp","engine coolant temp","87.0"]],"date":1461970270,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1807233333333,19.4281733333333],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","11.372549"],["air_intake_temp","air intake temp","62.0"],["speed","speed","25"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","55.686275"],["engine_load","engine load","16.862745"],["engine_rpm","engine rpm","1784"],["intake_manifold_pressure","intake manifold pressure","25"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970300,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.18082,19.4283166666667],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","14.90196"],["air_intake_temp","air intake temp","62.0"],["speed","speed","10"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","72.94118"],["engine_load","engine load","28.62745"],["engine_rpm","engine rpm","1589"],["intake_manifold_pressure","intake manifold pressure","33"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970305,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.1845679,19.4313704],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","11.372549"],["air_intake_temp","air intake temp","61.0"],["speed","speed","0"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","52.54902"],["engine_load","engine load","30.19608"],["engine_rpm","engine rpm","831"],["intake_manifold_pressure","intake manifold pressure","39"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970210,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"geometry":{"coordinates":[-99.18118,19.4313066666667],"type":"Point"},"properties":{"data":[["throttle_pos","throttle pos","10.980392"],["air_intake_temp","air intake temp","62.0"],["speed","speed","23"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","75.29412"],["engine_load","engine load","19.215687"],["engine_rpm","engine rpm","1758"],["intake_manifold_pressure","intake manifold pressure","22"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970235,"idCar":"pwc_car_test","route":1},"type":"Feature"},{"type":"Feature","properties":{"data":[["throttle_pos","throttle pos","14.90196"],["air_intake_temp","air intake temp","62.0"],["speed","speed","10"],["distance_traveled_mil_on","distance traveled mil on","0"],["dtc_number","dtc number","0"],["timing_advance","timing advance","72.94118"],["engine_load","engine load","28.62745"],["engine_rpm","engine rpm","1589"],["intake_manifold_pressure","intake manifold pressure","33"],["engine_coolant_temp","engine coolant temp","86.0"]],"date":1461970305,"route":1,"idCar":"pwc_car_test"},"geometry":{"type":"Point","coordinates":[-99.18082,19.4283166666667]}}]
\ No newline at end of file
diff --git a/test/trip-test.html b/test/trip-test.html
index a858a0f..32107cb 100644
--- a/test/trip-test.html
+++ b/test/trip-test.html
@@ -22,62 +22,36 @@
- function recursividad(counter, array){
- /*global aveo_coords*/
- var data = getData(array[counter]);
+ function recursividad(counter){
- $.post( URL_ADD, { data: JSON.stringify(data) }).done(function( result ) {
- $("#result").append("
ADD: " + JSON.stringify(data) + "
");
-
- if( counter >= (array.length-1) ){
-
- $.post( URL_END, { data: JSON.stringify(data) }).done(function( result ) {
- $("#result").append("
END: " + JSON.stringify(data) + "
");
- });
+
+ if(counter === window.features.length - 1){
+ $.post( URL_END, { data: JSON.stringify(window.features[counter]) }).done(function( result ) {
+ $("#result").append("
END: " + JSON.stringify(result) + "
");
+ });
+ }else{
+ $.post( URL_ADD, { data: JSON.stringify(window.features[counter]) }).done(function( result ) {
+ $("#result").append("
ADD: " + JSON.stringify(result) + "
");
- }else{
setTimeout(function(){
- recursividad(counter+1, array);
+ recursividad(counter + 1);
}, 5000);
-
- }
-
- });
- }
-
-
- function getData(coordinates){
- var global_data = {
- type: "Feature", //Valor para todos los puntos
- geometry: {
- type: "Point", //Tipo de geometría
- coordinates: coordinates //Coordenadas geograficas
- },
- properties: {
- idCar: "pwc_test_001", //Identificador del coche
- route: 1, //Numero del viaje
- date: 1457627912, //Fecha en formato epoch, segundos transcurridos desde el año 0 (1/1/1970)
- data: [
- ["rpm", "Revoluciones por minuto", 2000], //Valores de condición del coche, se agregan todos los comandos que soporte el OBD
- ["air_temp", "Temperatura ambiente", 20], //El segundo String es el texto que se mostrara en los mapas de los clientes
- ["gas_lvl", "Nivel de gasolina",0.49]
- ]
- }
- };
+ });
+ }
+ /*global $*/
- return global_data;
}
$(function(){
- var start_data = getData([-99.166192,19.442404]);
+ var start_data = window.features[0];
$.post( URL_START, { data: JSON.stringify(start_data) }).done(function( result ) {
$("#result").append("
START: " + JSON.stringify(start_data) + "
");
setTimeout(function(){
- recursividad(0, aveo_coords);
+ recursividad(1);
}, 5000);
});
});
diff --git a/views/car.jade b/views/car.jade
new file mode 100644
index 0000000..38110a7
--- /dev/null
+++ b/views/car.jade
@@ -0,0 +1,180 @@
+doctype html
+html(lang='es')
+ head
+ meta(charset='utf-8')
+ meta(http-equiv='X-UA-Compatible', content='IE=edge')
+ meta(name='viewport', content='width=device-width, initial-scale=1')
+ title PwC - D&A [Lovelace]
+ // Bootstrap Core CSS
+ link(href='js/bootstrap/bootstrap.min.css', rel='stylesheet')
+ // Custom CSS
+ link(href='css/sb-admin.css', rel='stylesheet')
+ // Custom Fonts
+ link(href='font-awesome/css/font-awesome.min.css', rel='stylesheet', type='text/css')
+ // Leaflet
+ link(href='js/leaflet/leaflet.css', rel='stylesheet')
+ // Custom
+ link(href='css/car.css', rel='stylesheet')
+ // D3
+ link(href='js/d3/d3.parcoords.css', rel='stylesheet')
+ // HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries
+ // WARNING: Respond.js doesn't work if you view the page via file://
+ //if lt IE 9
+ script(src='https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js')
+ script(src='https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js')
+ body
+ #wrapper
+ // Navigation
+ nav#nav_navigation.navbar.navbar-inverse.navbar-fixed-top(role='navigation')
+ // Brand and toggle get grouped for better mobile display
+ .navbar-header
+ button.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-ex1-collapse')
+ span.sr-only Toggle navigation
+ span.icon-bar
+ span.icon-bar
+ span.icon-bar
+ a.navbar-brand(href='../../') Lovelace
+ // Top Menu Items
+ ul.nav.navbar-right.top-nav
+ li.dropdown
+ a.dropdown-toggle(href='#', data-toggle='dropdown')
+ i.fa.fa-bell
+ b.caret
+ ul.dropdown-menu.alert-dropdown
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-default Alert Badge
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-primary Alert Badge
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-success Alert Badge
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-info Alert Badge
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-warning Alert Badge
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-danger Alert Badge
+ li.divider
+ li
+ a(href='#') View All
+ li.dropdown
+ a.dropdown-toggle(href='#', data-toggle='dropdown')
+ i.fa.fa-user
+ | Leonel Castañeda
+ b.caret
+ ul.dropdown-menu
+ li
+ a(href='#')
+ i.fa.fa-fw.fa-user
+ | Profile
+ li
+ a(href='#')
+ i.fa.fa-fw.fa-envelope
+ | Inbox
+ li
+ a(href='#')
+ i.fa.fa-fw.fa-gear
+ | Settings
+ li.divider
+ li
+ a(href='#')
+ i.fa.fa-fw.fa-power-off
+ | Log Out
+ // Sidebar Menu Items - These collapse to the responsive navigation menu on small screens
+ .collapse.navbar-collapse.navbar-ex1-collapse
+ ul.nav.navbar-nav.side-nav
+ li
+ a(href='../../')
+ i.fa.fa-fw.fa-dashboard
+ | Dashboard
+ li
+ a(href='../map')
+ i.fa.fa-fw.fa-globe
+ | Mapa
+ li.active
+ a(href='')
+ i.fa.fa-fw.fa-car
+ | Automóviles
+ // /.navbar-collapse
+ #page-wrapper
+ .container-fluid
+ // Page Heading
+ .row
+ .col-lg-12
+ h2
+ | #{idCar}
+ hr
+ ol.breadcrumb
+ li
+ i.fa.fa-car
+ a(href='./') Automóviles
+ li.active
+ i.fa.fa-bar-chart-o
+ | #{idCar}
+ .row
+ #mapCar.content
+ #graphContainer.content.parcoords(style="width:100%;height:200px")
+ // /.container-fluid
+ // /#page-wrapper
+ // /#wrapper
+ // Utils
+ // D3
+ script(src='https://d3js.org/d3.v3.min.js', charset="utf-8")
+ script(src='js/d3/d3.parcoords.js')
+ // jQuery
+ script(src='https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js')
+ // Bootstrap Core JavaScript
+ script(src='js/bootstrap/bootstrap.min.js')
+ // Leaflet
+ script(src='js/leaflet/leaflet.js')
+ script(src='js/mapCar.js')
+ // Notifications
+ script(src='js/plugins/notify.min.js')
+ script(src='js/utils.js')
+ script(src='js/graphsCar.js')
+ // Socket
+ script(src='https://cdn.socket.io/socket.io-1.3.7.js')
+ script.
+ $(function(){
+ window.startMap("mapCar","300px");
+
+ window.socket = io();
+ socket.emit("carFeatures", "#{idCar}");
+ socket.on("carFeatures", function(data){
+ var properties = [];
+
+ for(var i = 0; i < data.length; i++){
+ if(data[i].features){
+ window.addLayer(data[i].features);
+
+ for(var j = 0; j < data[i].features.length; j++){
+ var featureProperties = arrayToJson(data[i].features[j].properties.data);
+ properties.push(featureProperties);
+ }
+ }
+ }
+
+ window.parallelBar(properties);
+ });
+ });
+
+ function arrayToJson(data){
+ var aux = {};
+ for(var i = 0; i < data.length; i++){
+ aux[data[i][0]] = data[i][2];
+ }
+
+ return aux;
+ }
+
diff --git a/views/carsGlobal.jade b/views/carsGlobal.jade
new file mode 100644
index 0000000..465f799
--- /dev/null
+++ b/views/carsGlobal.jade
@@ -0,0 +1,132 @@
+doctype html
+html(lang='es')
+ head
+ meta(charset='utf-8')
+ meta(http-equiv='X-UA-Compatible', content='IE=edge')
+ meta(name='viewport', content='width=device-width, initial-scale=1')
+ title PwC - D&A [Lovelace]
+ // Bootstrap Core CSS
+ link(href='js/bootstrap/bootstrap.min.css', rel='stylesheet')
+ // Custom CSS
+ link(href='css/sb-admin.css', rel='stylesheet')
+ // Custom Fonts
+ link(href='font-awesome/css/font-awesome.min.css', rel='stylesheet', type='text/css')
+ // HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries
+ // WARNING: Respond.js doesn't work if you view the page via file://
+ //if lt IE 9
+ script(src='https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js')
+ script(src='https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js')
+ style(type='text/css').
+ #map_realtime{
+ width:100%;
+ height: 600px;
+ }
+ body
+ #wrapper
+ // Navigation
+ nav#nav_navigation.navbar.navbar-inverse.navbar-fixed-top(role='navigation')
+ // Brand and toggle get grouped for better mobile display
+ .navbar-header
+ button.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-ex1-collapse')
+ span.sr-only Toggle navigation
+ span.icon-bar
+ span.icon-bar
+ span.icon-bar
+ a.navbar-brand(href='../../') Lovelace
+ // Top Menu Items
+ ul.nav.navbar-right.top-nav
+ li.dropdown
+ a.dropdown-toggle(href='#', data-toggle='dropdown')
+ i.fa.fa-bell
+ b.caret
+ ul.dropdown-menu.alert-dropdown
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-default Alert Badge
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-primary Alert Badge
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-success Alert Badge
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-info Alert Badge
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-warning Alert Badge
+ li
+ a(href='#')
+ | Alert Name
+ span.label.label-danger Alert Badge
+ li.divider
+ li
+ a(href='#') View All
+ li.dropdown
+ a.dropdown-toggle(href='#', data-toggle='dropdown')
+ i.fa.fa-user
+ | Leonel Castañeda
+ b.caret
+ ul.dropdown-menu
+ li
+ a(href='#')
+ i.fa.fa-fw.fa-user
+ | Profile
+ li
+ a(href='#')
+ i.fa.fa-fw.fa-envelope
+ | Inbox
+ li
+ a(href='#')
+ i.fa.fa-fw.fa-gear
+ | Settings
+ li.divider
+ li
+ a(href='#')
+ i.fa.fa-fw.fa-power-off
+ | Log Out
+ // Sidebar Menu Items - These collapse to the responsive navigation menu on small screens
+ .collapse.navbar-collapse.navbar-ex1-collapse
+ ul.nav.navbar-nav.side-nav
+ li
+ a(href='../../')
+ i.fa.fa-fw.fa-dashboard
+ | Dashboard
+ li
+ a(href='../map')
+ i.fa.fa-fw.fa-globe
+ | Mapa
+ li.active
+ a(href='')
+ i.fa.fa-fw.fa-car
+ | Automóviles
+ // /.navbar-collapse
+ #page-wrapper
+ .container-fluid
+ // Page Heading
+ .row
+ .col-lg-12
+ h2
+ | Automóviles.
+ hr
+ // /.row
+ #panelContainer.content
+ // /.container-fluid
+ // /#page-wrapper
+ // /#wrapper
+ // Utils
+ // jQuery
+ script(src='https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js')
+ // Bootstrap Core JavaScript
+ script(src='js/bootstrap/bootstrap.min.js')
+ // Notifications
+ script(src='js/plugins/notify.min.js')
+ // Socket
+ script(src='https://cdn.socket.io/socket.io-1.3.7.js')
+ script(src='js/socketTrips.js')
+ script(src='js/trips.js')
diff --git a/views/index.jade b/views/index.jade
index 6831a15..2c635a6 100644
--- a/views/index.jade
+++ b/views/index.jade
@@ -97,12 +97,12 @@ html(lang='en')
| Dashboard
li
a(href='./map')
- i.fa.fa-fw.fa-bar-chart-o
+ i.fa.fa-fw.fa-globe
| Mapa
li
- a(href='./trips')
- i.fa.fa-fw.fa-table
- | Viajes
+ a(href='./cars')
+ i.fa.fa-fw.fa-car
+ | Automóviles
// /.navbar-collapse
#page-wrapper
.container-fluid
diff --git a/views/map.jade b/views/map.jade
index af963d8..d099134 100644
--- a/views/map.jade
+++ b/views/map.jade
@@ -18,11 +18,6 @@ html(lang='es')
//if lt IE 9
script(src='https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js')
script(src='https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js')
- style(type='text/css').
- #map_realtime{
- width:100%;
- height: 600px;
- }
body
#wrapper
// Navigation
@@ -101,12 +96,12 @@ html(lang='es')
| Dashboard
li.active
a(href='')
- i.fa.fa-fw.fa-bar-chart-o
+ i.fa.fa-fw.fa-globe
| Mapa
li
- a(href='../trips')
- i.fa.fa-fw.fa-table
- | Viajes
+ a(href='../cars')
+ i.fa.fa-fw.fa-car
+ | Automóviles
// /.navbar-collapse
#page-wrapper
.container-fluid
@@ -133,6 +128,10 @@ html(lang='es')
script(src='js/map.js')
// Real-time
script(src='https://cdn.socket.io/socket.io-1.3.7.js')
- script(src='js/socket.js')
+ script(src='js/socketMap.js')
// Notifications
script(src='js/plugins/notify.min.js')
+ script.
+ $(function(){
+ window.startMap("map_realtime","600px");
+ });