flexmeasures.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. $(document).ready(function () {
  2. ready();
  3. });
  4. $(window).resize(function () {
  5. $('body').css('padding-top', $("#navbar-fixed-top").height());
  6. $('.floatThead-container').css('top', $("#navbar-container").height() - $('#topnavbar').height());
  7. $('.floatThead-container').css('margin-top', $("#navbar-container").height() - $('#topnavbar').height());
  8. });
  9. $(window).scroll(function () {
  10. $('.floatThead-container').css('top', $("#navbar-container").height() - $('#topnavbar').height());
  11. });
  12. var offshoreOrdered = false;
  13. var batteryOrdered = false;
  14. function showMsg(msg) {
  15. $("#msgModal .modal-content").html(msg);
  16. $("#msgModal").modal("show");
  17. }
  18. function showImage(resource, action, value) {
  19. // $("#expectedValueModal .modal-dialog .modal-content img").html("static/control-mock-imgs/" + resource + "-action" + action + "-" + value + "MW.png")
  20. document.getElementById('expected_value_mock').src = "ui/static/control-mock-imgs/value-" + resource + "-action" + action + "-" + value + "MW.png"
  21. load_images = document.getElementsByClassName('expected_load_mock')
  22. for (var i = 0; i < load_images.length; i++) {
  23. load_images[i].src = "ui/static/control-mock-imgs/load-" + resource + "-action" + action + "-" + value + "MW.png"
  24. }
  25. }
  26. function defaultImage(action) {
  27. load_images = document.getElementsByClassName('expected_load_mock reset_default')
  28. for (var i = 0; i < load_images.length; i++) {
  29. load_images[i].src = "ui/static/control-mock-imgs/load-action" + action + ".png"
  30. }
  31. }
  32. function clickableTable(element, urlColumn) {
  33. // This will keep actions like text selection or dragging functional
  34. var table = $(element).DataTable();
  35. var tbody = element.getElementsByTagName('tbody')[0];
  36. var startX, startY;
  37. var radiusLimit = 0; // how much the mouse is allowed to move during clicking
  38. $(tbody).on({
  39. mousedown: function (event) {
  40. startX = event.pageX;
  41. startY = event.pageY;
  42. },
  43. mouseup: function (event) {
  44. var endX = event.pageX;
  45. var endY = event.pageY;
  46. var deltaX = endX - startX;
  47. var deltaY = endY - startY;
  48. var euclidean = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
  49. if (euclidean <= radiusLimit) {
  50. var columnIndex = table.column(':contains(' + urlColumn + ')').index();
  51. var data = table.row(this).data();
  52. if(Array.isArray(data)){
  53. var url = data[columnIndex];
  54. } else{
  55. var url = data["url"];
  56. }
  57. handleClick(event, url);
  58. }
  59. }
  60. }, 'tr');
  61. }
  62. function handleClick(event, url) {
  63. // ignore clicks on <a href>, <button> or <input> elements
  64. if (['a', 'button', 'input'].includes(event.target.tagName.toLowerCase())) {
  65. return
  66. } else if (event.ctrlKey) {
  67. window.open(url, "_blank");
  68. } else {
  69. window.open(url, "_self");
  70. }
  71. }
  72. function ready() {
  73. console.log("ready...");
  74. // For custom hover effects that linger for some time
  75. $("i").hover(
  76. function () {
  77. $(this).addClass('over');
  78. },
  79. function () {
  80. $(this).delay(3000).queue(function (next) {
  81. $(this).removeClass('over');
  82. next();
  83. });
  84. }
  85. );
  86. // Table pagination
  87. $.extend(true, $.fn.dataTable.defaults, {
  88. "conditionalPaging": {
  89. style: 'fade',
  90. speed: 2000,
  91. },
  92. "searching": true,
  93. "ordering": true,
  94. "info": true,
  95. "order": [],
  96. "lengthMenu": [[5, 10, 25, 50, 100, -1], [5, 10, 25, 50, 100, "All"]],
  97. "pageLength": 10, // initial page length
  98. "oLanguage": {
  99. "sLengthMenu": "Show _MENU_ records",
  100. "sSearch": "Filter records:",
  101. "sInfo": "Showing _START_ to _END_ out of _TOTAL_ records",
  102. "sInfoFiltered": "(filtered from _MAX_ total records)",
  103. "sInfoEmpty": "No records to show",
  104. },
  105. "columnDefs": [{
  106. "targets": 'no-sort',
  107. "orderable": false,
  108. }],
  109. "stateSave": true,
  110. });
  111. // just searching and ordering, no paging
  112. $('.paginate-without-paging').DataTable({
  113. "paging": false,
  114. });
  115. // searching, ordering and paging
  116. $('.paginate').DataTable();
  117. // set default page lengths
  118. $('.paginate-5').dataTable().api().page.len(5).draw();
  119. $('.paginate-10').dataTable().api().page.len(10).draw();
  120. // Tables with the nav-on-click class
  121. navTables = document.getElementsByClassName('nav-on-click');
  122. Array.prototype.forEach.call(navTables, function(t) {clickableTable(t, 'URL')});
  123. // Sliders
  124. $('#control-action-setting-offshore')
  125. .ionRangeSlider({
  126. skin: "big",
  127. type: "single",
  128. grid: true,
  129. grid_snap: true,
  130. min: 0,
  131. max: 5,
  132. from_min: 2,
  133. from_max: 3,
  134. from_shadow: true,
  135. postfix: "MW",
  136. force_edges: true,
  137. onChange: function (settingData) {
  138. action = 1;
  139. if (offshoreOrdered) {
  140. action = 2;
  141. }
  142. value = settingData.from;
  143. $("#control-expected-value-offshore").html(numberWithCommas(value * 35000));
  144. showImage("offshore", action, value);
  145. }
  146. });
  147. $('#control-action-setting-battery').ionRangeSlider({
  148. skin: "big",
  149. type: "single",
  150. grid: true,
  151. grid_snap: true,
  152. min: 0,
  153. max: 5,
  154. from_min: 1,
  155. from_max: 2,
  156. from_shadow: true,
  157. postfix: "MW",
  158. force_edges: true,
  159. onChange: function (settingData) {
  160. action = 1;
  161. if (offshoreOrdered) {
  162. action = 2;
  163. }
  164. value = settingData.from;
  165. $("#control-expected-value-battery").html(numberWithCommas(value * 10000));
  166. showImage("battery", action, value);
  167. }
  168. });
  169. // Hover behaviour
  170. $("#control-tr-offshore").mouseenter(function (data) {
  171. action = 1;
  172. if (offshoreOrdered) {
  173. action = 2;
  174. }
  175. var value = $("#control-action-setting-offshore").data("ionRangeSlider").old_from;
  176. showImage("offshore", action, value);
  177. }).mouseleave(function (data) {
  178. action = 1;
  179. if (offshoreOrdered) {
  180. action = 2;
  181. }
  182. defaultImage(action);
  183. });
  184. $("#control-tr-battery").mouseenter(function (data) {
  185. action = 1;
  186. if (offshoreOrdered) {
  187. action = 2;
  188. }
  189. var value = $("#control-action-setting-battery").data("ionRangeSlider").old_from;
  190. showImage("battery", action, value);
  191. }).mouseleave(function (data) {
  192. action = 1;
  193. if (offshoreOrdered) {
  194. action = 2;
  195. }
  196. defaultImage(action);
  197. });
  198. // Navbar behaviour
  199. $(document.body).css('padding-top', $('#topnavbar').height());
  200. $(window).resize(function () {
  201. $(document.body).css('padding-top', $('#topnavbar').height());
  202. });
  203. // Table behaviour
  204. $('table').floatThead({
  205. position: 'absolute',
  206. top: $('#topnavbar').height(),
  207. scrollContainer: true
  208. });
  209. $(document).on('change', '#user-list-options input[name="include_inactive"]', function () {
  210. //Users list inactive
  211. $(this).closest('form').submit();
  212. })
  213. // Security messages styling
  214. $('.flashes').addClass('alert alert-info');
  215. // Check button behaviour
  216. $("#control-check-expected-value-offshore").click(function (data) {
  217. var value = $("#control-action-setting-offshore").data("ionRangeSlider").old_from;
  218. showImage("offshore", 1, value);
  219. $("#expectedValueModal").modal("show");
  220. });
  221. $("#control-check-expected-value-battery").click(function (data) {
  222. var value = $("#control-action-setting-battery").data("ionRangeSlider").old_from;
  223. action = 1;
  224. if (offshoreOrdered) {
  225. action = 2;
  226. }
  227. showImage("battery", action, value);
  228. $("#expectedValueModal").modal("show");
  229. });
  230. // Order button behaviour
  231. $("#control-order-button-ev").click(function (data) {
  232. showMsg("This action is not supported in this mockup.");
  233. });
  234. $("#control-order-button-onshore").click(function (data) {
  235. showMsg("This action is not supported in this mockup.");
  236. });
  237. $("#control-order-button-offshore").click(function (data) {
  238. if (offshoreOrdered) {
  239. showMsg("This action is not supported in this mockup.");
  240. }
  241. var value = $("#control-action-setting-offshore").data("ionRangeSlider").old_from;
  242. console.log("Offshore was ordered for " + value + "MW!");
  243. if (value == 2) {
  244. showMsg("Your order of " + value + "MW offshore wind curtailment will be processed!");
  245. $("#control-tr-offshore").addClass("active");
  246. $("#control-offshore-volume").html("Ordered: <b>2MW</b>");
  247. $("#control-order-button-offshore").html('<span class="fa fa-minus" aria-hidden="true"></span> Cancel').removeClass("btn-success").addClass("btn-danger");
  248. $("#control-check-expected-value-offshore").hide();
  249. $("#total_load").html("4.4");
  250. $("#total_value").html("230,000");
  251. offshoreOrdered = true;
  252. }
  253. else {
  254. showMsg("In this mockup, only ordering 2MW of offshore wind is supported.");
  255. }
  256. });
  257. $("#control-order-button-battery").click(function (data) {
  258. if (batteryOrdered) {
  259. showMsg("This action is not supported in this mockup.");
  260. }
  261. else if (!offshoreOrdered) {
  262. showMsg("In this mockup, please first order 2MW of offshore wind.");
  263. } else {
  264. var value = $("#control-action-setting-battery").data("ionRangeSlider").old_from;
  265. console.log("Battery was ordered for " + value + "MW!");
  266. showMsg("Your order of " + value + "MW battery shifting will be processed!");
  267. $("#control-tr-battery").addClass("active");
  268. $("#control-order-button-battery").html('<span class="fa fa-minus" aria-hidden="true"></span> Cancel').removeClass("btn-success").addClass("btn-danger");
  269. $("#control-check-expected-value-battery").hide();
  270. if (value == 1) {
  271. $("#control-battery-volume").html("Ordered: <b>1MW</b>");
  272. $("#total_load").html("5.4");
  273. $("#total_value").html("240,000");
  274. }
  275. else {
  276. $("#control-battery-volume").html("Ordered: <b>2MW</b>");
  277. $("#total_load").html("6.4");
  278. $("#total_value").html("250,000");
  279. }
  280. batteryOrdered = true;
  281. }
  282. });
  283. // activate tooltips
  284. $('[data-toggle="tooltip"]').tooltip();
  285. }
  286. const numberWithCommas = (x) => {
  287. var parts = x.toString().split(".");
  288. parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  289. return parts.join(".");
  290. }
  291. /** Analytics: Submit the resource selector, but reload to a clean URL,
  292. without any existing resource selection (confusing)
  293. */
  294. var empty_location = location.protocol + "//" + location.hostname + ":" + location.port + "/analytics";
  295. function submit_resource() {
  296. $("#resource-form").attr("action", empty_location).submit();
  297. }
  298. function submit_market() {
  299. $("#market-form").attr("action", empty_location).submit();
  300. }
  301. function submit_sensor_type() {
  302. $("#sensor_type-form").attr("action", empty_location).submit();
  303. }
  304. /** Tooltips: Register custom formatters */
  305. /* Quantities incl. units
  306. * Usage:
  307. * {
  308. * 'format': [<d3-format>, <sensor unit>, <optional preference to show currency symbol instead of currency code>],
  309. * 'formatType': 'quantityWithUnitFormat'
  310. * }
  311. * The use of currency symbols, such as the euro sign (€), should be reserved for use in graphics.
  312. * See, for example, https://publications.europa.eu/code/en/en-370303.htm
  313. * The rationale behind this is that they are often ambiguous.
  314. * For example, both the Australian dollar (AUD) and the United States dollar (USD) map to the dollar sign ($).
  315. */
  316. vega.expressionFunction('quantityWithUnitFormat', function(datum, params) {
  317. const formatDef = {
  318. "decimal": ".",
  319. "thousands": " ",
  320. "grouping": [3],
  321. };
  322. const locale = d3.formatLocale(formatDef);
  323. // The third element on param allows choosing to show the currency symbol (true) or the currency name (false)
  324. if (params.length > 2 && params[2] === true){
  325. return locale.format(params[0])(datum) + " " + convertCurrencyCodeToSymbol(params[1]);
  326. }
  327. else {
  328. return locale.format(params[0])(datum) + " " + params[1];
  329. }
  330. });
  331. /*
  332. * Timedeltas measured in human-readable quantities (usually not milliseconds)
  333. * Usage:
  334. * {
  335. * 'format': [<d3-format>, <breakpoint>],
  336. * 'formatType': 'timedeltaFormat'
  337. * }
  338. * <d3-format> is a d3 format identifier, e.g. 'd' for decimal notation, rounded to integer.
  339. * See https://github.com/d3/d3-format for more details.
  340. * <breakpoint> is a scalar that decides the breakpoint from one duration unit to the next larger unit.
  341. * For example, a breakpoint of 4 means we format 4 days as '4 days', but 3.96 days as '95 hours'.
  342. */
  343. vega.expressionFunction('timedeltaFormat', function(timedelta, params) {
  344. return (Math.abs(timedelta) > 1000 * 60 * 60 * 24 * 365.2425 * params[1] ? d3.format(params[0])(timedelta / (1000 * 60 * 60 * 24 * 365.2425)) + " years"
  345. : Math.abs(timedelta) > 1000 * 60 * 60 * 24 * params[1] ? d3.format(params[0])(timedelta / (1000 * 60 * 60 * 24)) + " days"
  346. : Math.abs(timedelta) > 1000 * 60 * 60 * params[1] ? d3.format(params[0])(timedelta / (1000 * 60 * 60)) + " hours"
  347. : Math.abs(timedelta) > 1000 * 60 * params[1] ? d3.format(params[0])(timedelta / (1000 * 60)) + " minutes"
  348. : Math.abs(timedelta) > 1000 * params[1] ? d3.format(params[0])(timedelta / 1000) + " seconds"
  349. : d3.format(params[0])(timedelta) + " milliseconds");
  350. });
  351. /*
  352. * Timezone offset including IANA timezone name
  353. * Usage:
  354. * {
  355. * 'format': [<IANA timezone name, e.g. 'Europe/Amsterdam'>],
  356. * 'formatType': 'timezoneFormat'
  357. * }
  358. */
  359. vega.expressionFunction('timezoneFormat', function(date, params) {
  360. const timezoneString = params[0];
  361. const tzOffsetNumber = date.getTimezoneOffset();
  362. const tzDate = new Date(0,0,0,0,Math.abs(tzOffsetNumber));
  363. return `${ tzOffsetNumber > 0 ? '-' : '+'}${("" + tzDate.getHours()).padStart(2, '0')}:${("" + tzDate.getMinutes()).padStart(2, '0')}` + ' (' + timezoneString + ')';
  364. });
  365. /*
  366. * Convert any currency codes in the unit to currency symbols.
  367. * This relies on the currencyToSymbolMap imported from currency-symbol-map/map.js
  368. */
  369. const convertCurrencyCodeToSymbol = (unit) => {
  370. return replaceMultiple(unit, currencySymbolMap);
  371. };
  372. /**
  373. * Replaces multiple substrings in a given string based on a provided mapping object.
  374. *
  375. * @param {string} str - The input string in which replacements will be performed.
  376. * @param {Object} mapObj - An object where keys are substrings to be replaced, and values are their corresponding replacements.
  377. * @returns {string} - A new string with the specified substitutions applied.
  378. *
  379. * @example
  380. * // Replace currency codes with symbols in the given string
  381. * const inputString = "The price is 50 EUR/MWh, and 30 AUD/MWh.";
  382. * const currencyMapping = { EUR: '€', AUD: '$' };
  383. * const result = replace_multiple(inputString, currencyMapping);
  384. * // The result will be "The price is 50 €/MWh, and 30 $/MWh."
  385. */
  386. function replaceMultiple(str, mapObj){
  387. // Create a regular expression pattern using the keys of the mapObj joined with "|" (OR) to match any of the substrings.
  388. let regex = new RegExp(Object.keys(mapObj).join("|"),"g");
  389. // Use the regular expression to replace matched substrings with their corresponding values from the mapObj.
  390. // The "g" flag makes the replacement global (replaces all occurrences), and it is case-sensitive by default.
  391. return str.replace(regex, matched => mapObj[matched]);
  392. }
  393. function getTimeAgo(timestamp) {
  394. /**
  395. * Converts a timestamp into a human-readable "time ago" format.
  396. *
  397. * @param {number} timestamp - The timestamp in milliseconds to convert.
  398. * @returns {string} A string representing how much time has passed since the given timestamp,
  399. * formatted as "X seconds ago", "X minutes ago", "X hours ago", or "X days ago".
  400. */
  401. const now = Date.now();
  402. const diffInSeconds = Math.floor((now - timestamp) / 1000); // Difference in seconds
  403. if (diffInSeconds < 60) {
  404. return `${diffInSeconds} seconds ago`;
  405. } else if (diffInSeconds < 3600) {
  406. const minutes = Math.floor(diffInSeconds / 60);
  407. return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
  408. } else if (diffInSeconds < 86400) {
  409. const hours = Math.floor(diffInSeconds / 3600);
  410. return `${hours} hour${hours > 1 ? 's' : ''} ago`;
  411. } else {
  412. const days = Math.floor(diffInSeconds / 86400);
  413. return `${days} day${days > 1 ? 's' : ''} ago`;
  414. }
  415. }
  416. // Function to return a loading row for a table
  417. function getLoadingRow(id="loading-row") {
  418. const loading_row = `
  419. <tr id="${id}">
  420. <td colspan="5" class="text-center">
  421. <i class="fa fa-spinner fa-spin"></i> Loading...
  422. </td>
  423. </tr>
  424. `;
  425. return loading_row;
  426. }