Files
gtav-src/tools_ng/web/dev/map_exports/js/overlay.js
T
2025-09-29 00:52:08 +02:00

738 lines
21 KiB
JavaScript
Executable File

// 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;
}
*/