12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088 |
- {% extends "defaults.jinja" %}
- {% block base %}
- <!DOCTYPE html>
- <html lang="en">
- <head>
- {% block head %}
- <title>{% block title %}{% endblock %} - {{ FLEXMEASURES_PLATFORM_NAME }}
- </title>
- {% endblock head %}
- <meta charset="windows-1252">
- {% if FLEXMEASURES_ENFORCE_SECURE_CONTENT_POLICY %}
- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
- {% endif %}
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <link rel="icon" href="/favicon.ico" type="image/x-icon" />
- <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
- {% block styles %}
- <style>
- :root {
- --primary-color: {{ primary_color }};
- --primary-border-color: {{ primary_border_color }};
- --primary-hover-color: {{ primary_hover_color}};
- --primary-transparent: {{ primary_transparent }};
- --secondary-color: {{ secondary_color }};
- --secondary-hover-color: {{ secondary_hover_color }};
- --secondary-transparent: {{ secondary_transparent }};
- --white: #FFF;
- --black: #000;
- --light-gray: #eeeeee;
- --gray: #bbb;
- --red: #c21431;
- --green: #14c231;
- /* colors by function */
- --nav-default-color: var(--white);
- --nav-default-background-color: var(--primary-color);
- --nav-hover-color: var(--secondary-hover-color);
- --nav-hover-background-color: var(--primary-hover-color);
- --nav-open-color: var(--secondary-color);
- --nav-open-background-color: var(--primary-color);
- --nav-current-color: var(--black);
- --nav-current-background-color: var(--secondary-color);
- --nav-current-hover-color: var(--black);
- --nav-current-hover-background-color: var(--secondary-hover-color);
- --create-color: var(--green);
- --delete-color: var(--red);
- }
- #asset_audit_log.nav-on-click tr {
- cursor: default;
- }
- #account_audit_log.nav-on-click tr {
- cursor: default;
- }
- #user_audit_log.nav-on-click tr {
- cursor: default;
- }
- </style>
- <!-- Leaflet -->
- <link href="https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet.css" rel="stylesheet" />
- <link href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css" rel="stylesheet" />
- <link href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css" rel="stylesheet" />
- <!-- Latest Bootstrap link-->
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
- <!-- Fonts -->
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
- rel="stylesheet" />
- <link href="{{ url_for('flexmeasures_ui.static', filename='css/external/weather-icons.min.css') }}"
- rel="stylesheet" />
- <!-- Ion range slider -->
- <link href="https://cdn.jsdelivr.net/npm/ion-rangeslider@2.3.1/css/ion.rangeSlider.css"
- rel="stylesheet" />
- <!-- Custom CSS -->
- <link href="{{ url_for('flexmeasures_ui.static', filename='css/flexmeasures.css') }}?v={{ flexmeasures_version }}" rel="stylesheet" />
- {% if active_page == "tasks" %}
- <link href="{{ url_for('rq_dashboard.static', filename='css/main.css') }}" rel="stylesheet">
- <link href="{{ url_for('flexmeasures_ui.static', filename='css/external/rq-dashboard-bootstrap.min.css') }}" rel="stylesheet" />
- {% elif active_page in ("assets", "users", "accounts") %}
- <link href="https://cdn.datatables.net/1.10.22/css/jquery.dataTables.min.css" rel="stylesheet" />
- <link href="https://cdn.datatables.net/buttons/3.0.2/css/buttons.bootstrap5.min.css" rel="stylesheet"/>
- {% endif %}
- {% if extra_css %}
- <link href="{{ extra_css }}" rel="stylesheet" />
- {% endif %}
- {% endblock %}
- </head>
- <body class="d-flex flex-column min-vh-100">
- {% block body %}
- {% block nav %}
- <nav class="navbar navbar-expand-lg fixed-top mt-0 navbar-default mb-2 navbar-fixed-top " id="topnavbar">
- <div class="container-fluid" id="navbar-container">
-
-
- <div class="navbar-brand">
- <a href="/">
- <span class="navbar-tool-name">
- {% if menu_logo %}
- <img id="navbar-logo" src="{{menu_logo}}"/>
- {% else %}
- <img id="navbar-logo" src="https://artwork.lfenergy.org/projects/flexmeasures/horizontal/white/flexmeasures-horizontal-white.svg" alt="FlexMeasures"/>
- {% endif %}
- </span>
- </a>
- </div>
- <div
- class="navbar-toggler border-0"
- type="button"
- data-bs-toggle="collapse"
- data-bs-target="#navbarSupportedContent"
- aria-controls="navbarSupportedContent"
- aria-expanded="false"
- aria-label="Toggle navigation"
- >
- <i class="fa fa-bars"></i>
- </div>
-
- <div class="collapse navbar-collapse justify-content-end flex-row" id="navbarSupportedContent">
- <ul class="nav navbar-nav d-flex">
- {% for href, id, caption, tooltip, icon in navigation_bar %}
- {% if id == "tasks" %}
- <li {% if id == active_page %} class="nav-item dropdown active" {% else %} class="nav-item dropdown" {% endif %}>
- <a class="nav-link dropdown-toggle" href="#" id="tasksDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
- <span class="fa fa-tasks" aria-hidden="true"></span>
- Tasks
- </a>
- <ul class="dropdown-menu" aria-labelledby="tasksDropdown">
- {% for queue in queue_names %}
- <li {% if current_user.has_role('anonymous') %} class="disabled" {% endif %}>
- <a class="dropdown-item" {% if not current_user.has_role('anonymous') %} href="/tasks/0/view/jobs/{{ queue }}/started/10/asc/1" {% endif %}>
- {{ queue | capitalize }}
- </a>
- </li>
- {% endfor %}
- <li {% if current_user.has_role('anonymous') %} class="disabled" {% endif %}>
- <a class="dropdown-item" {% if not current_user.has_role('anonymous') %} href="/tasks/" {% endif %}>
- Overview
- </a>
- </li>
- </ul>
- </li>
- {% else %}
- <li {% if id == active_page %} class="nav-item active" {% else %} class="nav-item" {% endif %}
- data-bs-toggle="tooltip" title="{{ tooltip }}" data-placement="bottom"
- {% if id == 'upload' or (current_user.has_role('anonymous') and id in ('tasks', 'users')) %}
- class="disabled" {% endif %}>
- <a {% if not ( id == 'upload' or (current_user.has_role('anonymous') and id in ('tasks', 'users')) ) %} href="/{{ href|e }}" {% endif %}
- {% if id == 'docs' %} target="_blank" {% endif %} class="nav-link">
- <span class="fa fa-{{ icon }}" aria-hidden="true"></span>
- {{ caption|e }}
- {# use tooltip as caption for small screens showing a collapsed menu #}
- {% if not caption %}
- <span class="d-inline d-lg-none">{{ tooltip }}</span>
- {% endif %}
- </a>
- </li>
- {% endif %}
- {% endfor %}
- </ul>
- </div>
- </div>
- </nav>
- {% endblock nav %}
- <div
- class="position-fixed bottom-0 end-0 p-3"
- id="toast-container"
- role="alert"
- aria-live="assertive"
- aria-atomic="true"
- style="z-index: 1000000"
- >
- <button id="close-all-toasts" class="btn">Close All</button>
- </div>
- {% if message and message != "" %}
- <div class="col-md-12 alert alert-info">{{ message}} </div>
- {% endif %}
- {% if (msg is defined) and msg %}
- <div class="col-md-12 alert alert-info">{{ msg }}</div>
- {% endif %}
- <!-- loading this earlier so templates can use it -->
- <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.5.1.min.js"></script>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/floatthead/2.2.1/jquery.floatThead.min.js"></script>
- {# Div blocks that child pages can reference #}
- {% block divs %}
- {% block breadcrumbs %}
- {% if breadcrumb_info is defined %}
- <nav aria-label="breadcrumb ">
- <ol class="breadcrumb p-2">
- {% for breadcrumb in breadcrumb_info["ancestors"] %}
- <li class="breadcrumb-item{% if loop.last %} dropdown active{% endif %}" {% if loop.last %}aria-current="page"{% endif %}>
- {% if breadcrumb["url"] is not none and not loop.last %}
- <a href="{{ breadcrumb['url'] }}">{{ breadcrumb['name'] }}</a>
- {% elif breadcrumb["url"] is none %}
- {{ breadcrumb['name'] }}
- {% else %}
- {% if breadcrumb_info["siblings"]|length > 0 %}
- <a href="{{ breadcrumb['url'] }}" class="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" role="button">{{ breadcrumb['name'] }}</a>
- <ul class="dropdown-menu">
- {% for sibling in breadcrumb_info["siblings"] %}
- <li><a class="p-3 dropdown-item {% if sibling['name'] == breadcrumb['name'] %}active{% endif %}" href="{{ sibling['url'] }}">{{ sibling["name"] }}</a></li>
- {% endfor %}
- </ul>
- {% else %}
- <a href="{{ breadcrumb['url'] }}" role="button">{{ breadcrumb['name'] }}</a>
- {% endif %}
- {% endif %}
- </li>
- {% endfor %}
- {% if breadcrumb_info["views"]|length > 0 %}
- <li class="breadcrumb-item dropdown active">
- <a href="#" class="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" role="button">{{ breadcrumb_info["current_asset_view"] }}</a>
- <ul class="dropdown-menu">
- {% for view in breadcrumb_info["views"] %}
- <li><a class="p-3 dropdown-item {% if view['name'] == breadcrumb_info['current_asset_view'] %}active{% endif %}" href="{{ view['url'] }}">{{ view["name"] }}</a></li>
- {% endfor %}
- </ul>
- </li>
- <div class="form-check form-switch" style="margin-left: auto;">
- <input class="form-check-input" type="checkbox" {% if breadcrumb_info['current_asset_view'] == session.get("default_asset_view") %} checked {% endif %} id="defaultAssetView" onchange="setDefaultAssetView(this, '{{ breadcrumb_info["current_asset_view"] }}')">
- <label class="form-check-label" for="defaultAssetView">Set as my default asset view</label>
- </div>
- {% endif %}
- </ol>
- </nav>
- {% endif %}
- {% endblock %}
- {% block forecastpicker %}
- <div class="form-group row">
- <div class="col-md-2">
- <div class="col-md-1"><i class="icon-binoculars center-icon"></i></div>
- </div>
- <div class="col-md-10">
- <label class="control-label">Forecast (rolling)</label>
- <!--<div class="btn-group btn-group-justified" role="group" aria-label="...">-->
- <!--<a role="button" href="#" class="btn btn-default forecast-toggle active" forecast-type="rolling">Rolling</a>-->
- <!--<a role="button" href="#" class="btn btn-default forecast-toggle" forecast-type="static">Static</a>-->
- <!--</div>-->
- <div>
- <form action="" method="POST" id="forecast_horizon_form">
- <select class="form-control" id="forecast_horizon" name="forecast_horizon"
- onchange="this.form.submit()">
- {% for horizon in forecast_horizons %}
- <option value="{{ horizon }}" {% if horizon == active_forecast_horizon %} selected="selected"
- {% endif %}>with a horizon of {{ horizon }}</option>
- {% endfor %}
- </select>
- </form>
- </div>
- </div>
- </div>
- {% endblock forecastpicker %}
- {% block leftsidepanel %}
- <script>
- // Set up swiping and clicking for the left sidepanel
- var leftSidepanels = document.getElementsByClassName('left-sidepanel');
- var leftSidepanelLabels = document.getElementsByClassName('left-sidepanel-label');
- async function openSidepanel(e) {
- if ( (e.target.classList.contains('sidepanel-container')) | (e.type == 'click') ) {
- for (var i = leftSidepanels.length - 1; i >= 0; i--) {
- leftSidepanels[i].classList.add('sidepanel-show');
- }
- }
- }
- async function closeSidepanel(e) {
- for (var i = leftSidepanels.length - 1; i >= 0; i--) {
- leftSidepanels[i].classList.remove('sidepanel-show');
- }
- }
- for (var i = leftSidepanelLabels.length - 1; i >= 0; i--) {
- leftSidepanelLabels[i].addEventListener("click", openSidepanel);
- }
- document.addEventListener('swiped-right', openSidepanel);
- document.addEventListener('swiped-left', closeSidepanel);
- </script>
- {% endblock leftsidepanel %}
- {% block sensorChartSetup %}
- <!-- Render Charts -->
- <script>
- // Define picker as a global variable so that other code on the page can access it, e.g. get the times
- var picker;
- </script>
- <script type="module" type="text/javascript">
- // Import local js (the FM version is used for cache-busting, causing the browser to fetch the updated version from the server)
- import { getUniqueValues, convertToCSV } from "{{ url_for('flexmeasures_ui.static', filename='js/data-utils.js') }}?v={{ flexmeasures_version }}";
- import { subtract, thisMonth, lastNMonths, countDSTTransitions, getOffsetBetweenTimezonesForDate } from "{{ url_for('flexmeasures_ui.static', filename='js/daterange-utils.js') }}?v={{ flexmeasures_version }}";
- import { partition, updateBeliefs, beliefTimedelta, setAbortableTimeout} from "{{ url_for('flexmeasures_ui.static', filename='js/replay-utils.js') }}?v={{ flexmeasures_version }}";
- let vegaView;
- let previousResult;
- let queryStartDate;
- let queryEndDate;
- let storeStartDate;
- let storeEndDate;
- {% if event_starts_after and event_ends_before %}
- storeStartDate = new Date('{{ event_starts_after }}');
- storeEndDate = new Date('{{ event_ends_before }}');
- checkDSTTransitions(storeStartDate, storeEndDate);
- {% endif %}
- let replaySpeed = 100
- let chartType = '{{ chart_type }}'; // initial chart type from session variable
- // Update chart type picker: active state, reload data event
- document.addEventListener('DOMContentLoaded', function() {
- var dropdownItems = document.querySelectorAll('#chart-type-picker .dropdown-item');
- for (var i = 0; i < dropdownItems.length; i++) {
- // Set initial chart type
- if (dropdownItems[i].getAttribute('data-chart-type') === chartType) {
- dropdownItems[i].classList.add('active');
- }
- // Add event listener
- dropdownItems[i].addEventListener('click', function(e) {
- e.preventDefault();
- chartType = this.getAttribute('data-chart-type');
- // Update the active state of the dropdown items
- var dropdownItems = document.querySelectorAll('.dropdown-item');
- dropdownItems.forEach(item => {
- if (item === this) {
- item.classList.add('active');
- } else {
- item.classList.remove('active');
- }
- });
- // Reload daterange
- picker.setDateRange(picker.getStartDate(), picker.getEndDate());
- });
- }
- });
- function checkDSTTransitions(startDate, endDate) {
- var numDSTTransitions = countDSTTransitions(startDate, endDate, 90)
- if (numDSTTransitions != 0) {
- document.getElementById('dstwarn').style.display = 'block';
- if (numDSTTransitions == 1) {
- document.getElementById('dstwarn').innerHTML = 'Please note that the sensor data you are viewing includes a daylight saving time (DST) transition.';
- } else {
- document.getElementById('dstwarn').innerHTML = 'Please note that the sensor data you are viewing includes ' + numDSTTransitions + ' daylight saving time (DST) transitions.';
- }
- }
- else {
- document.getElementById('dstwarn').style.display = 'none';
- }
- }
- function checkSourceMasking(data) {
- var sourceWarn = document.getElementById("sourcewarn");
- if (sourceWarn == null)
- return
- var uniqueSourceIds = getUniqueValues(data, 'source.id');
- if (chartType == 'daily_heatmap' && uniqueSourceIds.length > 1) {
- sourceWarn.style.display = 'block';
- sourceWarn.innerHTML = 'Please note that only data from the most prevalent source is shown.';
- }
- else {
- sourceWarn.style.display = 'none';
- }
- }
- async function embedAndLoad(chartSpecsPath, elementId, datasetName, previousResult, startDate, endDate) {
- await vegaEmbed('#'+elementId, chartSpecsPath + 'dataset_name=' + datasetName + '&combine_legend='+ combineLegend + '&width=container&include_sensor_annotations=false&include_asset_annotations=false&chart_type=' + chartType, {{ chart_options | safe }})
- .then(function (result) {
- // Create a custom menu item for exporting to CSV
- const exportToCSVAction = document.createElement('a');
- exportToCSVAction.id = 'exportToCSVAction';
- exportToCSVAction.href = '#';
- exportToCSVAction.download = datasetName + '.csv';
- exportToCSVAction.textContent = 'Save as CSV';
- exportToCSVAction.addEventListener('mousedown', async function (event) {
- event.preventDefault();
- const chartData = vegaView.data(datasetName);
- const csvContent = convertToCSV(chartData);
- const encodedUri = encodeURI(csvContent);
- exportToCSVAction.href = encodedUri;
- });
- // Append menu item to chart actions (hamburger menu)
- const vegaActions = document.querySelector('.vega-actions');
- if (vegaActions) {
- vegaActions.appendChild(exportToCSVAction);
- } else {
- console.log('Warning: CSV export functionality is not available, because no div with class=vega-actions was found to append export action to');
- }
- // result.view is the Vega View, chartSpecsPath is the original Vega-Lite specification
- vegaView = result.view;
- if (previousResult) {
- checkSourceMasking(previousResult);
- var slicedPreviousResult = previousResult.filter(item => {
- return item.event_start >= startDate.getTime() && item.event_start < endDate.getTime();
- })
- vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(slicedPreviousResult)).resize().run();
- }
- });
- }
- // event listener to refresh graph
- document.addEventListener('sensorsToShowUpdated', async function() {
- await embedAndLoad(chartSpecsPath + 'event_starts_after=' + storeStartDate.toISOString() + '&event_ends_before=' + storeEndDate.toISOString() + '&', elementId, datasetName, previousResult, storeStartDate, storeEndDate);
- })
- var combineLegend = 'true';
- {% if active_page == "assets" %}
- var dataPath = '/api/v3_0/assets/' + {{ asset.id }};
- var dataDevPath = '/api/dev/asset/' + {{ asset.id }};
- var datasetName = 'asset_' + {{ asset.id }};
- {% set total_sensors = asset.sensors_to_show | map(attribute='sensors') | map('length') | sum %}
- {% if total_sensors > 7 %}
- combineLegend = 'false';
- {% endif %}
- {% elif active_page == "sensors" %}
- var dataPath = '/api/dev/sensor/' + {{ sensor.id }};
- var dataDevPath = '/api/dev/sensor/' + {{ sensor.id }};
- var datasetName = 'sensor_' + {{ sensor.id }};
- {% endif %}
- var elementId = 'sensorchart';
- var chartSpecsPath = dataPath + '/chart?';
- // Set up abort controller to cancel requests
- var controller = new AbortController();
- var signal = controller.signal;
- const initialData = fetch(dataPath + '/chart_data?event_starts_after=' + '{{ event_starts_after }}' + '&event_ends_before=' + '{{ event_ends_before }}', {
- method: "GET",
- headers: {"Content-Type": "application/json"},
- signal: signal,
- })
- .then(function(response) { return response.json(); });
- // Set session start and end
- {% if event_starts_after and event_ends_before %}
- var sessionStart = new Date('{{ event_starts_after }}');
- var sessionEnd = new Date('{{ event_ends_before }}');
- sessionEnd.setSeconds(sessionEnd.getSeconds() - 1); // -1 second in case most recent event ends at midnight
- sessionStart.setHours(0,0,0,0); // get start of first day
- sessionEnd.setHours(0,0,0,0); // get start of last day
- {% else %}
- var sessionStart = null
- var sessionEnd = null
- {% endif %}
- // Create date range picker and the logic for a new date range selection (mostly, fetching data and displaying the chart for it)
- const date = Date();
- picker = new Litepicker({
- element: document.getElementById('datepicker'),
- plugins: ['ranges', 'keyboardnav'],
- ranges: {
- customRanges: {
- 'Today': [new Date(date), new Date(date)],
- 'Last 7 days': [subtract(date, 6), new Date(date)],
- 'This month': thisMonth(date)
- },
- position: 'bottom'
- },
- autoRefresh: true,
- moduleRanges: true,
- showWeekNumbers: true,
- numberOfMonths: 1,
- numberOfColumns: 1,
- inlineMode: true,
- switchingMonths: 1,
- singleMode: false,
- startDate: sessionStart,
- endDate: sessionEnd,
- dropdowns: {
- years: true,
- months: true,
- },
- format: 'YYYY-MM-DD\\T00:00:00',
- });
- picker.on('selected', (startDate, endDate) => {
- startDate = startDate.toJSDate();
- endDate = endDate.toJSDate();
- endDate.setDate(endDate.getDate() + 1);
- storeStartDate = startDate;
- storeEndDate = endDate;
- var queryStartDate = (startDate != null) ? (startDate.toISOString()) : (null);
- var queryEndDate = (endDate != null) ? (endDate.toISOString()) : (null);
- stopReplay()
- $("#spinner").show();
- checkDSTTransitions(startDate, endDate)
- Promise.all([
- // Fetch time series data
- fetch(dataPath + '/chart_data?event_starts_after=' + queryStartDate + '&event_ends_before=' + queryEndDate, {
- method: "GET",
- headers: {"Content-Type": "application/json"},
- signal: signal,
- })
- .then(function(response) { return response.json(); }),
- /**
- // Fetch annotations
- fetch(dataPath + '/chart_annotations?event_starts_after=' + queryStartDate + '&event_ends_before=' + queryEndDate, {
- method: "GET",
- headers: {"Content-Type": "application/json"},
- signal: signal,
- })
- .then(function(response) { return response.json(); }),
- */
- // Embed chart
- embedAndLoad(chartSpecsPath + 'event_starts_after=' + queryStartDate + '&event_ends_before=' + queryEndDate + '&', elementId, datasetName, previousResult, startDate, endDate),
- ]).then(function(result) {
- $("#spinner").hide();
- vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(result[0])).resize().run();
- previousResult = result[0];
- checkSourceMasking(previousResult);
- /**
- vegaView.change(datasetName + '_annotations', vega.changeset().remove(vega.truthy).insert(result[1])).resize().run();
- */
- }).catch(console.error);
- });
- /** If the page is done loading, get data (for the time range stored in the session, or the default range),
- * check for time zone differences, and then trigger the time range picker to display the correct range.
- */
- document.onreadystatechange = async () => {
- if (document.readyState === 'complete') {
- {% if event_starts_after and event_ends_before %}
- // Initialize picker to the date selection specified in the session
- $("#spinner").show();
- let fetchedInitialData = await Promise.all([
- initialData,
- embedAndLoad(chartSpecsPath + 'event_starts_after=' + '{{ event_starts_after }}' + '&event_ends_before=' + '{{ event_ends_before }}' + '&', elementId, datasetName, previousResult, sessionStart, sessionEnd),
- ]).then(function (result) {return result[0]}).catch(console.error);
- $("#spinner").hide();
- vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(fetchedInitialData)).resize().run();
- previousResult = fetchedInitialData;
- checkSourceMasking(previousResult);
- var timerangeNotSetYet = false
- {% else %}
- var timerangeNotSetYet = true
- {% endif %}
- fetch(dataDevPath, {
- method: "GET",
- headers: {"Content-Type": "application/json"},
- })
- .then(function(response) { return response.json(); })
- .then(function(data) {
- var offsetDifference = getOffsetBetweenTimezonesForDate(new Date(), data.timezone, jstz.determine().name());
- if (offsetDifference != 0) {
- document.getElementById('tzwarn').style.display = 'block';
- var offsetNotice = (offsetDifference > 0) ? 'which is currently ahead by ' + offsetDifference + ' minutes' : 'which is currently behind by ' + offsetDifference + ' minutes';
- document.getElementById('tzwarn').innerHTML = 'Please note that the sensor data you are viewing is located in a different timezone (' + offsetNotice + ').<br/>To view the data from a local perspective, set your locale timezone to ' + data.timezone + '.';
- }
- {% if active_page == "assets" %}
- var timerangeVar = 'timerange_of_sensors_to_show';
- {% else %}
- var timerangeVar = 'timerange';
- {% endif %}
- if (timerangeVar in data) {
- var start = new Date(data[timerangeVar].start);
- var end = new Date(data[timerangeVar].end);
- end.setSeconds(end.getSeconds() - 1); // -1 second in case most recent event ends at midnight
- start.setHours(0,0,0,0); // get start of first day
- end.setHours(0,0,0,0); // get start of last day
- if (timerangeNotSetYet) {
- // Initialize picker to the last 2 days of sensor data
- var nearEnd = new Date(end)//.setDate(end.getDate() - 1);
- nearEnd.setDate(nearEnd.getDate() - 1);
- picker.setDateRange(
- nearEnd,
- end,
- );
- }
-
- // No use looking for data in years outside timerange of sensor data
- picker.setOptions({
- dropdowns: {
- minYear: start.getFullYear(),
- maxYear: end.getFullYear(),
- },
- });
-
- // Persist the range on the datepicker UI so correct range is shown when the user opens the datepicker
- var end_time = '{{event_ends_before}}';
- var new_end_date = new Date(end_time);
- // Adjusting the date by deducting one day to account for timezone discrepancies.
- new_end_date.setDate(new_end_date.getDate() - 1);
- var start_time = '{{event_starts_after}}';
- var new_start_date = new Date(start_time);
- picker.setDateRange(decodeURIComponent(toIsoString(new_start_date)), decodeURIComponent(toIsoString(new_end_date)));
- };
- });
- }
- };
- // Set up play/pause button for replay, incl. the complete replay logic
- let toggle = document.querySelector('#replay');
- toggle.addEventListener('click', function(e) {
- e.preventDefault();
- toggleReplay();
- });
- window.addEventListener('keypress', function(e) {
- // Do nothing if typing in an input field or textarea
- if (e.target.tagName.toLowerCase() === 'input' || e.target.tagName.toLowerCase() === 'textarea') {
- return;
- }
- // Start/pause/resume replay with 'p'
- if (e.key==='p') {
- toggleReplay();
- } else if (e.key==='s') {
- stopReplay();
- }
- }, false);
- function toggleReplay() {
- if (toggle.classList.contains('stopped')) {
- startReplay();
- } else if (toggle.classList.contains('playing')) {
- pauseReplay();
- } else {
- resumeReplay();
- }
- }
- function pauseReplay() {
- toggle.classList.remove('playing');
- toggle.classList.add('paused');
- }
- function resumeReplay() {
- toggle.classList.remove('paused');
- toggle.classList.add('playing');
- }
- async function stopReplay() {
- if (toggle.classList.contains('stopped')) {
- return;
- }
- toggle.classList.remove('playing');
- toggle.classList.remove('paused');
- toggle.classList.add('stopped');
- // Abort previous request and create abort controller for new request
- controller.abort();
- controller = new AbortController();
- signal = controller.signal;
- // Remove replay ruler and replay time
- vegaView.change('replay', vega.changeset().remove(vega.truthy).insert({'belief_time': null})).run().finalize();
- document.getElementById('replay-time').innerHTML = '';
- // Show previous results
- $("#spinner").show();
- await embedAndLoad(chartSpecsPath + 'event_starts_after=' + storeStartDate.toISOString() + '&event_ends_before=' + storeEndDate.toISOString() + '&', elementId, datasetName, previousResult, storeStartDate, storeEndDate);
- $("#spinner").hide();
- }
- function startReplay() {
- toggle.classList.remove('stopped');
- toggle.classList.add('playing');
- var beliefTime = new Date(storeStartDate);
- var numReplaySteps = Math.ceil((storeEndDate - storeStartDate) / beliefTimedelta);
- queryStartDate = (storeStartDate != null) ? (storeStartDate.toISOString()) : (null);
- queryEndDate = (storeEndDate != null) ? (storeEndDate.toISOString()) : (null);
- $("#spinner").show();
- Promise.all([
- // Fetch time series data (all data, not only the most recent beliefs)
- fetch(dataPath + '/chart_data?event_starts_after=' + queryStartDate + '&event_ends_before=' + queryEndDate + '&most_recent_beliefs_only=false', {
- method: "GET",
- headers: {"Content-Type": "application/json"},
- signal: signal,
- })
- .then(function(response) { return response.json(); }),
- ]).then(function(result) {
- $("#spinner").hide();
- replayBeliefsData(result[0]);
- }).catch(console.error);
- const timer = ms => new Promise(res => setAbortableTimeout(res, Math.max(ms, 0), signal));
- /**
- * Replays beliefs data.
- *
- * As we go forward in time in steps, replayedData is updated with newData that was known at beliefTime,
- * by splitting off newData from remainingData.
- * Then, replayedData is loaded into the chart.
- *
- * @param {Array} remainingData Array containing beliefs.
- */
- async function replayBeliefsData (remainingData) {
- var replayedData = [];
- for (var beliefTime = new Date(storeStartDate); beliefTime <= storeEndDate; beliefTime = new Date(beliefTime.getTime() + beliefTimedelta)) {
- while (document.getElementById('replay').classList.contains('paused') ) {
- await timer(1000);
- }
- if (document.getElementById('replay').classList.contains('stopped') ) {
- break;
- }
- var s = performance.now();
- // Split off one replay step of new data from the remaining data
- var newData;
- [newData, remainingData] = partition(
- remainingData,
- (item) => item.belief_time <= beliefTime.getTime(),
- );
- // Update beliefs in the replayed data given the new data
- replayedData = updateBeliefs(replayedData, newData);
- /** When selecting a longer time periode (more than a week), the replay slows down a bit. This
- * seems to be mainly from reloading the data into the graph. Slicing the data takes 10-30 ms, and
- * loading that data into the graph takes 30-200 ms, depending on how much data is shown in the
- * graph. After trying different approaches, we fell back to the original approach of telling vega
- * to remove all previous data and to insert a completely new dataset at each iteration. Updating
- * the view with removing only a few data points (representing obsolete beliefs) and inserting only
- * a few data points (representing the most recent new beliefs) actually made it slower.
- */
- vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(replayedData));
- vegaView.change('replay', vega.changeset().remove(vega.truthy).insert({'belief_time': beliefTime}));
- vegaView.run().finalize();
- document.getElementById('replay-time').innerHTML = beliefTime;
- // Approximate constant speed
- var e = performance.now();
- var throttle = e - s;
- await timer(replaySpeed - throttle);
- }
- // Stop replay when finished
- stopReplay()
- }
- }
- </script>
- {% endblock sensorChartSetup %}
- {% block attributions %}
- <div id="att-text" style="display: none;">
- <ul>
- <li>Plots made with <a href="https://vega.github.io/vega-lite/">Vega-Lite</a>.</li>
- <li>Icons made by <a href="https://freepik.com">Freepik</a>, <a
- href="https://www.flaticon.com/authors/tomas-knop" title="Tomas Knop">Tomas Knop</a>, <a
- href="https://www.flaticon.com/authors/gregor-cresnar" title="Gregor Cresnar">Gregor Cresnar</a> and
- <a href="https://www.flaticon.com/authors/those-icons" title="Those Icons">Those Icons</a> from <a
- href="https://flaticon.com">www.flaticon.com</a>.</li>
- </ul>
- </div>
- {% endblock attributions %}
- {% endblock divs %}
- {#- Scripts used by all views (e.g. by navigation menu) -#}
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
- <script src="https://cdn.datatables.net/2.0.8/js/dataTables.min.js"></script>
- <script src="https://cdn.datatables.net/2.0.8/js/dataTables.bootstrap5.min.js"></script>
- <!-- Add the class touched to body when the user uses any touch event. -->
- <script>
- document.body.addEventListener('touchstart', function() {
- document.body.classList.add('touched');
- });
- </script>
- {% block scripts %}
- <!-- JS to enable tooltips -->
- <script type='text/javascript'>
- $(document).ready(function () {
- if ($("[rel=tooltip]").length) {
- $("[rel=tooltip]").tooltip();
- }
- });
- </script>
- <script>
- // Set default asset view
- function setDefaultAssetView(checkbox, view_name) {
- // Get the checked status of the checkbox
- const isChecked = checkbox.checked;
- const apiBasePath = window.location.origin;
- fetch(apiBasePath + '/api/v3_0/assets/default_asset_view', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': '{{ csrf_token }}',
- },
- body: JSON.stringify({
- default_asset_view: view_name,
- use_as_default: isChecked
- })
- })
- .then(response => response.json())
- .catch(error => {
- console.error('Error during API call:', error);
- });
- }
- </script>
- <!-- JS to enable toast -->
- <script defer>
- const toastStack = document.getElementById("toast-container");
- const closeToastBtn = document.getElementById("close-all-toasts");
-
- // written like this since this script(order) is ontop of the main script
- document.addEventListener("DOMContentLoaded", function () {
- initiateToastCloseBtn(closeToastBtn);
- });
- function initiateToastCloseBtn(closeToastBtn){
- // hide button
- closeToastBtn.style.display = "none";
- closeToastBtn.addEventListener("click", function () {
- const toastElements = document.querySelectorAll(".toast");
- toastElements.forEach((toast) => {
- const toastInstance = bootstrap.Toast.getInstance(toast); // Get the toast instance
- if (toastInstance) {
- // destroy the toast
- toastInstance.dispose();
- toast.remove();
- }
- });
- // Hide the close button
- closeToastBtn.style.display = "none";
- });
- }
- function showAllToasts() {
- const toastElements = document.querySelectorAll(".toast");
- toastElements.forEach((toast) => {
- const toastInstance = new bootstrap.Toast(toast);
- toastInstance.show();
- });
- }
- function showToast(message, type) {
- let colorClass;
- let colorStyle = "";
- let title;
-
- // Determine the type of toast
- if (type == "error") {
- colorClass = "bg-danger";
- title = "Error";
- } else if (type == "success") {
- colorClass = "bg-success";
- title = "Success";
- } else {
- colorStyle =
- "background-color: {{ primary_color }};";
- title = "Info";
- }
-
- // Create the toast HTML
- const toast = document.createElement("div");
- toast.classList.add("toast", "mb-1");
- toast.setAttribute("data-bs-autohide", "true");
- toast.setAttribute("role", "alert");
- toast.setAttribute("aria-live", "assertive");
- toast.setAttribute("aria-atomic", "true");
-
- toast.innerHTML = `
- <div class="toast-header">
- <div class="rounded me-2 ${colorClass}" style="width: 20px; height: 20px; display: inline-block; ${colorStyle}"></div>
- <strong class="me-auto">${title}</strong>
- <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
- </div>
- <div class="toast-body">
- ${message}
- </div>
- `;
-
- // Append toast to the toast stack
- toastStack.insertAdjacentElement("afterbegin", toast);
-
- // Show the close all button
- closeToastBtn.style.display = "block";
-
- showAllToasts();
- // destroy only this toast if the close(X) button is clicked
- toast.querySelector(".btn-close").addEventListener("click", function () {
- const toastInstance = new bootstrap.Toast(toast);
- toastInstance.dispose();
- toast.remove();
- });
- }
- </script>
- <!-- External scripts -->
- <script src="https://cdn.jsdelivr.net/npm/ion-rangeslider@2.3.1/js/ion.rangeSlider.min.js"></script>
- <script src="https://d3js.org/d3.v6.min.js"></script>
- {% if js_versions %}
- <script src="https://cdn.jsdelivr.net/npm/vega@{{ js_versions.vega }}"></script>
- <script src="https://cdn.jsdelivr.net/npm/vega-lite@{{ js_versions.vegalite }}"></script>
- <script src="https://cdn.jsdelivr.net/npm/vega-embed@{{ js_versions.vegaembed }}"></script>
- {# Workaround for loading a NodeJS module without NodeJS #}
- <script>var module = { exports: {} };</script>
- <script src="https://cdn.jsdelivr.net/npm/currency-symbol-map@{{ js_versions.currencysymbolmap }}/map.js"></script>
- <script>const currencySymbolMap = module.exports;</script>
- {% endif %}
- <!-- Custom scripts -->
- <script src="{{ url_for('flexmeasures_ui.static', filename='js/swiped-events.min.js') }}"></script>
- <script src="{{ url_for('flexmeasures_ui.static', filename='js/flexmeasures.js') }}?v={{ flexmeasures_version }}"></script>
- {% endblock scripts %}
- {% block paginate_tables_script %}
- <script src="https://cdn.datatables.net/1.10.22/js/jquery.dataTables.min.js"></script>
- <script
- src="https://cdn.datatables.net/plug-ins/1.10.22/features/conditionalPaging/dataTables.conditionalPaging.js"></script>
- {% endblock paginate_tables_script %}
- <footer class="page-footer font-small pt-4 mt-auto">
- <div class="footer text-center">
- <div class="container-fluid">
- {% block copyright_notice %}
- FlexMeasures technology is created by <a href="https://seita.nl/">Seita Energy Flexibility</a>,
- in cooperation with <a href="https://aoneeng.com/">A1 Engineering</a>
- ©
- <script>var CurrentYear = new Date().getFullYear(); document.write(CurrentYear)</script>.
- {% endblock copyright_notice %}
- {% block about %}
- <a href="#" data-bs-toggle="modal" data-bs-target="#About">About FlexMeasures</a>.
- <!-- The modal -->
- <div class="modal fade" id="About" tabindex="-1" role="dialog" aria-labelledby="modalLabelLarge" aria-hidden="true">
- <div class="modal-dialog modal-lg">
- <div class="modal-content">
- <div class="modal-header">
- <h4 class="modal-title" id="modalLabelLarge">About FlexMeasures</h4>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- <div class="justify">
- <p>
- In a world with renewable energy, flexibility is crucial for cost and CO₂ reduction.
- Planning ahead allows flexible devices to profit from scheduling the best flexible
- actions (such as shifting or curtailing energy use).
- </p>
- <p>
- The {{ FLEXMEASURES_PLATFORM_NAME }} Platform is a tool for businesses operating energy devices.
- Its purpose is to realise the best value for device owners by scheduling balancing actions.
- For instance, if the devices draw from or supply to the power grid, {{ FLEXMEASURES_PLATFORM_NAME }} can assist in
- selling balancing services to energy markets.
- It fulfills this purpose with three services: Monitoring, forecasting and scheduling.
- {{ FLEXMEASURES_PLATFORM_NAME }} is designed as open-source software to empower energy service companies while they
- maintain autonomy over their operations and their technology roadmap.
- Read more <a href="https://flexmeasures.readthedocs.io">here</a>.
- </p>
- <p>
- {{ FLEXMEASURES_PLATFORM_NAME }} is compliant with the Universal Smart Energy Framework (<a
- href="https://www.usef.energy/download-the-framework/a-flexibility-market-design/">USEF</a>), a
- “flexibility market design for the trading and commoditisation of energy flexibility and the
- architecture, tools and rules to make it work effectively.
- USEF fits on top of most market models and is already being adopted across Europe to accelerate and
- future-proof smart energy projects.”
- </p>
- {% if current_user.has_role('anonymous') and current_user.has_role('CPO') %}
- <p>
- This demo is tuned to operators of charge points for electric vehicles (EVs).
- The terminology used follows the Open Charge Point Interface (<a
- href="https://ocpi-protocol.org/">OCPI</a>).
- This dashboard shows you the locations of all charge points connected to the platform and how they
- are doing.
- Each charge point contains multiple chargers—called Electric Vehicle Supply Equipment (EVSE)
- in OCPI—which are monitored individually (click on a charge point location to reveal its
- EVSE).
- </p>
- <p>
- {{ FLEXMEASURES_PLATFORM_NAME }} supports automated scheduling of charging profiles through its API.
- Charging profiles are optimised against applicable market conditions like wholesale prices and
- contracted (time-of-use) tariffs.
- Charging preferences can be set to reflect whether EV owners are in a hurry or parking for some
- time.
- </p>
- {% endif %}
- </div>
- </div>
-
- </div>
- </div>
- </div>
- {% endblock about %}
- {% block credits %}
- <a href="#" data-bs-toggle="modal" data-bs-target="#Credits">Credits</a>.
- <!-- The modal -->
- <div class="modal fade" id="Credits" tabindex="-1" role="dialog" aria-labelledby="modalLabelLarge" aria-hidden="true">
- <div class="modal-dialog modal-lg">
- <div class="modal-content">
- <div class="modal-header">
- <h4 class="modal-title" id="modalLabelLarge">Credits</h4>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- <div class="row">
- <div class="col-md-6">
- <h3>Images from <a href="https://seita.nl">Seita</a></h3>
- <div>FlexMeasures on laptop - image by <a href="https://drakemultimedia.nl/" title="Bobby Drake">Bobby Drake</a></div>
- <h3>Images from <a href="https://unsplash.com/" title="Unsplash">Unsplash</a></h3>
- <div>Tesla charging station - image by <a href="https://www.chasealewis.com/" title="Chase Lewis">Chase Lewis</a></div>
- <div>Wind turbines on top of mountain - image by <a href="https://www.tbk-f.com/" title="TJ K.">TJ K.</a></div>
- </div>
- <div class="col-md-6">
- <h3>Icons from <a href="https://www.flaticon.com/" title="Flaticon">Flaticon</a></h3>
- <div><a href="https://www.flaticon.com/free-icon/wind-power_1085695">Wind turbine</a> - icon by <a href="https://www.flaticon.com/authors/good-ware" title="Good Ware">Good Ware</a> (made to spin by Seita)</div>
- <div><a href="https://www.flaticon.com/search?word=currency&author_id=258">Coins</a> - icons by <a href="https://www.flaticon.com/authors/pixelmeetup" title="Pixelmeetup">Pixelmeetup</a></div>
- <div>Other icons by <a href="https://www.freepik.com" title="Freepik">Freepik</a></div>
- </div>
- </div>
- </div>
- <div class="modal-header">
- We did our best to give credit to the original photographers and illustrators for the images included on this website. If we have not properly credited an original photographer or illustrator, please notifiy us and we will make the appropriate changes necessary.
- </div>
- </div>
- </div>
- </div>
- {% endblock credits %}
- {% block technical_info %}
- {# We might render templates from plugins, where this meta info is not available #}
- {% if app_running_since %}
- This app is running since {{ app_running_since }}
- {% endif %}
- {% if (flexmeasures_version or git_version) and not current_user.has_role('anonymous') %}
- {% if flexmeasures_version %}
- on version {{ flexmeasures_version }}.
- {% else %}
- {% if git_version != "Unknown" %}
- on version {{ git_version }}+{{ git_commits_since }}.
- {% else %}
- on revision {{ git_hash }}.
- {% endif %}
- {% endif %}
- {% endif %}
- {% if loaded_plugins %}
- Loaded plugins: {{ loaded_plugins }}.
- {% endif %}
- {% endblock technical_info %}
- </div>
- {% block logo %}
- <div>
- <a href="https://flexmeasures.io/"><img src="https://artwork.lfenergy.org/projects/flexmeasures/icon/white/flexmeasures-icon-white.svg" alt="FlexMeasures" class="footer-logo"></a>
- </div>
- {% endblock logo %}
- </div>
- </footer>
- {% endblock body %}
- </body>
- </html>
- {% endblock base %}
|