Files
gtav-src/tools_ng/web/prod/shared/js/SVGMap.js
T
2025-09-29 00:52:08 +02:00

706 lines
19 KiB
JavaScript
Executable File

/*
* Class SVGMap
*
* @elementId - the html DOM id
*
*
* Requires project.js, heatmap.js
*/
var SVGMap = function(elementId, mapConf) {
var vBoxWidth = 1200,
vBoxHeight = 900,
bgWidth = 900,
bgHeight = 1500,
overlayID = "telemetry",
tooltip = new CustomTooltip(elementId + "-tooltip"),
clicked,
heatmapOptionsId;
// Initialise the map
loadSvg();
function loadSvg() {
//block();
$.ajax({
url: mapConf.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);
}
$("#" + elementId).append(importedNode);
},
error: function (xhr, ajaxOptions, thrownError){
console.error(this.url + "\n" + ajaxOptions + " " + xhr.status + " " + thrownError );
},
complete: function() {
var svg = d3.select("#" + elementId + " svg")
.attr("width", $("#" + elementId).width())
.attr("height", $("#" + elementId).height())
.attr("enable-background", "new 0 0 " + bgWidth + " " + bgHeight)
.call(svg_interact);
drawGrid();
}
});
//unBlock();
}
function drawGrid() {
var gridStep = 500/mapConf.scale; // The step of the grid in map coordinates
var grid = d3.select("#" + elementId + " 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 drawLayers(layersElementId) {
$("#" + layersElementId)
.append(
$("<fieldset>")
.attr("id", "map-layers")
.append(
$("<legend>").text("Map Layers")
)
)
mapConf.layers.push(overlayID);
$.each(mapConf.layers, function(i, layer) {
$("#map-layers")
.append(
$("<input>")
.attr("type", "checkbox")
.attr("id", "map-layer-" + layer)
.attr("checked", true)
.val(layer)
)
.append(
$("<label>")
.attr("for", "map-layer-" + layer)
.text(capitaliseString(layer))
)
.append($("<br />"));
});
$("#map-layers input[type=checkbox]").click(function() {
var layer = d3.select("#" + elementId + " svg").select("g#" + $(this).val());
if ($(this).is(":checked")) {
layer
.transition()
.attr("opacity", 1);
}
else {
layer
.transition()
.attr("opacity", 0.01);
}
});
}
function show() {
$("#" + elementId)
.removeClass("hidden");
}
function hide() {
$("#" + elementId)
.addClass("hidden");
}
function removeOverlay() {
d3.select("#" + elementId + " svg")
.selectAll("g#" + overlayID)
.transition()
.attr("opacity", 0.01)
.remove();
}
function drawCircles(dataObjects, dataFunctions) {
var minPercent = 2,
maxPercent = 12;
var svgOverlay = d3.select("#" + elementId + " svg")
.append("svg:g")
.attr("id", overlayID);
svgOverlay.append("svg:rect")
.attr("fill", "#000000")
.attr("fill-opacity", "0.5")
.attr("width", bgWidth)
.attr("height", bgHeight);
var valuesSum = d3.sum(dataObjects, function(d) {
return dataFunctions.getValue(d);
});
svgOverlay
.selectAll("circle")
.data(dataObjects)
.enter()
.append("svg:circle")
.attr("id", function(d, i) {return "circle_" + i;})
.attr("cx", function(d) {
return convert2WebXCoord(dataFunctions.getX(d));
})
.attr("cy", function(d) {
return convert2WebYCoord(dataFunctions.getY(d));
})
.attr("r", function(d, i) {
var radius = 50/mapConf.scale;
if (dataFunctions.getValue(d)) {
radius = (dataFunctions.getValue(d)/valuesSum) * 100;
radius = (radius < minPercent) ? minPercent : radius;
radius = (radius > maxPercent) ? maxPercent : radius;
}
return radius;
})
.attr("fill", function(d) {return dataFunctions.getColour(d)})
//.attr("fill-opacity", 0.85)
.attr("stroke", "rgba(0, 0, 0, 0.5)")
.attr("stroke-width", 0.2)
.attr("opacity", 0.85)
.on("mouseover", function(d, i) {
tooltip.showTooltip(dataFunctions.getTooltipContent(d), d3.event);
})
.on("mouseout", function(d, i) {
tooltip.hideTooltip();
});
/*
.append("title")
.text(function(d, i) {
var title = ""
+ "Name: " + dataFunctions.getName(d)
+ ", \nLocation: (x=" + dataFunctions.getX(d)
+ ", y="
+ dataFunctions.getY(d)
+ ")";
return title;
});
*/
}
function drawHeatmap(dataObjects, dataFunctions, isInverted) {
var min = 0, max = 0;
$.each(dataObjects, function(i, d) {
min = ((dataFunctions.getValue(d) < min) ? dataFunctions.getValue(d) : min);
max = ((dataFunctions.getValue(d) > max) ? dataFunctions.getValue(d) : max);
});
var med = ((min+max)/2);
var colourInterpolator;
if (heatmapOptionsId) {
// var thresholdSlider = $("#" + domElement + " div.threshold-slider");
// thresholdSlider.slider("option", "min", min);
// thresholdSlider.slider("option", "max", max);
// thresholdSlider.slider("refresh");
var valuesSlider = $("#" + heatmapOptionsId + " div.value-slider");
valuesSlider.slider("option", "min", min);
valuesSlider.slider("option", "max", max);
$("#" + heatmapOptionsId + " span.value-min").text(min.toFixed(2));
$("#" + heatmapOptionsId + " span.value-max").text(max.toFixed(2));
if ((valuesSlider.slider("values")[0] == min) && (valuesSlider.slider("values")[1] == min)) {
valuesSlider.slider("values", 0, min);
valuesSlider.slider("values", 1, max);
}
valuesSlider.slider("refresh");
rangeSliderValues = valuesSlider.slider("values");
var heatmapSliderValues = $("#" + heatmapOptionsId + " div.heatmap-slider-placeholder").slider("values");
var colourInterpolator = getColourInterpolator(rangeSliderValues[0], rangeSliderValues[1],
(isInverted) ? heatmapSliderValues[1] : heatmapSliderValues[0],
(isInverted) ? heatmapSliderValues[0] : heatmapSliderValues[1]
);
//var colourInterpolator = getColourInterpolator(min, max, 0, 120);
$("#" + heatmapOptionsId + " span.heatmap-gradient" + " span.heatmap-min").text(rangeSliderValues[0].toFixed(2));
$("#" + heatmapOptionsId + " span.heatmap-gradient" + " span.heatmap-med").text(
((rangeSliderValues[0] + rangeSliderValues[1]) / 2).toFixed(2)
);
$("#" + heatmapOptionsId + " span.heatmap-gradient" + " span.heatmap-max").text(rangeSliderValues[1].toFixed(2));
}
else {
colourInterpolator = getColourInterpolator(min, max, 0, 120);
}
var heatmap = d3.select("#" + elementId + " svg").append("svg:g")
.attr("id", overlayID)
//.attr("opacity", 0.01);
heatmap
.selectAll("rect")
.data(dataObjects)
.enter()
.append("svg:rect")
.attr("x", function(d) {
return convert2WebXCoord(dataFunctions.getX(d));
})
.attr("y", function(d) {
return convert2WebYCoord(dataFunctions.getY(d) + dataFunctions.getResolution());
})
.attr("width", dataFunctions.getResolution()/project.map.scale)
.attr("height", dataFunctions.getResolution()/project.map.scale)
.attr("fill", function(d) {
return colourInterpolator(dataFunctions.getValue(d));
})
//.attr("visibility", function(d) { // Hide if below the threshold limit
// return (valueFunc(d) >= thresholdSlider.slider("value")) ? "visible" : "hidden";
//})
.on("click", heatmapHighlight)
.append("title")
.text(dataFunctions.getTitle);
heatmap
.transition()
.attr("opacity", 1);
function heatmapHighlight(d) {
if (d && clicked !== d) {
clicked = d;
}
else {
clicked = null;
}
heatmap.selectAll("rect")
.classed("clicked", clicked && function(d) { return d === clicked; });
}
}
// end of drawHeatmap
function addHeatmapOptions(parentElementId, graphRedrawFunction, isInverted) {
var optionsId = "heatmap-options";
var optionsLabel = "Heatmap Options";
$("#" + parentElementId)
.append(
$("<fieldset>")
.attr("id", optionsId)
.append(
$("<legend>").text(optionsLabel)
)
);
$("#" + optionsId).append(
$("<div>")
.addClass("heatmap-options")
.append(
$("<div>")
.append(
$("<span>").text("Gradient Colour Range")
)
.append(
$("<br />")
)
.append(
$("<span>")
.addClass("heatmap-slider")
.append(
$("<div>")
.addClass("heatmap-slider-placeholder")
.append(
$("<canvas>")
)
)
)
)
.append(
$("<br />")
)
.append(
$("<div>")
.append(
$("<span>").text("Gradient Value Range")
)
.append(
$("<div>").addClass("value-slider")
)
.append(
$("<span>").html("&lt;- Min -")
)
.append(
$("<span>").addClass("value-min")
)
.append(
$("<span>").html(" Max -&gt; ")
)
.append(
$("<span>").addClass("value-max")
)
)
.append(
$("<br />")
)
.append(
$("<div>")
.append(
$("<span>").text("Heatmap Gradient")
)
.append(
$("<span>").addClass("heatmap-units")
)
.append(
$("<br />")
)
.append(
$("<span>")
.addClass("heatmap-gradient")
.append(
$("<canvas>")
)
.append(
$("<br />")
)
.append(
$("<span>").addClass("heatmap-min")
)
.append(
$("<span>").addClass("heatmap-med")
)
.append(
$("<span>").addClass("heatmap-max")
)
)
)
);
var defaultMinHue = 0, // Hue angle starts from Red
defaultMaxHue = 240, // and interpolates to Blue
defaultMinHueSlider = 0, //
defaultMaxHueSlider = 120; //
$("#" + optionsId + " div.heatmap-slider-placeholder").slider({
range: true,
min: defaultMinHue,
max: defaultMaxHue,
values: [defaultMinHueSlider, defaultMaxHueSlider],
stop: function(event, ui ) { // fire when mouse is released
generateHeatmapGradient(optionsId + " span.heatmap-gradient",
(isInverted) ? ui.values[1] : ui.values[0],
(isInverted) ? ui.values[0] : ui.values[1]
);
typewatch(graphRedrawFunction, 200);
}
});
$("#" + optionsId + " div.value-slider").slider({
range: true,
min: 0,
max: 0,
values: [0, 0],
stop: function(event, ui ) { // fire when mouse is released
typewatch(graphRedrawFunction, 200);
}
});
/*
$("#" + optionsId + " 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));
//generateCustomOverlay(true);
graphRedrawFunction();
}
});
*/
generateHeatmapGradient(optionsId + " span.heatmap-slider", defaultMinHue, defaultMaxHue);
generateHeatmapGradient(optionsId + " span.heatmap-gradient",
(isInverted) ? defaultMaxHueSlider : defaultMinHueSlider,
(isInverted) ? defaultMinHueSlider : defaultMaxHueSlider
);
//defaultMaxHueSlider, defaultMinHueSlider);
// Save to global
heatmapOptionsId = optionsId;
}
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, hsv2hsl(minHue, s, v));
grd.addColorStop(0.167, hsv2hsl(minHue+hueStep, s, v));
grd.addColorStop(0.333, hsv2hsl(minHue+2*hueStep, s, v));
grd.addColorStop(0.5, hsv2hsl(minHue+3*hueStep, s, v));
grd.addColorStop(0.667, hsv2hsl(minHue+4*hueStep, s, v));
grd.addColorStop(0.833, hsv2hsl(minHue+5*hueStep, s, v));
grd.addColorStop(1, hsv2hsl(maxHue, s, v));
context.fillStyle = grd;
context.fill();
}
function exportToImage() {
var canvasId = "viewport-canvas";
$("#" + canvasId).remove();
$("#" + elementId)
.append(
$("<canvas>")
.attr("id", canvasId)
.addClass("hidden")
.css("width", 1000)
.css("height", 1000)
);
var svgMarkup = $("<div>").append(
$("#" + elementId + " svg")
.clone()
.attr("width", 1000)
.attr("height", 1000)
)
.html()
//.replace(/xmlns=\"http:\/\/www\.w3\.org\/2000\/svg\"/, '') // IE fixes
//.replace(/xmlns:NS1=\"\"/, '')
//.replace(/xmlns:NS2=\"\"/, '')
//.replace(/NS2:xml:space=\"preserve\"/, '')
//.replace(/NS1:xmlns:xlink=\"http:\/\/www\.w3\.org\/1999\/xlink\"/, '');
canvg(canvasId, svgMarkup);
var canvas = document.getElementById(canvasId);
var img = canvas.toDataURL("image/png");
window.open(img);
}
function convert2WebXCoord(x) {
return ((x + mapConf.coords.x)/mapConf.scale);
}
function convert2WebYCoord(y) {
return (((-1)*y + mapConf.coords.y)/mapConf.scale);
}
function convert2WebCoords(p) {
return [convert2WebXCoord(p[0]), convert2WebYCoord(p[1])];
}
/* Copied from ie-hacks.js */
function ieImportNode(node, allChildren) {
// Manually imports node to the provided document
switch (node.nodeType) {
case document.ELEMENT_NODE:
var newNode = document.createElementNS(node.namespaceURI, node.nodeName);
if(node.attributes && node.attributes.length > 0) {
for(var i = 0, il = node.attributes.length; i < il; i++) {
newNode.setAttribute(node.attributes[i].nodeName, node.getAttribute(node.attributes[i].nodeName));
}
}
if(allChildren && node.childNodes && node.childNodes.length > 0) {
for(var i = 0, il = node.childNodes.length; i < il; i++){
newNode.appendChild(ieImportNode(node.childNodes[i], allChildren));
}
}
return newNode;
break;
case document.TEXT_NODE:
case document.CDATA_SECTION_NODE:
case document.COMMENT_NODE:
return document.createTextNode(node.nodeValue);
break;
}
}
/* Copied from svg_interact.js */
function svg_interact(svg,p) {
return new svg_interactObj(svg,p);
}
function svg_interactObj(svg,p) {
var p = p ? p : {},
zoom_speed = p.zoom_speed ? p.zoom_speed : 1.15,
viewBox = svg[0][0].viewBox.baseVal;
panning = null,
current_mouse = null;
/* Panning moves the viewbox */
function mousemove(){
//console.log(current_mouse);
if (d3.mouse)
current_mouse = d3.mouse(this);
else
current_mouse = d3.svg.mouse(this);
if (panning) {
viewBox.x += (panning[0] - current_mouse[0]);
viewBox.y += (panning[1] - current_mouse[1]);
updateViewBoxAttr(viewBox);
}
};
function mousedown() {
if (d3.mouse)
panning = d3.mouse(this);
else
panning = d3.svg.mouse(this);
}
/* Zoom with mousewheel - keeping mouse position in same location*/
function wheel(event) {
var delta = 0;
if (event.wheelDelta) {
delta = event.wheelDelta/120;
}
else if (event.detail) {
delta = -event.detail/3;
}
move = (delta<0) ? -delta * zoom_speed : 1/(delta*zoom_speed);
viewBox.x = (current_mouse[0] - (current_mouse[0]-viewBox.x) * move);
viewBox.y = (current_mouse[1] - (current_mouse[1]-viewBox.y) * move);
viewBox.height = viewBox.height * move;
viewBox.width = viewBox.width * move;
updateViewBoxAttr(viewBox);
};
// Webkit fix - otherwise the viewbox is't always picking up the changes and zoom/pan fails
function updateViewBoxAttr(viewBox) {
svg[0][0].setAttribute("viewBox", viewBox.x + " " + viewBox.y + " " + viewBox.width + " " + viewBox.height);
}
if (navigator.userAgent.toLowerCase().indexOf("firefox") > -1)
svg[0][0].addEventListener("DOMMouseScroll", wheel, false);
else
svg[0][0].addEventListener("mousewheel", wheel, false);
svg[0][0].onmousewheel = wheel;
svg.onmousewheel = wheel;
svg.on("mousemove", mousemove);
svg.on("mousedown", mousedown);
d3.select(window).on("mouseup", function () { panning = null;})
svg[0][0].ondragstart = function() { return false } // Firefox fix
return svg;
}
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 hsv2hsl(h, s, v);
}
}
function hsv2hsl(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();
}
return {
removeOverlay: removeOverlay,
drawCircles: drawCircles,
drawHeatmap: drawHeatmap,
addHeatmapOptions: addHeatmapOptions,
drawLayers: drawLayers,
show: show,
hide: hide,
exportToImage: exportToImage,
};
}