/**
* @namespace regionUtils
* @classdesc Region utilities, everything to do with
* regions or their calculations goes here
* @property {Bool} regionUtils._isNewRegion - if _isNewRegion is true then a new region will start
* @property {Bool} regionUtils._currentlyDrawing - Boolean to specify if a region is currently being drawn
* @property {Number} regionUtils._currentRegionId - Keep then number of drawn regions and also let them be the id,
* @property {Object[]} regionUtils._currentPoints - Array of points for the current region,
* @property {String} regionUtils._colorInactiveHandle - String for a color "#cccccc",
* @property {String} regionUtils._colorActiveHandle - Color of the point in the region,
* @property {Number} regionUtils._scaleHandle - Scale of the point in regions,
* @property {Number} regionUtils._polygonStrokeWidth - Width of the stroke of the polygon,
* @property {Number} regionUtils._handleRadius - Radius of the point of the region,
* @property {Number} regionUtils._epsilonDistance - Distance at which a click from the first point will consider to close the region,
* @property {Object} regionUtils._regions - Object that contains the regions in the viewer,
* @property {String} regionUtils._drawingclass - String that accompanies the classes of the polygons in the interface"drawPoly",
*/
regionUtils = {
_isNewRegion: true,
_currentlyDrawing: false,
_currentRegionId: 0,
_currentPoints: null,
_colorInactiveHandle: "#cccccc",
_colorActiveHandle: "#ffff00",
_scaleHandle: 0.0025,
_polygonStrokeWidth: 0.0015,
_handleRadius: 0.1,
_epsilonDistance: 0.004,
_regions: {},
_drawingclass: "drawPoly",
_maxRegionsInMenu: 200
}
/**
* Reset the drawing of the regions */
regionUtils.resetManager = function () {
var drawingclass = regionUtils._drawingclass;
d3.select("." + drawingclass).remove();
regionUtils._isNewRegion = true;
regionUtils._currentPoints = null;
}
/**
* When a region is being drawn, this function takes care of the creation of the region */
regionUtils.manager = function (event) {
//console.log(event);
var drawingclass = regionUtils._drawingclass;
//if we come here is because overlayUtils.drawRegions mode is on
// No matter what we have to get the normal coordinates so
//I am going to have to do a hack to get the right OSD viewer
//I will go two parents up to get the DOM id which will tell me the name
//and then I will look for it in tmapp... this is horrible, but will work
/*var eventSource=event.eventSource;//this is a mouse tracker not a viewer
var OSDsvg=d3.select(eventSource.element).select("svg").select("g");
var stringOSDVname=eventSource.element.parentElement.parentElement.id;
var overlay=stringOSDVname.substr(0,stringOSDVname.indexOf('_'));*/
//console.log(overlay);
var OSDviewer = tmapp[tmapp["object_prefix"] + "_viewer"];
var normCoords = OSDviewer.viewport.pointFromPixel(event.position);
//var canvas=tmapp[tmapp["object_prefix"]+"_svgov"].node();
var canvas = overlayUtils._d3nodes[tmapp["object_prefix"] + "_regions_svgnode"].node();
//console.log(normCoords);
var regionobj;
//console.log(d3.select(event.originalEvent.target).attr("is-handle"));
var strokeWstr = regionUtils._polygonStrokeWidth / tmapp["ISS_viewer"].viewport.getZoom();
if (regionUtils._isNewRegion) {
//if this region is new then there should be no points, create a new array of points
regionUtils._currentPoints = [];
//it is not a new region anymore
regionUtils._isNewRegion = false;
//give a new id
regionUtils._currentRegionId += 1;
var idregion = regionUtils._currentRegionId;
//this is out first point for this region
var startPoint = [normCoords.x, normCoords.y];
regionUtils._currentPoints.push(startPoint);
//create a group to store region
regionobj = d3.select(canvas).append('g').attr('class', drawingclass);
regionobj.append('circle').attr('r', 10* regionUtils._handleRadius / tmapp["ISS_viewer"].viewport.getZoom()).attr('fill', regionUtils._colorActiveHandle).attr('stroke', '#ff0000')
.attr('stroke-width', strokeWstr).attr('class', 'region' + idregion).attr('id', 'handle-0-region' + idregion)
.attr('transform', 'translate(' + (startPoint[0].toString()) + ',' + (startPoint[1].toString()) + ') scale(' + regionUtils._scaleHandle + ')')
.attr('is-handle', 'true').style({ cursor: 'pointer' });
} else {
var idregion = regionUtils._currentRegionId;
var nextpoint = [normCoords.x, normCoords.y];
var count = regionUtils._currentPoints.length - 1;
//check if the distance is smaller than epsilonDistance if so, CLOSE POLYGON
if (regionUtils.distance(nextpoint, regionUtils._currentPoints[0]) < 2* regionUtils._epsilonDistance / tmapp["ISS_viewer"].viewport.getZoom() && count >= 2) {
regionUtils.closePolygon();
return;
}
regionUtils._currentPoints.push(nextpoint);
regionobj = d3.select("." + drawingclass);
regionobj.append('circle')
.attr('r', 10* regionUtils._handleRadius / tmapp["ISS_viewer"].viewport.getZoom()).attr('fill', regionUtils._colorActiveHandle).attr('stroke', '#ff0000')
.attr('stroke-width', strokeWstr).attr('class', 'region' + idregion).attr('id', 'handle-' + count.toString() + '-region' + idregion)
.attr('transform', 'translate(' + (nextpoint[0].toString()) + ',' + (nextpoint[1].toString()) + ') scale(' + regionUtils._scaleHandle + ')')
.attr('is-handle', 'true').style({ cursor: 'pointer' });
regionobj.select('polyline').remove();
var polyline = regionobj.append('polyline').attr('points', regionUtils._currentPoints)
.style('fill', 'none')
.attr('stroke-width', strokeWstr)
.attr('stroke', '#ff0000').attr('class', "region" + idregion);
}
}
/**
* Close a polygon, adding a region to the viewer and an interface to it in the side panel */
regionUtils.closePolygon = function () {
var canvas = overlayUtils._d3nodes[tmapp["object_prefix"] + "_regions_svgnode"].node();
var drawingclass = regionUtils._drawingclass;
var regionid = 'region' + regionUtils._currentRegionId.toString();
d3.select("." + drawingclass).remove();
regionsobj = d3.select(canvas);
var hexcolor = overlayUtils.randomColor("hex");
regionUtils._isNewRegion = true;
regionUtils.addRegion([[regionUtils._currentPoints]], regionid, hexcolor);
regionUtils._currentPoints = null;
var strokeWstr = regionUtils._polygonStrokeWidth / tmapp["ISS_viewer"].viewport.getZoom();
regionsobj.append('path').attr("d", regionUtils.pointsToPath(regionUtils._regions[regionid].points)).attr("id", regionid + "_poly")
.attr("class", "regionpoly").attr("polycolor", hexcolor).attr('stroke-width', strokeWstr)
.style("stroke", hexcolor).style("fill", "none")
.append('title').text(regionid).attr("id","path-title-" + regionid);
regionUtils.updateAllRegionClassUI();
$(document.getElementById("regionClass-")).collapse("show");
}
/**
* @param {Object} JSON formatted region to convert to GeoJSON
* @summary This is only for backward compatibility */
regionUtils.oldRegions2GeoJSON = function (regionsObjects) {
try {
// Checking if json is in old format
if (Object.values(regionsObjects)[0].globalPoints) {
return regionUtils.regions2GeoJSON(regionsObjects)
}
else {
return regionsObjects;
}
} catch (error) {
return regionsObjects;
}
}
/**
* @param {Object} GeoJSON formatted region to import
* @summary When regions are imported, create all objects for it from a region object */
regionUtils.regions2GeoJSON = function (regionsObjects) {
function HexToRGB(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return [ parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16) ];
}
function oldCoord2GeoJSONCoord(coordinates) {
// Check for older JSON format with only one list of coordinates
if (coordinates[0].x) {
return [[coordinates.map(function(x) {
return [x.x, x.y];
})]];
}
return coordinates.map (function(coordinateList, i) {
return coordinateList.map (function(coordinateList_i, index) {
return coordinateList_i.map(function(x) {
return [x.x, x.y];
});
});
})
}
geoJSONObjects = {
"type": "FeatureCollection",
"features": Object.values(regionsObjects).map (function(Region, i) {
return {
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": oldCoord2GeoJSONCoord(Region.globalPoints)
},
"properties": {
"name": Region.regionName,
"classification": {
"name": Region.regionClass
},
"color": HexToRGB(Region.polycolor),
"isLocked": false
}
}
})
}
return geoJSONObjects;
}
/**
* @param {Object} GeoJSON formatted region to import
* @summary When regions are imported, create all objects for it from a region object */
regionUtils.geoJSON2regions = function (geoJSONObjects) {
// Helper functions for converting colors to hexadecimal
var viewer = tmapp[tmapp["object_prefix"] + "_viewer"]
if (!viewer.world || !viewer.world.getItemAt(0)) {
setTimeout(function() {
regionUtils.geoJSON2regions(geoJSONObjects);
}, 100);
return;
}
function rgbToHex(rgb) {
return "#" + ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1);
}
function decimalToHex(number) {
if (number < 0){ number = 0xFFFFFFFF + number + 1; }
return "#" + number.toString(16).toUpperCase().substring(2, 8);
}
var canvas = overlayUtils._d3nodes[tmapp["object_prefix"] + "_regions_svgnode"].node();
geoJSONObjects = regionUtils.oldRegions2GeoJSON(geoJSONObjects);
if (!Array.isArray(geoJSONObjects)) {
geoJSONObjects = [geoJSONObjects];
}
geoJSONObjects.forEach(function(geoJSONObj, geoJSONObjIndex) {
if (geoJSONObj.type == "FeatureCollection") {
return regionUtils.geoJSON2regions(geoJSONObj.features);
}
if (geoJSONObj.type == "GeometryCollection") {
return regionUtils.geoJSON2regions(geoJSONObj.geometries);
}
/*if (geoJSONObj.type != "Feature") {
return;
}*/
if (geoJSONObj.geometry === undefined)
geometry = geoJSONObj
else
geometry = geoJSONObj.geometry
var geometryType = geometry.type;
var coordinates;
if (geometryType=="Polygon") {
coordinates = [geometry.coordinates];
}
else if (geometryType=="MultiPolygon") {
coordinates = geometry.coordinates;
}
else {
coordinates = [];
}
var geoJSONObjClass = "";
var hexColor = "#ff0000";
if (!geoJSONObj.properties)
geoJSONObj.properties = {};
if (geoJSONObj.properties.color) {
hexColor = rgbToHex(geoJSONObj.properties.color)
}
if (geoJSONObj.properties.name) {
regionName = geoJSONObj.properties.name;
}
else {
regionName = "Region_" + (geoJSONObjIndex - -1);
}
if (geoJSONObj.properties.object_type) {
geoJSONObjClass = geoJSONObj.properties.object_type;
}
if (geoJSONObj.properties.classification) {
geoJSONObjClass = geoJSONObj.properties.classification.name;
if (geoJSONObj.properties.classification.colorRGB) {
hexColor = decimalToHex(geoJSONObj.properties.classification.colorRGB);
}
}
coordinates = coordinates.map (function(coordinateList, i) {
return coordinateList.map (function(coordinateList_i, index) {
coordinateList_i = coordinateList_i.map(function(x) {
xPoint = new OpenSeadragon.Point(x[0], x[1]);
xPixel = viewer.world.getItemAt(0).imageToViewportCoordinates(xPoint);
return [xPixel.x.toFixed(5), xPixel.y.toFixed(5)];
});
return coordinateList_i.filter(function(value, index, Arr) {
return index % 1 == 0;
});
});
})
var regionId = "Region_geoJSON_" + geoJSONObjIndex;
if (regionId in regionUtils._regions) {
regionId += "_" + (Math.random() + 1).toString(36).substring(7);
}
regionUtils.addRegion(coordinates, regionId, hexColor, geoJSONObjClass);
regionUtils._regions[regionId].regionName = regionName;
regionobj = d3.select(canvas).append('g').attr('class', "mydrawingclass");
var strokeWstr = regionUtils._polygonStrokeWidth / tmapp["ISS_viewer"].viewport.getZoom();
regionobj.append('path').attr("d", regionUtils.pointsToPath(regionUtils._regions[regionId].points)).attr("id", regionId + "_poly")
.attr("class", "regionpoly").attr("polycolor", hexColor).attr('stroke-width', strokeWstr)
.style("stroke", hexColor).style("fill", "none")
.append('title').text(regionName).attr("id","path-title-" + regionId);
if (document.getElementById(regionId + "_class_ta")) {
document.getElementById(regionId + "_class_ta").value = geoJSONObjClass;
document.getElementById(regionId + "_name_ta").value = regionName;
regionUtils.changeRegion(regionId);
}
});
}
/**
* @param {List} points List of list of list of points representing a path
* @summary Given points' coordinates, returns a path string */
regionUtils.pointsToPath = function (points) {
var path = "";
points.forEach(function (subregions) {
subregions.forEach(function (polygons) {
var first = true
polygons.forEach(function (point) {
if (first) {path += "M";first = false;}
else {path += "L"}
path += point.x + " " + point.y;
});
path += "Z"
});
});
return path;
}
/**
* @param {Number[]} p1 Array with x and y coords
* @param {Number[]} p2 Array with x and y coords
* @summary Distance between two points represented as arrays [x1,y1] and [x2,y2] */
regionUtils.distance = function (p1, p2) {
return Math.sqrt((p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1]))
}
/**
* @param {Number[]} points Array of 2D points in normalized coordinates
* @summary Create a region object and store it in the regionUtils._regions container */
regionUtils.addRegion = function (points, regionid, color, regionClass) {
if (!regionClass) regionClass = "";
var op = tmapp["object_prefix"];
var imageWidth = OSDViewerUtils.getImageWidth();
var region = { "id": regionid, "points": [], "globalPoints": [], "regionName": regionid, "regionClass": regionClass, "barcodeHistogram": [] };
region.len = points.length;
var _xmin = points[0][0][0][0], _xmax = points[0][0][0][0], _ymin = points[0][0][0][1], _ymax = points[0][0][0][1];
var objectPointsArray = [];
for (var i = 0; i < region.len; i++) {
subregion = [];
globalSubregion = [];
for (var j = 0; j < points[i].length; j++) {
polygon = [];
globalPolygon = [];
for (var k = 0; k < points[i][j].length; k++) {
if (points[i][j][k][0] > _xmax) _xmax = points[i][j][k][0];
if (points[i][j][k][0] < _xmin) _xmin = points[i][j][k][0];
if (points[i][j][k][1] > _ymax) _ymax = points[i][j][k][1];
if (points[i][j][k][1] < _ymin) _ymin = points[i][j][k][1];
polygon.push({ "x": points[i][j][k][0], "y": points[i][j][k][1] });
globalPolygon.push({ "x": points[i][j][k][0] * imageWidth, "y": points[i][j][k][1] * imageWidth });
}
subregion.push(polygon);
globalSubregion.push(globalPolygon);
}
region.points.push(subregion);
region.globalPoints.push(globalSubregion);
}
region._xmin = _xmin, region._xmax = _xmax, region._ymin = _ymin, region._ymax = _ymax;
region._gxmin = _xmin * imageWidth, region._gxmax = _xmax * imageWidth, region._gymin = _ymin * imageWidth, region._gymax = _ymax * imageWidth;
region.polycolor = color;
regionUtils._regions[regionid] = region;
regionUtils._regions[regionid].associatedPoints=[];
regionUtils.regionUI(regionid);
}
/**
* @param {String} regionid Region identifier to be searched in regionUtils._regions
* @summary Create the whole UI for a region in the side panel */
regionUtils.regionUI = function (regionid) {
var op = tmapp["object_prefix"];
regionClass = regionUtils._regions[regionid].regionClass;
if (regionClass) {
regionUtils.addRegionClassUI (regionClass)
regionClassID = HTMLElementUtils.stringToId(regionClass);
var regionsPanel = document.getElementById("markers-regions-panel-" + regionClassID);
numRegions = Object.values(regionUtils._regions).filter(x => x.regionClass==regionClass).length
if (numRegions > regionUtils._maxRegionsInMenu) {
spanEl = document.getElementById("regionGroupWarning-" + regionClassID)
if (spanEl) spanEl.innerHTML = "<i class='bi bi-exclamation-triangle'></i> Max "+regionUtils._maxRegionsInMenu+" regions displayed below";
return;
}
}
else {
regionUtils.addRegionClassUI (null)
regionClassID = "";
var regionsPanel = document.getElementById("markers-regions-panel-");
numRegions = Object.values(regionUtils._regions).filter(x => x.regionClass==regionClass).length
if (numRegions > regionUtils._maxRegionsInMenu) {
spanEl = document.getElementById("regionGroupWarning-" + regionClassID)
if (spanEl) spanEl.innerHTML = "<i class='bi bi-exclamation-triangle'></i> Max "+regionUtils._maxRegionsInMenu+" regions displayed below";
return;
}
}
var trPanel = HTMLElementUtils.createElement({
kind: "tr",
extraAttributes: {
class: "regiontr",
id: op + regionid + "_tr"
}
});
regionsPanel.appendChild(trPanel);
// Get Class name and Region name
if (regionUtils._regions[regionid].regionClass) {
rClass = regionUtils._regions[regionid].regionClass;
//regionclasstext.value = rClass;
}
else {
rClass = "";
}
if (regionUtils._regions[regionid].regionName) {
rName = regionUtils._regions[regionid].regionName;
//if (regionUtils._regions[regionid].regionName != regionid)
// regionnametext.value = rName;
} else {
rName = regionid;
}
var tdPanel = HTMLElementUtils.createElement({
kind: "td",
});
var checkinput = HTMLElementUtils.inputTypeCheckbox({
id: regionid + "_fill_ta",
class: "form-check-input",
value: regionUtils._regions[regionid].filled,
eventListeners: { click: function () {
regionUtils._regions[regionid].filled = this.checked;
regionUtils.fillRegion(regionid, regionUtils._regions[regionid].filled);
}}
});
tdPanel.appendChild(checkinput);
trPanel.appendChild(tdPanel);
var tdPanel = HTMLElementUtils.createElement({
kind: "td",
id: op + regionid + "_name",
});
var regionnametext = HTMLElementUtils.inputTypeText({
id: regionid + "_name_ta",
extraAttributes: {
size:9,
placeholder: "name",
value: rName,
class: "col mx-1 input-sm form-control form-control-sm"
}
});
regionnametext.addEventListener('change', function () {
regionUtils.changeRegion(regionid);
});
tdPanel.appendChild(regionnametext);
trPanel.appendChild(tdPanel);
var tdPanel = HTMLElementUtils.createElement({
kind: "td",
});
var regionclasstext = HTMLElementUtils.inputTypeText({
id: regionid + "_class_ta",
extraAttributes: {
size: 9,
placeholder: "class",
value: rClass,
class: "col mx-1 input-sm form-control form-control-sm"
}
});
regionclasstext.addEventListener('change', function () {
regionUtils.changeRegion(regionid);
});
tdPanel.appendChild(regionclasstext);
trPanel.appendChild(tdPanel);
var regioncolorinput = HTMLElementUtils.inputTypeColor({
id: regionid + "_color_input",
extraAttributes: {
class: "mx-1 form-control form-control-sm form-control-color-sm"
}
});
regioncolorinput.addEventListener('change', function () {
regionUtils.changeRegion(regionid);
});
if (document.getElementById(regionid + "_poly")) {
var regionpoly = document.getElementById(regionid + "_poly");
regioncolorinput.setAttribute("value", regionpoly.getAttribute("polycolor"));
} else if (regionUtils._regions[regionid].polycolor) {
regioncolorinput.setAttribute("value", regionUtils._regions[regionid].polycolor);
}
var tdPanel = HTMLElementUtils.createElement({
kind: "td",
});
tdPanel.appendChild(regioncolorinput);
trPanel.appendChild(tdPanel);
trPanel.appendChild(tdPanel);
var tdPanel = HTMLElementUtils.createElement({
kind: "td"
});
var regionsdeletebutton = HTMLElementUtils.createButton({
id: regionid + "_delete_btn",
innerText: "<i class='bi bi-trash'></i>",
extraAttributes: {
parentRegion: regionid,
class: "col btn btn-sm btn-primary form-control-sm mx-1"
}
});
regionsdeletebutton.addEventListener('click', function () {
regionUtils.deleteRegion(regionid);
});
tdPanel.appendChild(regionsdeletebutton);
trPanel.appendChild(tdPanel);
var trPanelHist = HTMLElementUtils.createElement({
kind: "tr",
extraAttributes: {
id: op + regionid + "_tr_hist"
}
});
trPanelHist.style.display="none";
regionsPanel.appendChild(trPanelHist);
var row = HTMLElementUtils.createElement({
kind: "td",
extraAttributes: {
class: "region-histogram my-1",
colspan: "52"
}
});
trPanelHist.appendChild(row);
}
/**
* @param {*} x X coordinate of the point to check
* @param {*} y Y coordinate of the point to check
* @param {*} path SVG path
* @param {*} tmpPoint Temporary point to check if in path. This is only for speed.
*/
regionUtils.globalPointInPath=function(x,y,path,tmpPoint) {
tmpPoint.x = x;
tmpPoint.y = y;
return path.isPointInFill(tmpPoint);
};
/**
* @param {Object} quadtree d3.quadtree where the points are stored
* @param {Number} x0 X coordinate of one point in a bounding box
* @param {Number} y0 Y coordinate of one point in a bounding box
* @param {Number} x3 X coordinate of diagonal point in a bounding box
* @param {Number} y3 Y coordinate of diagonal point in a bounding box
* @param {Object} options Tell the function
* @summary Search for points inside a particular region */
regionUtils.searchTreeForPointsInBbox = function (quadtree, x0, y0, x3, y3, options) {
if (options.globalCoords) {
var xselector = options.xselector;
var yselector = options.yselector;
}else{
throw {name : "NotImplementedError", message : "ViewerPointInPath not yet implemented."};
}
var pointsInside=[];
quadtree.visit(function (node, x1, y1, x2, y2) {
if (!node.length) {
const markerData = dataUtils.data[options.dataset]["_processeddata"];
const columns = dataUtils.data[options.dataset]["_csv_header"];
for (const d of node.data) {
const x = markerData[xselector][d];
const y = markerData[yselector][d];
if (x >= x0 && x < x3 && y >= y0 && y < y3) {
// Note: expanding each point into a full object will be
// very inefficient memory-wise for large datasets, so
// should return points as array of indices instead (TODO)
let p = {};
for (const key of columns) {
p[key] = markerData[key][d];
}
pointsInside.push(p);
}
}
}
return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;
});
return pointsInside;
}
/**
* @param {Object} quadtree d3.quadtree where the points are stored
* @param {Number} x0 X coordinate of one point in a bounding box
* @param {Number} y0 Y coordinate of one point in a bounding box
* @param {Number} x3 X coordinate of diagonal point in a bounding box
* @param {Number} y3 Y coordinate of diagonal point in a bounding box
* @param {Object} options Tell the function
* @summary Search for points inside a particular region */
regionUtils.searchTreeForPointsInRegion = function (quadtree, x0, y0, x3, y3, regionid, options) {
if (options.globalCoords) {
var pointInPath = regionUtils.globalPointInPath;
var xselector = options.xselector;
var yselector = options.yselector;
}else{
throw {name : "NotImplementedError", message : "ViewerPointInPath not yet implemented."};
}
var imageWidth = OSDViewerUtils.getImageWidth();
var countsInsideRegion = 0;
var pointsInside=[];
regionPath=document.getElementById(regionid + "_poly");
var svgovname = tmapp["object_prefix"] + "_svgov";
var svg = tmapp[svgovname]._svg;
tmpPoint = svg.createSVGPoint();
pointInBbox = regionUtils.searchTreeForPointsInBbox(quadtree, x0, y0, x3, y3, options);
for (d of pointInBbox) {
if (pointInPath(d[xselector] / imageWidth, d[yselector] / imageWidth, regionPath, tmpPoint)) {
countsInsideRegion += 1;
pointsInside.push(d);
}
}
if (countsInsideRegion) {
regionUtils._regions[regionid].barcodeHistogram.push({ "key": quadtree.treeID, "name": quadtree.treeName, "count": countsInsideRegion });
}
return pointsInside;
}
/** Fill all regions */
regionUtils.fillAllRegions=function(){
var allFilled = Object.values(regionUtils._regions).map(function(e) { return e.filled; }).includes(false);
for(var regionid in regionUtils._regions){
if (regionUtils._regions.hasOwnProperty(regionid)) {
regionUtils.fillRegion(regionid, allFilled);
if(document.getElementById(regionid + "_fill_ta"))
document.getElementById(regionid + "_fill_ta").checked = allFilled;
}
}
}
/**
* @param {String} regionid String id of region to fill
* @summary Given a region id, fill this region in the interface */
regionUtils.fillRegion = function (regionid, value) {
if (value === undefined) {
// we toggle
if(regionUtils._regions[regionid].filled === 'undefined'){
value = true;
}
else {
value = !regionUtils._regions[regionid].filled;
}
}
regionUtils._regions[regionid].filled=value;
var newregioncolor = regionUtils._regions[regionid].polycolor;
var d3color = d3.rgb(newregioncolor);
var newStyle="";
if(regionUtils._regions[regionid].filled){
newStyle = "stroke: " + d3color.rgb().toString()+";";
d3color.opacity=0.5;
newStyle +="fill: "+d3color.rgb().toString()+";";
}else{
newStyle = "stroke: " + d3color.rgb().toString() + "; fill: none;";
}
document.getElementById(regionid + "_poly").setAttribute("style", newStyle);
}
/**
* @param {String} regionid String id of region to delete
* @summary Given a region id, deletes this region in the interface */
regionUtils.deleteRegion = function (regionid) {
var regionPoly = document.getElementById(regionid + "_poly")
regionPoly.parentElement.removeChild(regionPoly);
delete regionUtils._regions[regionid];
var op = tmapp["object_prefix"];
var rPanel = document.getElementById(op + regionid + "_tr");
if (rPanel) {
rPanel.parentElement.removeChild(rPanel);
var rPanelHist = document.getElementById(op + regionid + "_tr_hist");
rPanelHist.parentElement.removeChild(rPanelHist);
}
regionUtils.updateAllRegionClassUI();
}
/**
* @param {String} regionid String id of region to delete
* @summary Given a region id, deletes this region in the interface */
regionUtils.deleteAllRegions = function () {
var canvas = overlayUtils._d3nodes[tmapp["object_prefix"] + "_regions_svgnode"].node();
regionsobj = d3.select(canvas);
regionsobj.selectAll("*").remove();
var regionsPanel = document.getElementById("markers-regions-panel");
regionsPanel.innerText = "";
var regionsPanel = document.getElementById("regionAccordions");
regionsPanel.innerText = "";
regionUtils._regions = {};
}
regionUtils.updateAllRegionClassUI = function (regionClass) {
// get all region classes
var allRegionClasses = Object.values(regionUtils._regions).map(function(e) { return e.regionClass; })
// get only unique values
var singleRegionClasses = allRegionClasses.filter((v, i, a) => a.indexOf(v) === i);
singleRegionClasses.forEach(function (regionClass) {
regionClassID = HTMLElementUtils.stringToId(regionClass);
numRegions = allRegionClasses.filter(x => x==regionClass).length
spanEl = document.getElementById("numRegions-" + regionClassID)
if (spanEl) {
spanEl.innerText = numRegions;
spanElS = document.getElementById("numRegionsS-" + regionClassID)
if (numRegions > 1) spanElS.innerText = "s"; else spanElS.innerText = "";
}
})
Array.from(document.getElementsByClassName("region-accordion")).forEach(function(accordionItem) {
if (Array.from(accordionItem.getElementsByClassName("regiontr")).length == 0) {
accordionItem.remove();
}
});
}
/**
* @param {String} regionClass Region class
* @summary Add accordion for a new region class */
regionUtils.addRegionClassUI = function (regionClass) {
if (regionClass == null) regionClass = "";
var op = tmapp["object_prefix"];
var regionClassID = HTMLElementUtils.stringToId(regionClass);
var accordion_item = document.getElementById("regionClassItem-" + regionClassID);
if (!accordion_item) {
var regionAccordions = document.getElementById("regionAccordions");
var accordion_item = HTMLElementUtils.createElement({
kind: "div",
extraAttributes: {
class: "accordion-item region-accordion",
id: "regionClassItem-" + regionClassID
}
});
regionAccordions.appendChild(accordion_item);
var accordion_header = HTMLElementUtils.createElement({
kind: "h2",
extraAttributes: {
class: "accordion-header",
id: "regionClassHeading-" + regionClassID
}
});
accordion_item.appendChild(accordion_header);
if (!regionClass) regionClassName = "Unclassified"; else regionClassName = regionClass;
var accordion_header_button = HTMLElementUtils.createElement({
kind: "button",
innerHTML: "<i class='bi bi-pentagon'></i> " + regionClassName + " (<span id='numRegions-" + regionClassID + "'>1</span> region<span id='numRegionsS-" + regionClassID + "'></span>) <span class='text-warning' id='regionGroupWarning-" + regionClassID + "'></span>",
extraAttributes: {
"type": "button",
"class": "accordion-button collapsed",
"id": "regionClassHeading-" + regionClassID,
"data-bs-toggle": "collapse",
"data-bs-target": "#" + "regionClass-" + regionClassID,
"aria-expanded": "true",
"aria-controls": "collapseOne"
}
});
accordion_header.appendChild(accordion_header_button);
var accordion_content = HTMLElementUtils.createElement({
kind: "div",
extraAttributes: {
class: "accordion-collapse collapse px-2",
id: "regionClass-" + regionClassID,
"aria-labelledby":"headingOne",
"data-bs-parent":"#regionAccordions"
}
});
accordion_item.appendChild(accordion_content);
var buttonRow = HTMLElementUtils.createElement({
kind: "div",
extraAttributes: {
class: "row my-1 mx-2"
}
});
accordion_content.appendChild(buttonRow);
var regionTable = HTMLElementUtils.createElement({
kind: "table",
extraAttributes: {
class: "table regions_table",
id: "markers-regions-table-" + regionClassID
}
});
accordion_content.appendChild(regionTable);
var colg=document.createElement ("colgroup");
colg.innerHTML='<col width="5%"><col width="38%"><col width="37%"><col width="10%"><col width="10%">';
regionTable.appendChild(colg);
var tblHead = document.createElement("thead");
var tblHeadTr = document.createElement("tr");
tblHead.appendChild(tblHeadTr);
tblHeadTr.appendChild(HTMLElementUtils.createElement({kind:"th",innerText:"Fill"}));
tblHeadTr.appendChild(HTMLElementUtils.createElement({kind:"th",innerText:"Name"}));
tblHeadTr.appendChild(HTMLElementUtils.createElement({kind:"th",innerText:"Class"}));
tblHeadTr.appendChild(HTMLElementUtils.createElement({kind:"th",innerText:"Color"}));
tblHeadTr.appendChild(HTMLElementUtils.createElement({kind:"th",innerText:"Delete"}));
regionTable.appendChild(tblHead);
var regionTbody = HTMLElementUtils.createElement({
kind: "tbody",
id: "markers-regions-panel-" + regionClassID
});
regionTable.appendChild(regionTbody);
var trPanel = HTMLElementUtils.createElement({
kind: "tr"
});
regionTbody.appendChild(trPanel);
var tdPanel = HTMLElementUtils.createElement({
kind: "td",
});
var checkinput = HTMLElementUtils.inputTypeCheckbox({
class: "form-check-input",
id: regionClassID + "_group_fill_ta",
value: false,
eventListeners: { click: function () {
var newFill = this.checked;
groupRegions = Object.values(regionUtils._regions).filter(
x => x.regionClass==regionClass
).forEach(function (region) {
region.filled = newFill;
if (document.getElementById(region.id + "_fill_ta"))
document.getElementById(region.id + "_fill_ta").checked = newFill;
regionUtils.fillRegion(region.id, newFill);
});
}}
});
tdPanel.appendChild(checkinput);
trPanel.appendChild(tdPanel);
var tdPanel = HTMLElementUtils.createElement({
kind: "td",
innerHTML: "<label style='cursor:pointer' for='"+regionClassID+"_group_fill_ta'>All</label>"
});
trPanel.appendChild(tdPanel);
var tdPanel = HTMLElementUtils.createElement({
kind: "td",
});
if (regionClass) rClass = regionClass; else rClass = "";
var regionclasstext = HTMLElementUtils.inputTypeText({
extraAttributes: {
size: 9,
placeholder: "class",
value: rClass,
class: "col mx-1 input-sm form-control form-control-sm"
}
});
regionclasstext.addEventListener('change', function () {
var newClass = this.value;
groupRegions = Object.values(regionUtils._regions).filter(
x => x.regionClass==regionClass
);
for (region of groupRegions) {
if (document.getElementById(region.id + "_class_ta"))
document.getElementById(region.id + "_class_ta").value = newClass;
regionUtils.changeRegion(region.id);
region.regionClass = newClass;
};
regionUtils.updateAllRegionClassUI();
});
tdPanel.appendChild(regionclasstext);
trPanel.appendChild(tdPanel);
var regioncolorinput = HTMLElementUtils.inputTypeColor({
extraAttributes: {
class: "mx-1 form-control form-control-sm form-control-color-sm"
}
});
regioncolorinput.addEventListener('change', function () {
var newColor = this.value;
groupRegions = Object.values(regionUtils._regions).filter(
x => x.regionClass==regionClass
)
for (region of groupRegions) {
region.polycolor = newColor;
if (document.getElementById(region.id + "_color_input"))
document.getElementById(region.id + "_color_input").value = newColor;
regionUtils.changeRegion(region.id);
};
});
var tdPanel = HTMLElementUtils.createElement({
kind: "td",
});
tdPanel.appendChild(regioncolorinput);
trPanel.appendChild(tdPanel);
trPanel.appendChild(tdPanel);
var tdPanel = HTMLElementUtils.createElement({
kind: "td"
});
var regionsdeletebutton = HTMLElementUtils.createButton({
innerText: "<i class='bi bi-trash'></i>",
extraAttributes: {
class: "col btn btn-sm btn-primary form-control-sm mx-1"
}
});
regionsdeletebutton.addEventListener('click', function () {
interfaceUtils.confirm('Are you sure you want to delete the whole '+regionClass+' group?')
.then(function(_confirm){
if (_confirm) {
groupRegions = Object.values(regionUtils._regions).filter(
x => x.regionClass==regionClass
).forEach(function (region) {
regionUtils.deleteRegion(region.id);
});
}
});
});
tdPanel.appendChild(regionsdeletebutton);
trPanel.appendChild(tdPanel);
var regionanalyzebutton = HTMLElementUtils.createButton({
id: regionClassID + "_analyze_btn",
innerText: "Analyze group",
extraAttributes: {
parentRegion: regionClassID,
class: "col btn btn-primary btn-sm form-control mx-1"
}
});
regionanalyzebutton.addEventListener('click', function () {
if (Object.keys(dataUtils.data).length == 0) {
interfaceUtils.alert("Load markers first");
return;
}
Object.values(regionUtils._regions).filter(
x => x.regionClass==regionClass
).forEach(function(region){
regionUtils.analyzeRegion(region.id);
});
});
buttonRow.appendChild(regionanalyzebutton);
}
}
/**
* @param {String} regionid Region identifier
* @summary Change the region properties like color, class name or region name */
regionUtils.changeRegion = function (regionid) {
if (document.getElementById(regionid + "_name_ta")) {
var op = tmapp["object_prefix"];
var rPanel = document.getElementById(op + regionid + "_tr");
var rPanel_hist = document.getElementById(op + regionid + "_tr_hist");
if (regionUtils._regions[regionid].regionClass != document.getElementById(regionid + "_class_ta").value) {
if (document.getElementById(regionid + "_class_ta").value) {
regionUtils._regions[regionid].regionClass = document.getElementById(regionid + "_class_ta").value;
classID = HTMLElementUtils.stringToId(regionUtils._regions[regionid].regionClass);
regionUtils.addRegionClassUI (regionUtils._regions[regionid].regionClass)
$(rPanel).detach().appendTo('#markers-regions-panel-' + classID)
$(rPanel_hist).detach().appendTo('#markers-regions-panel-' + classID)
} else {
regionUtils._regions[regionid].regionClass = null;
regionUtils.addRegionClassUI (null)
classID = HTMLElementUtils.stringToId(regionUtils._regions[regionid].regionClass);
$(rPanel).detach().appendTo('#markers-regions-panel-')
$(rPanel_hist).detach().appendTo('#markers-regions-panel-')
}
regionUtils.updateAllRegionClassUI();
}
if (document.getElementById(regionid + "_name_ta").value) {
regionUtils._regions[regionid].regionName = document.getElementById(regionid + "_name_ta").value;
} else {
regionUtils._regions[regionid].regionName = regionid;
}
var newregioncolor = document.getElementById(regionid + "_color_input").value;
regionUtils._regions[regionid].polycolor = newregioncolor;
}
regionUtils.updateRegionDraw(regionid);
}
/**
* @param {String} regionid Region identifier
* @summary Change the region properties like color, class name or region name */
regionUtils.updateRegionDraw = function (regionid) {
var newregioncolor = regionUtils._regions[regionid].polycolor;
var d3color = d3.rgb(newregioncolor);
var newStyle = "stroke: " + d3color.rgb().toString() + "; fill: none;";
document.getElementById(regionid + "_poly").setAttribute("style", newStyle);
if (regionUtils._regions[regionid].filled === undefined)
regionUtils._regions[regionid].filled = false;
regionUtils.fillRegion(regionid, regionUtils._regions[regionid].filled);
if (regionUtils._regions[regionid].regionName) {rName = regionUtils._regions[regionid].regionName;}
else {rName = regionid;}
document.getElementById("path-title-" + regionid).innerHTML = rName;
}
/**
* regionUtils */
regionUtils.analyzeRegion = function (regionid) {
var op = tmapp["object_prefix"];
function compare(a, b) {
if (a.count > b.count)
return -1;
if (a.count < b.count)
return 1;
return 0;
}
function clone(obj) {
if (null == obj || "object" != typeof obj) return obj;
var copy = obj.constructor();
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
}
return copy;
}
regionUtils._regions[regionid].associatedPoints=[];
regionUtils._regions[regionid].barcodeHistogram=[];
allDatasets = Object.keys(dataUtils.data);
for (var uid of allDatasets) {
var allkeys=Object.keys(dataUtils.data[uid]["_groupgarden"]);
var datapath = dataUtils.data[uid]["_csv_path"];
if (datapath.includes(".csv") || datapath.includes(".CSV")) {
// Strip everything except the filename, to save a bit of memory
// and reduce the filesize when exporting to CSV
datapath = dataUtils.data[uid]["_csv_path"].split("/").pop();
} else if (datapath.includes("base64")) {
// If the file is encoded in the path as a Base64 string, use
// the name of the marker tab as identifier in the output CSV
datapath = dataUtils.data[uid]["_name"];
}
for (var codeIndex in allkeys) {
var code = allkeys[codeIndex];
var pointsInside=regionUtils.searchTreeForPointsInRegion(dataUtils.data[uid]["_groupgarden"][code],
regionUtils._regions[regionid]._gxmin,regionUtils._regions[regionid]._gymin,
regionUtils._regions[regionid]._gxmax,regionUtils._regions[regionid]._gymax,
regionid, {
"globalCoords":true,
"xselector":dataUtils.data[uid]["_X"],
"yselector":dataUtils.data[uid]["_Y"],
"dataset":uid
});
if(pointsInside.length>0){
pointsInside.forEach(function(p){
var pin=clone(p);
pin.regionid=regionid;
pin.dataset=datapath
regionUtils._regions[regionid].associatedPoints.push(pin)
});
}
}
}
regionUtils._regions[regionid].barcodeHistogram.sort(compare);
var rPanel = document.getElementById(op + regionid + "_tr_hist");
if (rPanel) {
var rpanelbody = rPanel.getElementsByClassName("region-histogram")[0];
histodiv = document.getElementById(regionid + "_histogram");
if (histodiv) {
histodiv.parentNode.removeChild(histodiv);
}
var div = HTMLElementUtils.createElement({ kind: "div", id: regionid + "_histogram" });
var histogram = regionUtils._regions[regionid].barcodeHistogram;
var table = div.appendChild(HTMLElementUtils.createElement({
kind: "table",
extraAttributes: {
class: "table table-striped",
style: "overflow-y: auto;"
}
}));
thead = HTMLElementUtils.createElement({kind: "thead"});
thead.innerHTML = `<tr>
<th scope="col">Key</th>
<th scope="col">Name</th>
<th scope="col">Count</th>
</tr>`;
tbody = HTMLElementUtils.createElement({kind: "tbody"});
table.appendChild(thead);
table.appendChild(tbody);
for (var i in histogram) {
var innerHTML = "";
innerHTML += "<td>" + histogram[i].key + "</td>";
innerHTML += "<td>" + histogram[i].name + "</td>";
innerHTML += "<td>" + histogram[i].count + "</td>";
tbody.appendChild(HTMLElementUtils.createElement({
kind: "tr",
"innerHTML": innerHTML
}));
}
rpanelbody.appendChild(div);
$(rPanel).show();
}
}
/**
* regionUtils */
regionUtils.regionsOnOff = function () {
overlayUtils._drawRegions = !overlayUtils._drawRegions;
var op = tmapp["object_prefix"];
let regionIcon = document.getElementById(op + '_drawregions_icon');
if (overlayUtils._drawRegions) {
regionIcon.classList.remove("bi-circle");
regionIcon.classList.add("bi-check-circle");
} else {
regionUtils.resetManager();
regionIcon.classList.remove("bi-check-circle");
regionIcon.classList.add("bi-circle");
}
}
/**
* regionUtils */
regionUtils.exportRegionsToJSON = function () {
regionUtils.regionsToJSON();
}
/**
* regionUtils */
regionUtils.importRegionsFromJSON = function () {
regionUtils.deleteAllRegions();
regionUtils.JSONToRegions();
}
regionUtils.pointsInRegionsToCSV=function(){
var alldata=[]
for (r in regionUtils._regions){
var regionPoints=regionUtils._regions[r].associatedPoints;
regionUtils._regions[r].associatedPoints.forEach(function(p){
p.regionName=regionUtils._regions[r].regionName
p.regionClass=regionUtils._regions[r].regionClass
alldata.push(p);
});
}
var csvRows=[];
var headers=alldata.reduce(function(arr, o) {
return Object.keys(o).reduce(function(a, k) {
if (a.indexOf(k) == -1) a.push(k);
return a;
}, arr)
}, []);
csvRows.push(headers.join(','));
for(var row of alldata){
var values=[];
headers.forEach(function(header){
const value = row[header];
if (isNaN(value) && typeof(value) == "string" &&
(value.includes(",") || value.includes("\""))) {
// Make sure that commas and quotation marks are properly escaped
let escaped = value;
if (escaped.includes(",") || escaped.includes("\""))
escaped = "\"" + escaped.replaceAll("\"", "\"\"") + "\"";
values.push(escaped);
} else {
values.push(value);
}
});
csvRows.push(values.join(","));
}
var theblobdata=csvRows.join('\n');
regionUtils.downloadPointsInRegionsCSV(theblobdata);
}
regionUtils.downloadPointsInRegionsCSV=function(data){
var blob = new Blob([data],{kind:"text/csv"});
var url=window.URL.createObjectURL(blob);
var a=document.createElement("a");
a.setAttribute("hidden","");
a.setAttribute("href",url);
a.setAttribute("download","pointsinregions.csv");
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
regionUtils.regionsToJSON= function(){
if (window.Blob) {
var op=tmapp["object_prefix"];
var jsonse = JSON.stringify(regionUtils.regions2GeoJSON(regionUtils._regions));
var blob = new Blob([jsonse], {kind: "application/json"});
var url = URL.createObjectURL(blob);
var a=document.createElement("a");// document.getElementById("invisibleRegionJSON");
if(document.getElementById(op+"_region_file_name")){
var name=document.getElementById(op+"_region_file_name").value;
}else{
var name="regions.json";
}
a.href = url;
a.download = name;
a.textContent = "Download backup.json";
a.click();
// Great success! The Blob API is supported.
} else {
interfaceUtils.alert('The File APIs are not fully supported in this browser.');
}
}
regionUtils.JSONToRegions= function(filepath){
if(filepath!==undefined){
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const path = urlParams.get('path')
if (path != null) {
filepath = path + "/" + filepath;
}
fetch(filepath)
.then((response) => {
return response.json();
})
.then((regionsobj) => {
regionUtils.JSONValToRegions(regionsobj);
});
}
else if(window.File && window.FileReader && window.FileList && window.Blob) {
var op=tmapp["object_prefix"];
var text=document.getElementById(op+"_region_files_import");
var file=text.files[0];
var reader = new FileReader();
reader.onload=function(event) {
// The file's text will be printed here
regionUtils.JSONValToRegions(JSON.parse(event.target.result));
};
reader.readAsText(file);
} else {
interfaceUtils.alert('The File APIs are not fully supported in this browser.');
}
}
regionUtils.JSONValToRegions= function(jsonVal){
// The file's text will be printed here
var regions=jsonVal;
regionUtils.geoJSON2regions(regions);
regionUtils.updateAllRegionClassUI();
$('[data-bs-target="#markers-regions-project-gui"]').tab('show');
}