// TODO - most code copied from map-telemetry, need to move them to a separate module // Array that stores objects for each element returned via rest var restEndpoint = config.mapExportsBySection, restEndpointAsync = config.mapExportsBySectionAsync, restEndpoint2 = config.mapExportsSectionAge, restEndpoint2Async = config.mapExportsSectionAgeAsync, sectionsVectorData, // to store the section vector data exportsStats, // to store the map export raw returned data sectionExportAge; var vBoxWidth = 1200, vBoxHeight = 900, bgWidth = 900, bgHeight = 1500, clicked; //var defaultFilterText = "type text to filter the sections", defaultFilterText = "", sectionsMapId = "sections-overlay", propsSuffix = "_PROPS"; var sectionTypes = [{name: "Main Sections", value: "main"}, {name: "Props Sections", value: "props"}], statTypes = ["Average", "Min", "Max"]; var optionCalculations = { // create an associative array - use as hashtable "OverallTime": { hasTypes: true, calcValue: function(d, statType) { var value; if (statType == statTypes[1]) { //value = d.MinTotalTime; value = 0; } else if (statType == statTypes[2]) { value = d.MaxTotalTime; } else { value = d.AvgTotalTime; } return (value) ? value : 0; }, units: " secs", reversedColours: true, }, "ExportTime": { hasTypes: true, calcValue: function(d, statType) { var value; if (statType == statTypes[1]) { //value = d.MinSectionExportTime; value = 0; } else if (statType == statTypes[2]) { value = d.MaxSectionExportTime; } else { value = d.AvgSectionExportTime; } return (value) ? value : 0; }, units: " secs", reversedColours: true, }, "ExportCount": { hasTypes: false, calcValue: function(d, s) { return d.SectionExportCount; }, units: " times exported", reversedColours: true, }, "ExportSuccessRatio": { hasTypes: false, calcValue: function(d, s) { return (d.SectionExportCount) ? (d.SectionExportSuccessCount / d.SectionExportCount) : 0; }, units: " success ratio", reversedColours: false }, "BuildTime": { hasTypes: true, calcValue: function(d, statType) { var value; if (statType == statTypes[1]) { //value = d.MinBuildTime; value = 0; } else if (statType == statTypes[2]) { value = d.MaxBuildTime; } else { value = d.AvgBuildTime; } return (value) ? value : 0; }, units: " secs", reversedColours: true, }, "BuildCount": { hasTypes: false, calcValue: function(d, s) { return d.BuildCount; }, units: " times built", reversedColours: true, }, "BuildSuccessRatio": { hasTypes: false, calcValue: function(d, s) { return (d.BuildCount) ? (d.BuildSuccessCount / d.BuildCount) : 0; }, units: " success ratio", reversedColours: false, }, "LastExport": { hasTypes: false, calcValue: function(d, s) { return datesDiffInSecs(new Date(), parseJsonDate(d.MostRecentExport)); }, units: " secs", reversedColours: true, isSectionAgeStat: true, }, }; //set true for disabled filter var headerAndFilters = { headerType: "header-map-exports", // social club filtering header disabledFields: [ // disabled header fields false, // section false, // user false, // dates ], }; function convert2WebXCoord(x) { return ((x + project.map.coords.x)/project.map.scale); } function convert2WebYCoord(y) { return (((-1)*y + project.map.coords.y)/project.map.scale); } function convert2WebCoords(p) { return [convert2WebXCoord(p[0]), convert2WebYCoord(p[1])]; } function updateMapHeight() { // Adjust the height of the map automatically var windowHeight = $("#content").height() - 30; $("#map-wrapper").css("height", windowHeight); } function initPage() { // function from generic.js, variable from config file initHeaderAndFilters(headerAndFilters); if (!$("#section").val()) $("#section").val(defaultFilterText); $("#section").focus(function() { if($(this).val() == defaultFilterText) $(this).val(""); }); $("#section").blur(function() { if($(this).val() == "") $(this).val(defaultFilterText); }); /* $("#section").change(function() { if (exportsStats) generateOverlays(); }); */ $("#section").keyup(function() { if (exportsStats) generateOverlays(); }); /* Generate the html here */ /* End of html generation */ //block(); $.ajax({ url: project.map.svgFile, type: "GET", data: {}, dataType: "xml", async: false, success: function(xml, textStatus, jqXHR) { var importedNode; try { importedNode = document.importNode(xml.documentElement, true); } catch(e) { // IE case importedNode = ieImportNode(xml.documentElement, document); } $("#map").append(importedNode); }, error: function (xhr, ajaxOptions, thrownError){ console.error(this.url + "\n" + ajaxOptions + " " + xhr.status + " " + thrownError ); }, complete: function() { var svg = d3.select("#map svg") .attr("width", $("#map").width()) .attr("height", $("#map").height()) .attr("enable-background", "new 0 0 " + bgWidth + " "+ bgHeight) .call(svg_interact); drawGrid(); svg.append("svg:g") .attr("id", sectionsMapId); } }); $.ajax({ url: project.map.vectorsFile, type: "GET", data: {}, dataType: "xml", async: false, success: function(xml, textStatus, jqXHR) { sectionsVectorData = parseSectionsVectorXml(xml); }, error: function (xhr, ajaxOptions, thrownError){ console.error(this.url + "\n" + ajaxOptions + " " + xhr.status + " " + thrownError ); }, complete: function() {} }); //unBlock(); // Activate the map layer show/hide checkboxes $("#map-layers input[type=checkbox]").click(function() { var layer = d3.select("#map svg").select("g#" + $(this).val()); var currentLayerId = $(this).val(); if ($(this).is(":checked")) { layer .transition() .style("opacity", 1); } else { layer .transition() .style("opacity", 0.01); } }); $("#overlay-options input[name=section-type-radio]:radio").change(function() { generateOverlays(); }); $("#overlay-options input[name=calc-type-radio]:radio").change(function() { generateOverlays(); }); $("#overlay-options input[name=stat-type-radio]:radio").change(function() { generateOverlays(); }); /* Initialise the heatmap gradient and sliders*/ var defaultMinHue = 0, defaultMaxHue = 240, defaultMinHueSlider = 0, defaultMaxHueSlider = 120; $("#overlay-options div.heatmap-slider-placeholder").slider({ range: true, min: defaultMinHue, max: defaultMaxHue, values: [defaultMinHueSlider, defaultMaxHueSlider], stop: function(event, ui ) { // fire when mouse is released //generateHeatmapGradient("overlay-options span.heatmap-gradient", // ui.values[0], ui.values[1]); generateOverlays(); } }); $("#overlay-options div.value-slider").slider({ range: true, min: 0, max: 0, values: [0, 0], stop: function(event, ui ) { // fire when mouse is released generateOverlays() } }); $("#overlay-options div.threshold-slider").slider({ min: 0, max: 0, value: 0, stop: function(event, ui ) { // fire when mouse is released //$(this).next().text(ui.value.toFixed(2)); generateOverlays(); } }); generateHeatmapGradient("overlay-options span.heatmap-slider", defaultMinHue, defaultMaxHue); //generateHeatmapGradient("overlay-options span.heatmap-gradient", // defaultMinHueSlider, defaultMaxHueSlider); /* End of heatmap init */ //updateMapHeight(); $(window).resize(updateMapHeight); $("#filter").click(function() { fetchMapExportStats(); }); fetchMapExportStats(); } function drawGrid() { var gridStep = 500/project.map.scale; // The step of the grid in map coordinates var grid = d3.select("#map svg") .append("svg:g") .attr("id", "grid"); var xAxisDataNeg = d3.range(convert2WebXCoord(0), 0, -gridStep); var xAxisDataPos = d3.range(convert2WebXCoord(0), bgWidth, gridStep); var yAxisDataNeg = d3.range(convert2WebYCoord(0), 0, -gridStep); var yAxisDataPos = d3.range(convert2WebYCoord(0), bgHeight, gridStep); grid.selectAll("line.vertical") .data(xAxisDataNeg.concat(xAxisDataPos)) .enter() .append("svg:line") .attr("x1", function(d) {return d;}) .attr("y1", 0) .attr("x2", function(d) {return d;}) .attr("y2", bgHeight) .attr("class", "map-grid"); grid.selectAll("line.horizontal") .data(yAxisDataNeg.concat(yAxisDataPos)) .enter() .append("svg:line") .attr("x1", 0) .attr("y1", function(d) {return d;}) .attr("x2", bgWidth) .attr("y2", function(d) {return d;}) .attr("class", "map-grid"); } function fetchMapExportStats() { var pValues = config.headerOptions[headerAndFilters.headerType].getParamValues(); var endpointObject = { restUrl: config.restHost + restEndpoint, restAsyncUrl: config.restHost + restEndpointAsync + pValues.ForceUrlSuffix, pValues: [pValues], } // This is for querying the time of last export of all the sections var endpointObject2 = { restUrl: config.restHost + restEndpoint2, restAsyncUrl: config.restHost + restEndpoint2Async + pValues.ForceUrlSuffix, pValues: [pValues], } var req = new ReportRequest(null, "json", null, config.restHost + config.reportsQueryAsync); req.sendMultipleAsyncRequest([endpointObject, endpointObject2], handleMultipleResponse); } function handleMultipleResponse(monitoredProcesses) { exportsStats = monitoredProcesses[0][0].response; sectionExportAge = monitoredProcesses[1][0].response; generateOverlays(); } function generateOverlays() { removeOverlay(); // remove overlay if exists $("#stat-type").css("display", "none"); var calcType = $("#overlay-options input[name=calc-type-radio]:radio:checked").val(); var calcObj = optionCalculations[calcType]; var statType = $("#overlay-options input[name=stat-type-radio]:radio:checked").val(); var sectionType = $("#overlay-options input[name=section-type-radio]:radio:checked").val(); if (calcObj.hasTypes && ($("#stat-type").css("display") == "none")) $("#stat-type").css("display", "inherit"); var overlayStatsData; if (calcObj.isSectionAgeStat) overlayStatsData = sectionExportAge; else overlayStatsData = exportsStats; var filteredExports = []; var propsSelected = (sectionType == sectionTypes[1].value) ? true : false; var filterText = $("#section").val(); if (filterText == defaultFilterText) filterText = ""; $.each(overlayStatsData, function(i, mapExportStat) { // if the section passes the filter then continue further processing if ((new RegExp("^" + filterText, "i")).test(mapExportStat.SectionName)) { // check if the names matches the props suffix if ((new RegExp(propsSuffix + "$", "i")).test(mapExportStat.SectionName)) { if (propsSelected) { // add if it's props and pros is selected filteredExports.push(mapExportStat); } } else { // add if it's not props and pros is not selected if (!propsSelected) { filteredExports.push(mapExportStat); } } // end of props check } // end of sections filtering check }); //console.log(filteredExports); var min = d3.min(filteredExports, function(d) {return calcObj.calcValue(d, statType)}); min = (min) ? min : 0; //var avg = d3.mean(exportsStats, function(d) {return calcObj.calcValue(d, statType)}); var max = d3.max(filteredExports, function(d) {return calcObj.calcValue(d, statType)}); max = (max) ? max : 0; var med = ((min+max)/2); //console.log(filteredExports); var thresholdSlider = $("#overlay-options div.threshold-slider"); thresholdSlider.slider("option", "min", min); thresholdSlider.slider("option", "max", max); thresholdSlider.slider("refresh"); $("#overlay-options span.threshold-value").text(thresholdSlider.slider("value").toFixed(2)); var valuesSlider = $("#overlay-options div.value-slider"); valuesSlider.slider("option", "min", min); valuesSlider.slider("option", "max", max); $("#overlay-options span.value-min").text(min.toFixed(2)); $("#overlay-options span.value-max").text(max.toFixed(2)); if (((valuesSlider.slider("values")[0] == min) && (valuesSlider.slider("values")[1] == min)) || !calcObj.reversedColours) { //console.log("Values init"); valuesSlider.slider("values", 0, min); valuesSlider.slider("values", 1, max); } valuesSlider.slider("refresh"); var rangeSliderValues = valuesSlider.slider("values"); var heatmapSliderValues = $("#overlay-options div.heatmap-slider-placeholder").slider("values"); var colourCalc; if (calcObj.reversedColours) { colourCalc = getColourInterpolator(rangeSliderValues[0], rangeSliderValues[1], heatmapSliderValues[1], heatmapSliderValues[0]); generateHeatmapGradient("overlay-options span.heatmap-gradient", heatmapSliderValues[1], heatmapSliderValues[0]); //$(".heatmap-min").text(max.toFixed(2)); //$(".heatmap-med").text(med.toFixed(2)); //$(".heatmap-max").text(min.toFixed(2)); } else { colourCalc = getColourInterpolator(rangeSliderValues[0], rangeSliderValues[1], heatmapSliderValues[0], heatmapSliderValues[1]); generateHeatmapGradient("overlay-options span.heatmap-gradient", heatmapSliderValues[0], heatmapSliderValues[1]); //$(".heatmap-min").text(min.toFixed(2)); //$(".heatmap-med").text(med.toFixed(2)); //$(".heatmap-max").text(max.toFixed(2)); } // Update the heatmap gradient text $("#overlay-options span.heatmap-min").text(rangeSliderValues[0].toFixed(2)); $("#overlay-options span.heatmap-med").text( ((rangeSliderValues[0] + rangeSliderValues[1]) / 2).toFixed(2) ); $("#overlay-options span.heatmap-max").text(rangeSliderValues[1].toFixed(2)) $(".heatmap-units").text(" (" + calcObj.units + " )") var line = d3.svg.line() .x(function(point) {return convert2WebXCoord(point.x);}) .y(function(point) {return convert2WebYCoord(point.y);}) .interpolate("basic"); var sectionOverlays = d3.select("#map svg") .append("svg:g") .attr("id", sectionsMapId) .attr("opacity", 0.01); sectionOverlays .selectAll("path") .data(filteredExports) .enter() .append("svg:path") .attr("id", function(d) {return d.SectionName}) .attr("d", function(d) { //console.log(sectionsVectorData); var sectionData = sectionsVectorData[d.SectionName.toUpperCase().replace(propsSuffix, "")]; if (sectionData) return (line(sectionData.points) + "z"); }) .style("fill", function(d) {return colourCalc(calcObj.calcValue(d, statType));}) .attr("visibility", function(d) { // Hide if below the threshold limit return (calcObj.calcValue(d, statType) > thresholdSlider.slider("value")) ? "visible" : "hidden"; }) .on("click", highlightClicked) .append("title") .text(function(d) { //console.log(d); var title; var sectionValue = calcObj.calcValue(d, statType); if (calcObj.units == optionCalculations["OverallTime"].units) // == " secs", don't want to hardcode the string title = d.SectionName + " : " + formatSecsWithDays(sectionValue) + " (" + sectionValue.toFixed(2) + calcObj.units + ") "; else title = d.SectionName + " : " + sectionValue.toFixed(2) + calcObj.units; return title; }); sectionOverlays .transition() .attr("opacity", 1); } function highlightClicked(data) { if (data && (clicked !== data)) { clicked = data; } else { clicked = null; } d3.select("#map svg") .select("#" + sectionsMapId) .selectAll("path") .classed("active", clicked && function(d) {return d === clicked; }); } function removeOverlay() { d3.select("#map svg") .select("g#" + sectionsMapId) .transition() .style("opacity", 0.01) .remove(); } function generateHeatmapGradient(elementID, minHue, maxHue) { var canvas = $("#" + elementID + " canvas")[0]; var context = canvas.getContext("2d"); context.rect(0, 0, canvas.width, canvas.height); // add linear gradient var grd = context.createLinearGradient(0, 0, canvas.width, 0); var s = 0.9, v = 0.9; var hueStep = (maxHue - minHue) / 5; grd.addColorStop(0, hsv2hex(minHue, s, v)); grd.addColorStop(0.167, hsv2hex(minHue+hueStep, s, v)); grd.addColorStop(0.333, hsv2hex(minHue+2*hueStep, s, v)); grd.addColorStop(0.5, hsv2hex(minHue+3*hueStep, s, v)); grd.addColorStop(0.667, hsv2hex(minHue+4*hueStep, s, v)); grd.addColorStop(0.833, hsv2hex(minHue+5*hueStep, s, v)); grd.addColorStop(1, hsv2hex(maxHue, s, v)); context.fillStyle = grd; context.fill(); } function getColourInterpolator(minVal, maxVal, minHue, maxHue) { // copying Michael Taschler's code of interpolation return function (val) { // Give the values outside the limits the limit colours if (val < minVal) val = minVal; else if (val > maxVal) val = maxVal; // 0.0 to 1.0 scale convertion val -= minVal; if ((maxVal - minVal) != 0) // avoid division by zero val /= (maxVal - minVal); else val = 0; var lowHue = minHue / 360.0; var highHue = maxHue / 360.0; var diffHue = highHue - lowHue; var h = (lowHue + (val * diffHue)) * 360; var s = 0.9; var v = 0.9; return hsv2hex(h, s, v); } } function hsv2hex(h, s, v) { // Hue stays the same // Saturation is very different between the two color spaces // If (2-s)*v < 1 set it to s*v/((2-s)*v) // Otherwise s*v/(2-(2-s)*v) // Conditional is not operating with hue, it is reassigned! s = (s * v) / ((((2-s) * v) < 1) ? ((2-s) * v) : 2-((2-s) * v)); l = (2-s)*v/2; // Lightness is (2-s)*v/2 return d3.hsl(h, s, l).toString(); } /* function drawSectionOverlay(mapExportStat, sectionValue, units, colour) { var sectionData = sectionsVectorData[mapExportStat.SectionName.replace(propsSuffix, "")]; if (sectionData) { var line = d3.svg.line() .x(function(d) {return convert2WebXCoord(d.x);}) .y(function(d) {return convert2WebYCoord(d.y);}) .interpolate("basic"); var svg = d3.select("#map svg") .select("#" + sectionsMapId) svg.append("svg:path") .attr("d", line(sectionData.points) + "z") .style("fill", colour) .on("click", function() {click(mapExportStat);}) .append("title") .text(function() { var title; if (units == optionCalculations["OverallTime"].units) // == " secs", don't want to hardcode the string title = mapExportStat.SectionName + " : " + formatSecsWithDays(sectionValue) + " (" + sectionValue.toFixed(2) + "" + units + ") "; else title = mapExportStat.SectionName + " : " + sectionValue.toFixed(2) + units; return title; }); } function click(mapExportStat) { if (mapExportStat && (clicked !== mapExportStat)) { clicked = mapExportStat; } else { clicked = null; } console.log(clicked); d3.select("#map svg") .select("#" + sectionsMapId) .selectAll("path") .classed("active", clicked && function(d) { console.log(d); return d === clicked; }); } } */ function parseSectionsVectorXml(xml) { var sections = []; $(xml).find("section").each(function() { var points = []; $(this).find("point").each(function() { points.push({ x: Number($(this).attr("x")), y: Number($(this).attr("y")), z: Number($(this).attr("z")), }); }); var section = { name: $(this).attr("name").toUpperCase(), owner: $(this).attr("owner"), points: points, }; sections[section.name] = section; }); return sections; } /* function parseMapExportsAllStatsXml(xml) { var sectionStats = []; $(xml).find("DailySectionStatsDto").each(function() { var exportStat = $(this).find("ExportStat"); var imageBuildStat = $(this).find("ImageBuildStat"); var sectionStat = { SectionName: $(this).find("SectionName").text().toUpperCase(), // Capitalise the names to match the vectordata ExportStat: { SuccessfulCount: Number(exportStat.find("SuccessfulCount").text()), FailedCount: Number(exportStat.find("FailedCount").text()), TotalTimeTicks: Number(exportStat.find("TotalTimeTicks").text()), MinTimeTicks: Number(exportStat.find("MinTimeTicks").text()), MaxTimeTicks: Number(exportStat.find("MaxTimeTicks").text()), }, ImageBuildStat: { SuccessfulCount: Number(imageBuildStat.find("SuccessfulCount").text()), FailedCount: Number(imageBuildStat.find("FailedCount").text()), TotalTimeTicks: Number(imageBuildStat.find("TotalTimeTicks").text()), MinTimeTicks: Number(imageBuildStat.find("MinTimeTicks").text()), MaxTimeTicks: Number(imageBuildStat.find("MaxTimeTicks").text()), }, TotalSectionsBuilt: Number($(this).find("TotalSectionsBuilt").text()), }; sectionStats.push(sectionStat); }); return sectionStats; } */