From a74d87af7a43c96ebf181d48a98326c46f464653 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 25 Oct 2023 22:36:48 +0000 Subject: [PATCH] GUACAMOLE-1876: Display points of interest heatmap in history recording player. --- doc/licenses/d3-path-3.1.0/LICENSE | 13 + doc/licenses/d3-path-3.1.0/README | 8 + .../d3-path-3.1.0/dep-coordinates.txt | 1 + doc/licenses/d3-shape-3.2.0/LICENSE | 13 + doc/licenses/d3-shape-3.2.0/README | 8 + .../d3-shape-3.2.0/dep-coordinates.txt | 1 + guacamole/src/main/frontend/package-lock.json | 717 ++++++++++++++++++ guacamole/src/main/frontend/package.json | 1 + .../src/app/player/directives/player.js | 89 ++- .../player/services/playerHeatmapService.js | 264 +++++++ .../src/app/player/templates/player.html | 15 + .../app/settings/styles/history-player.css | 73 ++ .../main/frontend/src/translations/en.json | 10 +- 13 files changed, 1208 insertions(+), 5 deletions(-) create mode 100644 doc/licenses/d3-path-3.1.0/LICENSE create mode 100644 doc/licenses/d3-path-3.1.0/README create mode 100644 doc/licenses/d3-path-3.1.0/dep-coordinates.txt create mode 100644 doc/licenses/d3-shape-3.2.0/LICENSE create mode 100644 doc/licenses/d3-shape-3.2.0/README create mode 100644 doc/licenses/d3-shape-3.2.0/dep-coordinates.txt create mode 100644 guacamole/src/main/frontend/src/app/player/services/playerHeatmapService.js diff --git a/doc/licenses/d3-path-3.1.0/LICENSE b/doc/licenses/d3-path-3.1.0/LICENSE new file mode 100644 index 0000000000..ed25746bbf --- /dev/null +++ b/doc/licenses/d3-path-3.1.0/LICENSE @@ -0,0 +1,13 @@ +Copyright 2015-2022 Mike Bostock + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/doc/licenses/d3-path-3.1.0/README b/doc/licenses/d3-path-3.1.0/README new file mode 100644 index 0000000000..a6289bac56 --- /dev/null +++ b/doc/licenses/d3-path-3.1.0/README @@ -0,0 +1,8 @@ +d3-path (https://github.com/d3/d3-path) +---------------------------------------------------------- + + Version: 3.1.0 + From: 'Mike Bostock' + License(s): + BSD (bundled/d3-path-3.1.0/LICENSE) + diff --git a/doc/licenses/d3-path-3.1.0/dep-coordinates.txt b/doc/licenses/d3-path-3.1.0/dep-coordinates.txt new file mode 100644 index 0000000000..5159b4e033 --- /dev/null +++ b/doc/licenses/d3-path-3.1.0/dep-coordinates.txt @@ -0,0 +1 @@ +d3-path:3.1.0 diff --git a/doc/licenses/d3-shape-3.2.0/LICENSE b/doc/licenses/d3-shape-3.2.0/LICENSE new file mode 100644 index 0000000000..fbe44bdc9a --- /dev/null +++ b/doc/licenses/d3-shape-3.2.0/LICENSE @@ -0,0 +1,13 @@ +Copyright 2010-2022 Mike Bostock + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/doc/licenses/d3-shape-3.2.0/README b/doc/licenses/d3-shape-3.2.0/README new file mode 100644 index 0000000000..7f5f25f4ea --- /dev/null +++ b/doc/licenses/d3-shape-3.2.0/README @@ -0,0 +1,8 @@ +d3-path (https://github.com/d3/d3-shape) +---------------------------------------------------------- + + Version: 3.2.0 + From: 'Mike Bostock' + License(s): + BSD (bundled/d3-shape-3.2.0/LICENSE) + diff --git a/doc/licenses/d3-shape-3.2.0/dep-coordinates.txt b/doc/licenses/d3-shape-3.2.0/dep-coordinates.txt new file mode 100644 index 0000000000..10973f4897 --- /dev/null +++ b/doc/licenses/d3-shape-3.2.0/dep-coordinates.txt @@ -0,0 +1 @@ +d3-shape:3.2.0 diff --git a/guacamole/src/main/frontend/package-lock.json b/guacamole/src/main/frontend/package-lock.json index e44b0da8dd..89dbabcbdc 100644 --- a/guacamole/src/main/frontend/package-lock.json +++ b/guacamole/src/main/frontend/package-lock.json @@ -14,6 +14,7 @@ "angular-translate-loader-static-files": "^2.19.0", "blob-polyfill": ">=7.0.20220408", "csv": "^6.2.5", + "d3": "^7.8.5", "datalist-polyfill": "^1.25.1", "file-saver": "^2.0.5", "fuzzysort": "^2.0.4", @@ -4651,6 +4652,384 @@ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" }, + "node_modules/d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/datalist-polyfill": { "version": "1.25.1", "resolved": "https://registry.npmjs.org/datalist-polyfill/-/datalist-polyfill-1.25.1.tgz", @@ -4743,6 +5122,14 @@ "node": ">=0.10.0" } }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, "node_modules/des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -6131,6 +6518,17 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -6319,6 +6717,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -9609,6 +10015,11 @@ "inherits": "^2.0.1" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -9617,6 +10028,11 @@ "aproba": "^1.1.1" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -14937,6 +15353,276 @@ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" }, + "d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "requires": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + } + }, + "d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" + }, + "d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + } + }, + "d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "requires": { + "d3-array": "^3.2.0" + } + }, + "d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "requires": { + "delaunator": "5" + } + }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "requires": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + } + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "requires": { + "d3-dsv": "1 - 3" + } + }, + "d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "requires": { + "d3-array": "2.5.0 - 3" + } + }, + "d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + }, + "d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" + }, + "d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + }, + "d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "requires": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + } + }, + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, + "d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "requires": { + "d3-path": "^3.1.0" + } + }, + "d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "requires": { + "d3-time": "1 - 3" + } + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, "datalist-polyfill": { "version": "1.25.1", "resolved": "https://registry.npmjs.org/datalist-polyfill/-/datalist-polyfill-1.25.1.tgz", @@ -15012,6 +15698,14 @@ } } }, + "delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "requires": { + "robust-predicates": "^3.0.0" + } + }, "des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -16064,6 +16758,14 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, "icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -16199,6 +16901,11 @@ "side-channel": "^1.0.4" } }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, "interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -18796,6 +19503,11 @@ "inherits": "^2.0.1" } }, + "robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -18804,6 +19516,11 @@ "aproba": "^1.1.1" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", diff --git a/guacamole/src/main/frontend/package.json b/guacamole/src/main/frontend/package.json index 99d742dcf9..6afbf861d8 100644 --- a/guacamole/src/main/frontend/package.json +++ b/guacamole/src/main/frontend/package.json @@ -13,6 +13,7 @@ "angular-translate-loader-static-files": "^2.19.0", "blob-polyfill": ">=7.0.20220408", "csv": "^6.2.5", + "d3": "^7.8.5", "datalist-polyfill": "^1.25.1", "file-saver": "^2.0.5", "fuzzysort": "^2.0.4", diff --git a/guacamole/src/main/frontend/src/app/player/directives/player.js b/guacamole/src/main/frontend/src/app/player/directives/player.js index a239e0d2bf..a377750c26 100644 --- a/guacamole/src/main/frontend/src/app/player/directives/player.js +++ b/guacamole/src/main/frontend/src/app/player/directives/player.js @@ -79,6 +79,7 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay // Required services const keyEventDisplayService = $injector.get('keyEventDisplayService'); + const playerHeatmapService= $injector.get('playerHeatmapService'); const playerTimeService = $injector.get('playerTimeService'); const config = { @@ -161,6 +162,57 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay */ $scope.showKeyLog = false; + /** + * The height, in pixels, of the SVG heatmap paths. Note that this is not + * necessarily the actual rendered height, just the initial size of the + * SVG path before any styling is applied. + */ + $scope.HEATMAP_HEIGHT = 100; + + /** + * The width, in pixels, of the SVG heatmap paths. Note that this is not + * necessarily the actual rendered width, just the initial size of the + * SVG path before any styling is applied. + */ + $scope.HEATMAP_WIDTH = 1000; + + /** + * The maximum number of key events per millisecond to display in the + * key event heatmap. Any key event rates exceeding this value will be + * capped at this rate to ensure that unsually large spikes don't make + * swamp the rest of the data. + * + * Note: This is 6 keys per second (events include both presses and + * releases) - equivalent to ~88 words per minute typed. + */ + const KEY_EVENT_RATE_CAP = 12 / 1000; + + /** + * The maximum number of frames per millisecond to display in the + * frame heatmap. Any frame rates exceeding this value will be + * capped at this rate to ensure that unsually large spikes don't make + * swamp the rest of the data. + */ + const FRAME_RATE_CAP = 10 / 1000; + + /** + * An SVG path describing a smoothed curve that visualizes the relative + * number of frames rendered throughout the recording - i.e. a heatmap + * of screen updates. + * + * @type {String} + */ + $scope.frameHeatmap = ''; + + /** + * An SVG path describing a smoothed curve that visualizes the relative + * number of key events recorded throughout the recording - i.e. a + * heatmap of key events. + * + * @type {String} + */ + $scope.keyHeatmap = ''; + /** * Whether a seek request is currently in progress. A seek request is * in progress if the user is attempting to change the current playback @@ -179,6 +231,22 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay */ var resumeAfterSeekRequest = false; + /** + * The recording-relative timestamp of each frame of the recording that + * has been processed so far. + * + * @type {Number[]} + */ + var frameTimestamps = []; + + /** + * The recording-relative timestamp of each text event that has been + * processed so far. + * + * @type {Number[]} + */ + var keyTimestamps = []; + /** * Return true if any batches of key event logs are available for this * recording, or false otherwise. @@ -321,11 +389,25 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay // Begin downloading the recording $scope.recording.connect(); - // Notify listeners when the recording is completely loaded + // Notify listeners and set any heatmap paths + // when the recording is completely loaded $scope.recording.onload = function recordingLoaded() { $scope.operationMessage = null; $scope.$emit('guacPlayerLoaded'); $scope.$evalAsync(); + + const recordingDuration = $scope.recording.getDuration(); + + // Generate heat maps for rendered frames and typed text + $scope.frameHeatmap = ( + playerHeatmapService.generateHeatmapPath( + frameTimestamps, recordingDuration, FRAME_RATE_CAP, + $scope.HEATMAP_HEIGHT, $scope.HEATMAP_WIDTH)); + $scope.keyHeatmap = ( + playerHeatmapService.generateHeatmapPath( + keyTimestamps, recordingDuration, KEY_EVENT_RATE_CAP, + $scope.HEATMAP_HEIGHT, $scope.HEATMAP_WIDTH)); + }; // Notify listeners if an error occurs @@ -341,6 +423,9 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay $scope.operationProgress = src.size ? current / src.size : 0; $scope.$emit('guacPlayerProgress', duration, current); $scope.$evalAsync(); + + // Store the timestamp of the just-received frame + frameTimestamps.push(duration); }; // Notify listeners when playback has started/resumed @@ -362,6 +447,8 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay $scope.textBatches = ( keyEventDisplayService.parseEvents(events)); + keyTimestamps = events.map(event => event.timestamp); + }; // Notify listeners when current position within the recording diff --git a/guacamole/src/main/frontend/src/app/player/services/playerHeatmapService.js b/guacamole/src/main/frontend/src/app/player/services/playerHeatmapService.js new file mode 100644 index 0000000000..1149e9d066 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/services/playerHeatmapService.js @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * 'License'); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { curveCatmullRom } from 'd3-shape'; +import { path } from 'd3-path'; + +/** + * A service for generating heat maps of activity levels per time interval, + * for session recording playback. + */ +angular.module('player').factory('playerHeatmapService', [() => { + + /** + * A default, relatively-gentle Gaussian smoothing kernel. This kernel + * should help heatmaps look a bit less jagged, while not reducing fidelity + * very much. + * + * @type {Number[]} + */ + const GAUSSIAN_KERNEL = [0.0013, 0.1573, 0.6827, 0.1573, 0.0013]; + + /** + * The number of buckets that a series of activity timestamps should be + * divided into. + * + * @type {Number} + */ + const NUM_BUCKETS = 100; + + /** + * Given a list of values to smooth out, produce a smoothed data set with + * the same length as the original provided list. + * + * @param {!Number[]} values + * The list of histogram values to smooth out. + * + * @returns + * The smoothed value array. + */ + function smooth(values) { + + // The starting offset into the values array for each calculation + const lookBack = Math.floor(GAUSSIAN_KERNEL.length / 2); + + // Apply the smoothing kernel to each value in the provided array + return _.map(values, (value, index) => { + + // Total up the weighted values for each position in the kernel + return _.reduce(GAUSSIAN_KERNEL, (total, weight, kernelIndex) => { + + // The offset into the original values array for the kernel + const valuesOffset = kernelIndex - lookBack; + + // The position inside the original values array to be included + const valuesIndex = index + valuesOffset; + + // If the contribution to the final smoothed value would be outside + // the bounds of the array, just use the original value instead + const contribution = ((valuesIndex >= 0) && valuesIndex < values.length) + ? values[valuesIndex] : value; + + // Use the provided weight from the kernel and add to the total + return total + (contribution * weight); + + }, 0); + + }); + } + + /** + * Given an array of values, with each value representing an activity count + * during a bucket of time, generate a smooth curve, scaled to PATH_HEIGHT + * height, and PATH_WIDTH width. + * + * @param {!Number[]} bucketizedData + * The bucketized counts to create an SVG path from. + * + * @param {!Number} maxBucketValue + * The size of the largest value in the bucketized data. + * + * @param {!Number} height + * The target height, in pixels, of the highest point in the heatmap. + * + * @param {!Number} width + * The target width, in pixels, of the heatmap. + * + * @returns + * An SVG path representing a smooth curve, passing through all points + * in the provided data. + */ + function createPath(bucketizedData, maxBucketValue, height, width) { + + // Calculate scaling factor to ensure that paths are all the same heigh + const yScalingFactor = height / maxBucketValue; + + // Scale a given Y value appropriately + const scaleYValue = yValue => height - (yValue * yScalingFactor); + + // Calculate scaling factor to ensure that paths are all the same width + const xScalingFactor = width / bucketizedData.length; + + // Construct a continuous curved path + const curvedPath = path(); + const curve = curveCatmullRom(curvedPath); + + curve.lineStart(); + + // Add all the data points + for (let i = 0; i < bucketizedData.length; i++) { + + // Scale up the x value to hit the target width + const x = xScalingFactor * i; + + // Scale and invert the height for display + const y = scaleYValue(bucketizedData[i]); + + // Add the scaled point + curve.point(x, y); + + } + + // Move back to 0 to complete the path + curve.lineEnd(); + curvedPath.lineTo(width, scaleYValue(0)); + + // Generate the SVG path for this curve + const rawPathText = curvedPath.toString(); + + // The SVG path as generated by D3 starts with a move to the first data + // point. This means that when the path ends and the subpath is closed, + // it returns to the position of the first data point instead of the + // origin. To fix this, the initial move command is removed, and the + // path is amended to start at the origin. TODO: Find a better way to + // handle this. + const startAtOrigin = ( + + // Start at origin + 'M0,' + scaleYValue(0) + + + // Line to the first point in the curve, to close the shape + 'L0,' + scaleYValue(bucketizedData[0]) + + ); + + // Strip off the first move command from the path + const strippedPathText = _.replace(rawPathText, /^[^C]*/, ''); + + return startAtOrigin + strippedPathText; + } + + const service = {}; + + /** + * Given a raw array of timestamps indicating when events of a certain type + * occured during a record, generate and return a smoothed SVG path + * indicating how many events occured during each equal-length bucket. + * + * @param {!Number[]} timestamps + * A raw array of timestamps, one for every relevant event. These + * must be monotonically increasing. + * + * @param {!Number} duration + * The duration over which the heatmap should apply. This value may + * be greater than the maximum timestamp value, in which case the path + * will drop to 0 after the last timestamp in the provided array. + * + * @param {Number} maxRate + * The maximum number of events per millisecond that should be displayed + * in the final path. Any rates over this amount will just be capped at + * this value. + * + * @param {!Number} height + * The target height, in pixels, of the highest point in the heatmap. + * + * @param {!Number} width + * The target width, in pixels, of the heatmap. + * + * @returns + * A smoothed, graphable SVG path representing levels of activity over + * time, as extracted from the provided timestamps. + */ + service.generateHeatmapPath = (timestamps, duration, maxRate, height, width) => { + + // The height and width must both be valid in order to create the path + if (!height || !width) { + console.warn("Heatmap height and width must be positive."); + return ''; + } + + // If no timestamps are available, no path can be created + if (!timestamps || !timestamps.length) + return ''; + + // An initially empty array containing no activity in any bucket + const buckets = new Array(NUM_BUCKETS).fill(0); + + // If no events occured, return the an empty path + if (!timestamps.length) + return ''; + + // Determine the bucket granularity + const bucketDuration = duration / NUM_BUCKETS; + + // The rate-limited maximum number of events that any bucket can have, + const maxPossibleBucketValue = Math.floor(bucketDuration * maxRate); + + // If the duration is invalid, return the still-empty array + if (duration <= 0) + return ''; + + let maxBucketValue = 0; + + // Partition the events into a count of events per bucket + let currentBucketIndex = 0; + timestamps.forEach(timestamp => { + + // If the current timestamp has passed the end of the current + // bucket, move to the appropriate bucket + if (timestamp >= (currentBucketIndex + 1) * bucketDuration) + currentBucketIndex = Math.min( + Math.floor((timestamp / bucketDuration)), NUM_BUCKETS - 1); + + // Do not record events that exceed the maximum allowable rate + if (buckets[currentBucketIndex] >= maxPossibleBucketValue) + buckets[currentBucketIndex] = maxPossibleBucketValue; + + else + // Increment the count for the current bucket + buckets[currentBucketIndex]++; + + // Keep track of the maximum value seen so far + maxBucketValue = Math.max( + maxBucketValue, buckets[currentBucketIndex]); + + }); + + // Smooth the data for better aesthetics before creating the path + const smoothed = smooth(buckets); + + // Create an SVG path based on the smoothed data + return createPath(smoothed, maxBucketValue, height, width); + + } + + + return service; + +}]); diff --git a/guacamole/src/main/frontend/src/app/player/templates/player.html b/guacamole/src/main/frontend/src/app/player/templates/player.html index eeb3744b54..f002bd42ca 100644 --- a/guacamole/src/main/frontend/src/app/player/templates/player.html +++ b/guacamole/src/main/frontend/src/app/player/templates/player.html @@ -28,6 +28,21 @@ ng-model="playbackPosition" ng-on-change="commitSeekRequest()"> +
+ + + + + + + + +
+ {{ 'PLAYER.INFO_FRAME_EVENTS_LEGEND' | translate }} + {{ 'PLAYER.INFO_KEY_EVENTS_LEGEND' | translate }} +
+
+
diff --git a/guacamole/src/main/frontend/src/app/settings/styles/history-player.css b/guacamole/src/main/frontend/src/app/settings/styles/history-player.css index 7503cc8c30..9d3b3cfdaa 100644 --- a/guacamole/src/main/frontend/src/app/settings/styles/history-player.css +++ b/guacamole/src/main/frontend/src/app/settings/styles/history-player.css @@ -112,3 +112,76 @@ -o-transition-delay: 0s; transition-delay: 0s; } + +.settings.connectionHistoryPlayer .guac-player-controls .heat-map { + position: absolute; + width: 100%; +} + +.settings.connectionHistoryPlayer .guac-player-controls .heat-map svg { + position: absolute; + bottom: 7px; + height: 50px; + width: 100%; + z-index: 100; + pointer-events: none; + opacity: 0; + -webkit-transition: opacity 0.1s linear 0.1s; + -moz-transition: opacity 0.1s linear 0.1s; + -o-transition: opacity 0.1s linear 0.1s; + transition: opacity 0.1s linear 0.1s; +} + +.settings.connectionHistoryPlayer .guac-player-controls:hover .heat-map svg { + opacity: 0.5; +} + +.settings.connectionHistoryPlayer .guac-player-controls .heat-map .legend { + position: absolute; + display: flex; + flex-direction: column; + align-items: end; + bottom: 65px; + right: 10px; + z-index: 100; + opacity: 0; + -webkit-transition: opacity 0.1s linear 0.1s; + -moz-transition: opacity 0.1s linear 0.1s; + -o-transition: opacity 0.1s linear 0.1s; + transition: opacity 0.1s linear 0.1s; +} + +.settings.connectionHistoryPlayer .guac-player-controls:hover .heat-map .legend { + opacity: 1; +} + +.settings.connectionHistoryPlayer .guac-player-controls .heat-map .legend .key-events::after, +.settings.connectionHistoryPlayer .guac-player-controls .heat-map .legend .frame-events::after { + display: inline-block; + content: ''; + width: 25px; + height: 10px; + margin-left: 3px; + margin-right: 10px; + border-radius: 3px; +} + +.settings.connectionHistoryPlayer .guac-player-controls .heat-map .legend .key-events::after { + background-color: #5BA300; +} + +.settings.connectionHistoryPlayer .guac-player-controls .heat-map .legend .frame-events::after { + background-color: #FFFFFF; +} + +.settings.connectionHistoryPlayer .heat-map svg.key-events { + + /* Convert to #5BA300 color */ + filter: invert(69%) sepia(80%) saturate(5092%) hue-rotate(54deg) brightness(96%) contrast(101%); +} + +.settings.connectionHistoryPlayer .heat-map svg.frame-events { + + /* Convert to #FFFFFF color */ + filter: invert(100%) sepia(0%) saturate(0%) hue-rotate(93deg) brightness(103%) contrast(103%); +} diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 4997439f5c..e3423d7245 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -483,10 +483,12 @@ "ACTION_PLAY" : "@:APP.ACTION_PLAY", "ACTION_SHOW_KEY_LOG" : "Keystroke Log", - "INFO_LOADING_RECORDING" : "Your recording is now being loaded. Please wait...", - "INFO_NO_KEY_LOG" : "Keystroke Log Unavailable", - "INFO_NUMBER_OF_RESULTS" : "{RESULTS} {RESULTS, plural, one{Match} other{Matches}}", - "INFO_SEEK_IN_PROGRESS" : "Seeking to the requested position. Please wait...", + "INFO_FRAME_EVENTS_LEGEND" : "On-screen Activity", + "INFO_KEY_EVENTS_LEGEND" : "Keyboard Activity", + "INFO_LOADING_RECORDING" : "Your recording is now being loaded. Please wait...", + "INFO_NO_KEY_LOG" : "Keystroke Log Unavailable", + "INFO_NUMBER_OF_RESULTS" : "{RESULTS} {RESULTS, plural, one{Match} other{Matches}}", + "INFO_SEEK_IN_PROGRESS" : "Seeking to the requested position. Please wait...", "FIELD_PLACEHOLDER_TEXT_BATCH_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER"