diff --git a/app/js/controllers/controller-outfit.js b/app/js/controllers/controller-outfit.js index ad4ea372..91445217 100755 --- a/app/js/controllers/controller-outfit.js +++ b/app/js/controllers/controller-outfit.js @@ -1,4 +1,4 @@ -angular.module('app').controller('OutfitController', ['$window', '$rootScope', '$scope', '$state', '$stateParams', 'ShipsDB', 'Ship', 'Components', 'Serializer', 'Persist', 'calcTotalRange', function($window, $rootScope, $scope, $state, $p, Ships, Ship, Components, Serializer, Persist, calcTotalRange) { +angular.module('app').controller('OutfitController', ['$window', '$rootScope', '$scope', '$state', '$stateParams', 'ShipsDB', 'Ship', 'Components', 'Serializer', 'Persist', 'calcTotalRange', 'calcSpeed', function($window, $rootScope, $scope, $state, $p, Ships, Ship, Components, Serializer, Persist, calcTotalRange, calcSpeed) { var data = Ships[$p.shipId]; // Retrieve the basic ship properties, slots and defaults var ship = new Ship($p.shipId, data.properties, data.slots); // Create a new Ship instance var win = angular.element($window); // Angularized window object for event triggering @@ -55,8 +55,7 @@ angular.module('app').controller('OutfitController', ['$window', '$rootScope', ' title: 'Jump Range', unit: 'LY' } - }, - watch: $scope.fsd + } }; $scope.trSeries = { @@ -78,8 +77,30 @@ angular.module('app').controller('OutfitController', ['$window', '$rootScope', ' title: 'Total Range', unit: 'LY' } - }, - watch: $scope.fsd + } + }; + + $scope.speedSeries = { + xMin: 0, + xMax: ship.cargoCapacity, + yMax: 500, + yMin: 0, + series: ['speed', 'boost'], + func: function(cargo) { // X Axis is Cargo + return calcSpeed(ship.unladenMass + $scope.fuel + cargo, ship.speed, ship.boost, $scope.th.c); + } + }; + $scope.speedChart = { + labels: { + xAxis: { + title: 'Cargo', + unit: 'T' + }, + yAxis: { + title: 'Speed', + unit: 'm/s' + } + } }; /** @@ -122,8 +143,7 @@ angular.module('app').controller('OutfitController', ['$window', '$rootScope', ' ship.useBulkhead(id); } $scope.selectedSlot = null; - $scope.code = Serializer.fromShip(ship); - updateState(); + updateState(Serializer.fromShip(ship)); } }; @@ -133,8 +153,7 @@ angular.module('app').controller('OutfitController', ['$window', '$rootScope', ' $scope.reloadBuild = function() { if ($scope.buildName && $scope.savedCode) { Serializer.toShip(ship, $scope.savedCode); // Repopulate with components from last save - $scope.code = $scope.savedCode; - updateState(); + updateState($scope.savedCode); } }; @@ -149,8 +168,7 @@ angular.module('app').controller('OutfitController', ['$window', '$rootScope', ' ship.hardpoints.forEach(function(slot) { ship.use(slot, null, null); }); ship.internal.forEach(function(slot) { ship.use(slot, null, null); }); ship.useBulkhead(0); - $scope.code = Serializer.fromShip(ship); - updateState(); + updateState(Serializer.fromShip(ship)); }; /** @@ -169,7 +187,7 @@ angular.module('app').controller('OutfitController', ['$window', '$rootScope', ' if ($scope.code != $scope.savedCode) { Persist.saveBuild(ship.id, $scope.buildName, $scope.code); $scope.savedCode = $scope.code; - updateState(); + updateState($scope.code); } }; @@ -218,21 +236,18 @@ angular.module('app').controller('OutfitController', ['$window', '$rootScope', ' */ $scope.togglePwr = function(c) { ship.setSlotEnabled(c, !c.enabled); - $scope.code = Serializer.fromShip(ship); - updateState(); + updateState(Serializer.fromShip(ship)); }; $scope.incPriority = function(c) { if (ship.changePriority(c, c.priority + 1)) { - $scope.code = Serializer.fromShip(ship); - updateState(); + updateState(Serializer.fromShip(ship)); } }; $scope.decPriority = function(c) { if (ship.changePriority(c, c.priority - 1)) { - $scope.code = Serializer.fromShip(ship); - updateState(); + updateState(Serializer.fromShip(ship)); } }; @@ -251,9 +266,10 @@ angular.module('app').controller('OutfitController', ['$window', '$rootScope', ' // Utilify functions - function updateState() { + function updateState(code) { + $scope.code = code; $state.go('outfit', { shipId: ship.id, code: $scope.code, bn: $scope.buildName }, { location: 'replace', notify: false }); - $scope.trSeries.xMax = $scope.jrSeries.xMax = ship.cargoCapacity; + $scope.speedSeries.xMax = $scope.trSeries.xMax = $scope.jrSeries.xMax = ship.cargoCapacity; $scope.jrSeries.yMax = ship.unladenRange; $scope.trSeries.yMax = ship.unladenTotalRange; win.triggerHandler('pwrchange'); diff --git a/app/js/directives/directive-line-chart.js b/app/js/directives/directive-line-chart.js new file mode 100644 index 00000000..9b5000b9 --- /dev/null +++ b/app/js/directives/directive-line-chart.js @@ -0,0 +1,190 @@ +angular.module('app').directive('lineChart', ['$window', function($window) { + return { + restrict: 'A', + scope: { + config: '=', + series: '=' + }, + link: function(scope, element) { + var seriesConfig = scope.series, + series = seriesConfig.series, + color = d3.scale.ordinal().range([ '#ff8c0d', '#1fb0ff', '#a05d56', '#d0743c']), + config = scope.config, + labels = config.labels, + margin = { top: 15, right: 15, bottom: 35, left: 60 }, + fmt = d3.format('.3r'), + fmtLong = d3.format('.2f'), + func = seriesConfig.func, + drag = d3.behavior.drag(), + dragging = false, + // Define Scales + x = d3.scale.linear(), + y = d3.scale.linear(), + // Define Axes + xAxis = d3.svg.axis().scale(x).outerTickSize(0).orient('bottom').tickFormat(d3.format('.2r')), + yAxis = d3.svg.axis().scale(y).ticks(6).outerTickSize(0).orient('left').tickFormat(fmt), + data = []; + + // Create chart + var svg = d3.select(element[0]).append('svg'); + var vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + var lines = vis.append('g'); + + // Define Area + var line = d3.svg.line().y(function(d) { return y(d[1]); }); + + // Create Y Axis SVG Elements + var yTxt = vis.append('g').attr('class', 'y axis') + .append('text') + .attr('transform', 'rotate(-90)') + .attr('y', -50) + .attr('dy', '.1em') + .style('text-anchor', 'middle') + .text(labels.yAxis.title + ' (' + labels.yAxis.unit + ')'); + // Create X Axis SVG Elements + var xLbl = vis.append('g').attr('class', 'x axis'); + var xTxt = xLbl.append('text') + .attr('y', 30) + .attr('dy', '.1em') + .style('text-anchor', 'middle') + .text(labels.xAxis.title + ' (' + labels.xAxis.unit + ')'); + + // Create and Add tooltip + var tipWidth = (Math.max(labels.yAxis.unit.length, labels.xAxis.unit.length) * 1.25) + 2; + var tips = vis.append('g').style('display', 'none'); + + var background = vis.append('rect') // Background to capture hover/drag + .attr('fill-opacity', 0) + .on('mouseover', showTip) + .on('mouseout', hideTip) + .on('mousemove', moveTip) + .call(drag); + + drag + .on('dragstart', function() { + dragging = true; + moveTip.call(this); + showTip(); + }) + .on('dragend', function() { + dragging = false; + hideTip(); + }) + .on('drag', moveTip); + + /** + * Watch for changes in the series data (mass changes, etc) + */ + scope.$watchCollection('series', render); + angular.element($window).bind('orientationchange resize render', render); + + function render() { + var width = element[0].parentElement.offsetWidth, + height = width * 0.5, + xMax = seriesConfig.xMax, + xMin = seriesConfig.xMin, + yMax = seriesConfig.yMax, + yMin = seriesConfig.yMin, + w = width - margin.left - margin.right, + h = height - margin.top - margin.bottom, + s, val, yVal, delta; + + data.length = 0; // Reset Data array + + if (seriesConfig.xMax == seriesConfig.xMin) { + line.x(function(d, i) { return i * w; }); + } else { + line.x(function(d) { return x(d[0]); }); + } + + if (series) { + for (s = 0; s < series.length; s++) { + data.push([]); + } + + if (xMax == xMin) { + yVal = func(xMin); + for (s = 0; s < series.length; s++) { + data[s].push( [ xMin, yVal[ series[s] ] ], [ 1, yVal[ series[s] ] ]); + } + } else { + delta = (xMax - xMin) / 30; // Only render 30 points on the graph + for (val = xMin; val <= xMax; val += delta) { + yVal = func(val); + for (s = 0; s < series.length; s++) { + data[s].push([ val, yVal[ series[s] ] ]); + } + } + } + + } else { + var seriesData = []; + if (xMax == xMin) { + yVal = func(xMin); + seriesData.push([ xMin, yVal ], [ 1, yVal ]); + } else { + delta = (xMax - xMin) / 30; // Only render 30 points on the graph + for (val = xMin; val <= xMax; val += delta) { + seriesData.push([val, func(val) ]); + } + } + + data.push(seriesData); + } + + // Update Chart Size + svg.attr('width', width).attr('height', height); + background.attr('height', h).attr('width', w); + + // Update domain and scale for axes + x.range([0, w]).domain([xMin, xMax]).clamp(true); + xLbl.attr('transform', 'translate(0,' + h + ')'); + xTxt.attr('x', w / 2); + y.range([h, 0]).domain([yMin, yMax]); + yTxt.attr('x', -h / 2); + vis.selectAll('.y.axis').call(yAxis); + vis.selectAll('.x.axis').call(xAxis); + + lines.selectAll('path.line') + .data(data) + .attr('d', line) // Update existing series + .enter() // Add new series + .append('path') + .attr('class', 'line') + .attr('stroke', function(d, i) { return color(i); }) + .attr('stroke-width', 2) + .attr('d', line); + + var tip = tips.selectAll('g.tooltip').data(data).enter().append('g').attr('class', 'tooltip'); + tip.append('rect').attr('width', tipWidth + 'em').attr('height', '2em').attr('x', '0.5em').attr('y', '-1em').attr('class', 'tip'); + tip.append('circle').attr('class', 'marker').attr('r', 4); + tip.append('text').attr('class', 'label x').attr('y', '-0.25em'); + tip.append('text').attr('class', 'label y').attr('y', '0.85em'); + } + + function showTip() { + tips.style('display', null); + } + + function hideTip() { + if (!dragging) { + tips.style('display', 'none'); + } + } + + function moveTip() { + var xPos = d3.mouse(this)[0], x0 = x.invert(xPos), y0 = func(x0), flip = (x0 / x.domain()[1] > 0.65); + var tip = tips.selectAll('g.tooltip').attr('transform', function(d, i) { return 'translate(' + x(x0) + ',' + y(series ? y0[series[i]] : y0) + ')'; }); + tip.selectAll('rect').attr('x', flip ? (-tipWidth - 0.5) + 'em' : '0.5em').style('text-anchor', flip ? 'end' : 'start'); + tip.selectAll('text.label').attr('x', flip ? '-1em' : '1em').style('text-anchor', flip ? 'end' : 'start'); + tip.selectAll('text.label.x').text(fmtLong(x0) + ' ' + labels.xAxis.unit); + tips.selectAll('text.label.y').text(function(d, i) { return fmtLong(series ? y0[series[i]] : y0) + ' ' + labels.yAxis.unit; }); + } + + scope.$on('$destroy', function() { + angular.element($window).unbind('orientationchange resize render', render); + }); + + } + }; +}]); diff --git a/app/js/shipyard/module-shipyard.js b/app/js/shipyard/module-shipyard.js index 618118a8..8c98dad4 100755 --- a/app/js/shipyard/module-shipyard.js +++ b/app/js/shipyard/module-shipyard.js @@ -208,9 +208,6 @@ angular.module('shipyard', ['ngLodash']) * @return {number} Approximate shield strengh in MJ */ .value('calcShieldStrength', function(mass, shields, sg, multiplier) { - if (!sg) { - return 0; - } if (mass <= sg.minmass) { return shields * multiplier * sg.minmul; } @@ -221,4 +218,20 @@ angular.module('shipyard', ['ngLodash']) return shields * multiplier * (sg.optmul + (mass - sg.optmass) / (sg.maxmass - sg.optmass) * (sg.maxmul - sg.optmul)); } return shields * multiplier * sg.maxmul; + }) + /** + * Calculate the a ships speed based on mass, and thrusters. Currently Innacurate / Incomplete :( + * + * @private + * @param {number} mass Current mass of the ship + * @param {number} baseSpeed Base speed m/s for ship + * @param {number} baseBoost Base boost m/s for ship + * @param {object} thrusters The shield generator used + * @return {object} Approximate speed and boost speed in m/s + */ + .value('calcSpeed', function(mass, baseSpeed, baseBoost) { //, thrusters) { + //var speed = baseSpeed * (1 + ((thrusters.optmass / mass) * 0.1 ) ); // TODO: find thruser coefficient(s) + //var boost = baseBoost * (1 + ((thrusters.optmass / mass) * 0.1 ) ); + + return { boost: baseSpeed, speed: baseBoost }; }); diff --git a/app/less/outfit.less b/app/less/outfit.less index 570d81a9..faad7a9e 100755 --- a/app/less/outfit.less +++ b/app/less/outfit.less @@ -167,7 +167,7 @@ table.total { } &.semi { - width: 50%; + width: 33%; .smallTablet({ .axis { @@ -179,7 +179,7 @@ table.total { } }); - .medPhone({ + .largePhone({ width: 100% !important; }); } diff --git a/app/views/page-outfit.html b/app/views/page-outfit.html index 67c1e44f..63030e12 100644 --- a/app/views/page-outfit.html +++ b/app/views/page-outfit.html @@ -266,12 +266,17 @@