diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index 5b72d337..69f2576a 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -55,6 +55,8 @@ class FormData: infected_start: minutes_since_midnight location: str location_coordinates: str + weather_station_location: str + weather_station_ref: str mask_type: str mask_wearing_option: str mechanical_ventilation_type: str @@ -105,8 +107,10 @@ class FormData: 'infected_lunch_start': '12:30', 'infected_people': _NO_DEFAULT, 'infected_start': '08:30', - 'location': _NO_DEFAULT, + 'location': 'Geneva', 'location_coordinates': '(46.2044, 6.1432)', + 'weather_station_location':'GENEVA COINTRIN', + 'weather_station_ref': '067000-99999', 'mask_type': 'Type I', 'mask_wearing_option': 'mask_off', 'mechanical_ventilation_type': 'not-applicable', @@ -264,9 +268,16 @@ class FormData: datetime_object = datetime.datetime.strptime(self.event_month[:3], "%b") month = datetime_object.month + #set location + self.weather_station_location = data.location_to_weather_stn(self.location_coordinates)[1] + print('wstn resolve location_', data.location_to_weather_stn(self.location_coordinates)[1]) + data.local_temperatures = data.location_celcius_per_hour(self.location_coordinates) + print('inputcoords_', self.location_coordinates) inside_temp = models.PiecewiseConstant((0, 24), (293,)) outside_temp = data.Temperatures[str(month)] + + ventilation: models.Ventilation if self.window_type == 'window_sliding': ventilation = models.SlidingWindow( diff --git a/cara/apps/calculator/static/js/form.js b/cara/apps/calculator/static/js/form.js new file mode 100644 index 00000000..f9587f97 --- /dev/null +++ b/cara/apps/calculator/static/js/form.js @@ -0,0 +1,628 @@ +/* -------HTML structure------- */ +function getChildElement(elem) { + // Get the element named in the given element's data-enables attribute. + return $(elem.data("enables")); +} + +function insertErrorFor(referenceNode, text) { + var element = document.createElement("span"); + element.setAttribute("class", "error_text"); + element.classList.add("red_text"); + element.innerHTML = " " + text; + referenceNode.parentNode.insertBefore(element, referenceNode.nextSibling); +} + +function removeErrorFor(referenceNode) { + $(referenceNode).next('span.error_text').remove(); +} + +/* -------Required fields------- */ +function require_fields(obj) { + switch ($(obj).attr('id')) { + case "room_data_volume": + require_room_volume(true); + require_room_dimensions(false); + break; + case "room_data_dimensions": + require_room_volume(false); + require_room_dimensions(true); + break; + case "mechanical_ventilation": + require_mechanical_ventilation(true); + require_natural_ventilation(false); + break; + case "natural_ventilation": + require_mechanical_ventilation(false); + require_natural_ventilation(true); + break; + case "window_sliding": + require_window_width(false); + break; + case "window_hinged": + require_window_width(true); + break; + case "mech_type_air_changes": + require_air_changes(true); + require_air_supply(false); + break; + case "mech_type_air_supply": + require_air_changes(false); + require_air_supply(true); + break; + case "windows_open_periodically": + require_venting(true); + break; + case "windows_open_permanently": + require_venting(false); + break; + case "hepa_yes": + require_hepa(true); + break; + case "hepa_no": + require_hepa(false); + break; + case "mask_on": + require_mask(true); + break; + case "mask_off": + require_mask(false); + break; + case "exposed_lunch_option_no": + case "infected_lunch_option_no": + require_lunch($(obj).attr('id'), false); + break; + case "exposed_lunch_option_yes": + case "infected_lunch_option_yes": + require_lunch($(obj).attr('id'), true); + break; + default: + break; + } +} + +function unrequire_fields(obj) { + switch (obj.id) { + case "mechanical_ventilation": + require_mechanical_ventilation(false); + break; + case "natural_ventilation": + require_natural_ventilation(false); + break; + default: + break; + } +} + +function require_room_volume(option) { + require_input_field("#room_volume", option); + set_disabled_status("#room_volume", !option); +} + +function require_room_dimensions(option) { + require_input_field("#floor_area", option); + require_input_field("#ceiling_height", option); + set_disabled_status("#floor_area", !option); + set_disabled_status("#ceiling_height", !option); +} + +function require_mechanical_ventilation(option) { + $("#mech_type_air_changes").prop('required', option); + $("#mech_type_air_supply").prop('required', option); + if (!option) { + require_input_field("#air_changes", option); + require_input_field("#air_supply", option); + } +} + +function require_natural_ventilation(option) { + require_input_field("#windows_number", option); + require_input_field("#window_height", option); + require_input_field("#opening_distance", option); + $("#window_sliding").prop('required', option); + $("#window_hinged").prop('required', option); + $("#windows_open_permanently").prop('required', option); + $("#windows_open_periodically").prop('required', option); + if (!option) { + require_input_field("#window_width", option); + require_input_field("#windows_duration", option); + require_input_field("#windows_frequency", option); + } +} + +function require_window_width(option) { + require_input_field("#window_width", option); + set_disabled_status("#window_width", !option); +} + +function require_air_changes(option) { + require_input_field("#air_changes", option); + set_disabled_status("#air_changes", !option); +} + +function require_air_supply(option) { + require_input_field("#air_supply", option); + set_disabled_status("#air_supply", !option); +} + +function require_venting(option) { + require_input_field("#windows_duration", option); + require_input_field("#windows_frequency", option); + set_disabled_status("#windows_duration", !option); + set_disabled_status("#windows_frequency", !option); +} + +function require_lunch(id, option) { + var activity = $(document.getElementById(id)).data('lunch-select'); + var startObj = $(".start_time[data-lunch-for='" + activity + "']")[0]; + var startID = '#' + $(startObj).attr('id'); + var finishObj = $(".finish_time[data-lunch-for='" + activity + "']")[0]; + var finishID = '#' + $(finishObj).attr('id'); + + require_input_field(startID, option); + require_input_field(finishID, option); + set_disabled_status(startID, !option); + set_disabled_status(finishID, !option); + + if (!option) { + $(finishID).removeClass("red_border finish_time_error lunch_break_error"); + removeErrorFor(finishObj); + } else { + if (startObj.value === "" && finishObj.value === "") { + startObj.value = "12:30"; + finishObj.value = "13:30"; + } + validateLunchBreak($(startObj).data('time-group')); + } +} + +function require_mask(option) { + $("#mask_type_1").prop('required', option); + $("#mask_type_ffp2").prop('required', option); +} + +function require_hepa(option) { + require_input_field("#hepa_amount", option); + set_disabled_status("#hepa_amount", !option); +} + +function require_input_field(id, option) { + $(id).prop('required', option); + if (!option) { + removeInvalid(id); + } +} + +function set_disabled_status(id, option) { + if (option) + $(id).addClass("disabled"); + else + $(id).removeClass("disabled"); +} + +function setMaxInfectedPeople() { + $("#training_limit_error").hide(); + var max = $("#total_people").val() + + if ($("#activity_type").val() === "training") { + max = 1; + $("#training_limit_error").show(); + } + + $("#infected_people").attr("max", max); +} + +function removeInvalid(id) { + if ($(id).hasClass("red_border")) { + $(id).val(""); + $(id).removeClass("red_border"); + removeErrorFor(id); + } +} + +function on_ventilation_type_change() { + ventilation_types = $('input[type=radio][name=ventilation_type]'); + ventilation_types.each(function(index) { + if (this.checked) { + getChildElement($(this)).show(); + require_fields(this); + } else { + getChildElement($(this)).hide(); + unrequire_fields(this); + + //Clear invalid inputs for this newly hidden child element + removeInvalid("#" + getChildElement($(this)).find('input').not('input[type=radio]').attr('id')); + } + }); +} + +/* -------UI------- */ + +function show_disclaimer() { + var dots = document.getElementById("dots"); + var moreText = document.getElementById("more"); + var btnText = document.getElementById("myBtn"); + + if (dots.style.display === "none") { + dots.style.display = "inline"; + btnText.innerHTML = "Read more"; + moreText.style.display = "none"; + } else { + dots.style.display = "none"; + btnText.innerHTML = "Read less"; + moreText.style.display = "inline"; + } +} + +$("[data-has-radio]").on('click', function(event) { + $($(this).data("has-radio")).click(); +}); + +$("[data-has-radio]").on('change', function(event) { + $($(this).data("has-radio")).click(); +}); + +function toggle_split_breaks() { + $("#DIVinfected_breaks").toggle(this.checked); + $("#exposed_break_title").toggle(this.checked); + require_lunch("infected_lunch_option_yes", document.getElementById("infected_dont_have_breaks_with_exposed").checked); +} + +/* -------Form validation------- */ +function validate_form(form) { + var submit = true; + + // Activity times and lunch break times are co-dependent + // -> So if 1 fails it doesn't make sense to check the rest + + //Validate all finish times + $("input[required].finish_time").each(function() { + var activity = $(this).data('lunch-for'); + if (document.getElementById("infected_dont_have_breaks_with_exposed").checked || activity != "infected") { + if (!validateFinishTime(this)) { + submit = false; + } + } + }); + + //Validate all lunch breaks + if (submit) { + $("input[required].start_time[data-lunch-for]").each(function() { + var activity = $(this).data('lunch-for'); + if (document.getElementById("infected_dont_have_breaks_with_exposed").checked || activity != "infected") { + if (!validateLunchBreak($(this).data('time-group'))) { + submit = false; + } + } + }); + } + + //Check if breaks length >= activity length + if (submit) { + $("[data-lunch-for]").each(function() { + var activity = $(this).data('lunch-for'); + if (document.getElementById("infected_dont_have_breaks_with_exposed").checked || activity != "infected") { + var activityBreaksObj = document.getElementById("activity_breaks"); + removeErrorFor(activityBreaksObj); + + var lunch_mins = 0; + if (document.getElementById(activity + "_lunch_option_yes").checked) { + var lunch_start = document.getElementById(activity + "_lunch_start"); + var lunch_finish = document.getElementById(activity + "_lunch_finish"); + lunch_mins = parseTimeToMins(lunch_finish.value) - parseTimeToMins(lunch_start.value); + } + + var coffee_breaks = parseInt(document.querySelector('input[name="' + activity + '_coffee_break_option"]:checked').value); + var coffee_duration = parseInt(document.getElementById(activity + "_coffee_duration").value); + var coffee_mins = coffee_breaks * coffee_duration; + + var activity_start = document.getElementById(activity + "_start"); + var activity_finish = document.getElementById(activity + "_finish"); + var activity_mins = parseTimeToMins(activity_finish.value) - parseTimeToMins(activity_start.value); + + if ((lunch_mins + coffee_mins) >= activity_mins) { + insertErrorFor(activityBreaksObj, "Length of breaks >= Length of " + activity + " presence"); + submit = false; + } + } + }); + } + + //Validate all non zero values + $("input[required].non_zero").each(function() { + if (!validateValue(this)) { + submit = false; + } + }); + + + //Validate window venting duration < venting frequency + if (!$("#windows_duration").hasClass("disabled")) { + var windowsDurationObj = document.getElementById("windows_duration"); + var windowsFrequencyObj = document.getElementById("windows_frequency"); + removeErrorFor(windowsFrequencyObj); + + if (parseInt(windowsDurationObj.value) >= parseInt(windowsFrequencyObj.value)) { + insertErrorFor(windowsFrequencyObj, "Duration >= Frequency"); + submit = false; + } + } + + return submit; +} + +function validateValue(obj) { + $(obj).removeClass("red_border"); + removeErrorFor(obj); + + if (!isLessThanZeroOrEmpty($(obj).val())) { + $(obj).addClass("red_border"); + insertErrorFor(obj, "Value must be > 0"); + return false; + } + return true; +} + +function isLessThanZeroOrEmpty(value) { + if (value === "") return true; + if (value <= 0) + return false; + return true; +} + +function validateDate(obj) { + $(obj).removeClass("red_border"); + removeErrorFor(obj); + + if (!isValidDateOrEmpty($(obj).val())) { + $(obj).addClass("red_border"); + insertErrorFor(obj, "Incorrect date format"); + return false; + } + return true; +} + +function isValidDateOrEmpty(date) { + if (date === "") return true; + var matches = /^(\d+)[-\/](\d+)[-\/](\d+)$/.exec(date); + if (matches == null) return false; + var d = matches[1]; + var m = matches[2]; + var y = matches[3]; + if (y > 2100 || y < 1900) return false; + var composedDate = new Date(y + '/' + m + '/' + d); + return composedDate.getDate() == d && composedDate.getMonth() + 1 == m && composedDate.getFullYear() == y; +} + +function validateFinishTime(obj) { + var groupID = $(obj).data('time-group'); + var startObj = $(".start_time[data-time-group='" + groupID + "']")[0]; + var finishObj = $(".finish_time[data-time-group='" + groupID + "']")[0]; + + if ($(finishObj).hasClass("finish_time_error")) { + $(finishObj).removeClass("red_border finish_time_error"); + removeErrorFor(finishObj); + } + + //Check if finish time error (takes precedence over lunch break error) + var startTime = parseValToNumber(startObj.value); + var finishTime = parseValToNumber(finishObj.value); + if (startTime >= finishTime) { + $(finishObj).addClass("red_border finish_time_error"); + removeErrorFor(finishObj); + insertErrorFor(finishObj, "Finish time must be after start"); + return false; + } + return true; +} + +function validateLunchBreak(lunchGroup) { + //Valid if lunch break not selected + if (document.getElementById(lunchGroup + "_option_no").checked) + return true; + + var lunchStartObj = $(".start_time[data-time-group='" + lunchGroup + "']")[0]; + var lunchFinishObj = $(".finish_time[data-time-group='" + lunchGroup + "']")[0]; + + //Skip if finish time error present (it takes precedence over lunch break error) + if ($(lunchStartObj).hasClass("finish_time_error") || $(lunchFinishObj).hasClass("finish_time_error")) + return false; + + removeErrorFor(lunchFinishObj); + var valid = validateLunchTime(lunchStartObj) & validateLunchTime(lunchFinishObj); + if (!valid) { + insertErrorFor(lunchFinishObj, "Lunch break must be within presence times"); + } + + return valid; +} + +//Check if exposed/infected lunch time within exposed/infected presence times +function validateLunchTime(obj) { + var activityGroup = $(obj).data('lunch-for'); + var activityStart = parseValToNumber($(".start_time[data-time-group='" + activityGroup + "']")[0].value); + var activityFinish = parseValToNumber($(".finish_time[data-time-group='" + activityGroup + "']")[0].value); + + var time = parseValToNumber(obj.value); + $(obj).removeClass("red_border lunch_break_error"); + if ((time < activityStart) || (time > activityFinish)) { + $(obj).addClass("red_border lunch_break_error"); + return false; + } + + return true; +} + +function parseValToNumber(val) { + return parseInt(val.replace(':', ''), 10); +} + +function parseTimeToMins(cTime) { + var time = cTime.match(/(\d+):(\d+)/); + return parseInt(time[1] * 60) + parseInt(time[2]); +} + +/* -------On Load------- */ +$(document).ready(function() { + + //Pre-fill form with known values + (new URL(decodeURIComponent(window.location.href))).searchParams.forEach((value, name) => { + + //If element exists + if (document.getElementsByName(name).length > 0) { + var elemObj = document.getElementsByName(name)[0]; + + //Pre-select checked radios + if (elemObj.type === 'radio') { + // Calculator <= 1.5.0 used to send not-applicable in the URL for radios that + // weren't set. Now those are not sent at all, but we keep the behaviour for compatibility. + if (value !== 'not-applicable') { + $('[name="' + name + '"][value="' + value + '"]').prop('checked', true); + } + } + //Pre-select checkboxes + else if (elemObj.type === 'checkbox') { + elemObj.checked = (value == 1); + } + + //Pre-select location + else if (elemObj.id === 'location_select') { + var location_option = document.createElement('option'); + location_option.value = value; + location_option.innerHTML = value; + elemObj.append(location_option); + } + + //Ignore 0 (default) values from server side + else if (!(elemObj.classList.contains("non_zero") || elemObj.classList.contains("remove_zero")) || (value != "0.0" && value != "0")) { + elemObj.value = value; + validateValue(elemObj); + } + } + }); + + // When the document is ready, deal with the fact that we may be here + // as a result of a forward/back browser action. If that is the case, update + // the visibility of some of our inputs. + + // Chrome fix - on back button infected break DIV not shown + if (document.getElementById("infected_dont_have_breaks_with_exposed").checked) { + $("#DIVinfected_breaks").show(); + $("#exposed_break_title").show(); + require_lunch("infected_lunch_option_yes", true); + } + + //Check all radio buttons previously selected + $("input[type=radio]:checked").each(function() { require_fields(this) }); + + // When the ventilation_type changes we want to make its respective + // children show/hide. + $("input[type=radio][name=ventilation_type]").change(on_ventilation_type_change); + // Call the function now to handle forward/back button presses in the browser. + on_ventilation_type_change(); + + // Setup the maximum number of people at page load (to handle back/forward), + // and update it when total people is changed. + setMaxInfectedPeople(); + $("#total_people").change(setMaxInfectedPeople); + $("#activity_type").change(setMaxInfectedPeople); + + //Validate all non zero values + $("input[required].non_zero").each(function() { validateValue(this) }); + $(".non_zero").change(function() { validateValue(this) }); + + //Validate all finish times + $("input[required].finish_time").each(function() { validateFinishTime(this) }); + $(".finish_time").change(function() { validateFinishTime(this) }); + $(".start_time").change(function() { validateFinishTime(this) }); + + //Validate lunch times + $(".start_time[data-lunch-for]").each(function() { validateLunchBreak($(this).data('time-group')) }); + $("[data-lunch-for]").change(function() { validateLunchBreak($(this).data('time-group')) }); + $("[data-lunch-break]").change(function() { validateLunchBreak($(this).data('lunch-break')) }); + + $("#location_select").select2({ + ajax: { + url: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates", + dataType: 'json', + delay: 250, + data: function(params) { + return { + SingleLine: params.term, // search term + f: 'json', + page: params.page, + outFields: 'country, location', + }; + }, + processResults: function(data, params) { + // parse the results into the format expected by Select2 + // since we are using custom formatting functions we do not need to + // alter the remote JSON data, except to indicate that infinite + // scrolling can be used + params.page = params.page || 1; + + return { + results: data.candidates.map(function(candidate) { + return { + id: candidate.address, + text: candidate.address, + country: candidate.attributes.country, + latitude: String(candidate.location.y), + longitude: String(candidate.location.x), + } + }), + pagination: { + more: (params.page * 30) < data.candidates.length + } + }; + }, + cache: true + }, + placeholder: 'Search for a location', + minimumInputLength: 1, + templateResult: formatlocation, + templateSelection: formatLocationSelection + }); +}); + +function formatlocation(location) { + if (location.loading) { + return location.text; + } + + var $container = $( + "
Room Volume: {{ model.concentration_model.room.volume }} m³
Room Central Heating: {{ "On" if form.room_heating_option else "Off" }}
Room Location: {{ form.location }}
Geographic Location: {{ form.location }}
Nearest weather station: {{ form.weather_station_location }}
Ventilation data:
diff --git a/cara/data.py b/cara/data.py index f8286628..57958b1b 100644 --- a/cara/data.py +++ b/cara/data.py @@ -23,7 +23,11 @@ weather_debug = True def location_to_weather_stn(location_loc): #expects a tuple (lat, long) #returns: weather station ID, weather station name, weather station lat, long - search_coords = [location_loc[0], location_loc[1]] + print('location_loc,', location_loc) + + search_coords = location_loc.split(',') #[location_loc[0], location_loc[1]] + print('location_searchcoords,', search_coords) + lat=[] long=[] station_array=[] @@ -63,22 +67,56 @@ def location_celcius_per_hour(location): weather_dict = json.load(json_file) Location_hourly_temperatures_celsius_per_hour = weather_dict[w_station[0]] if weather_debug: + print(location) print("weather station name: ", w_station[1]) + weather_station_location = w_station[1] print("weather station ref: ", w_station[0]) + weather_station_ref = w_station[0] print("weather station location: ", w_station[2], " ", w_station[3]) print(Location_hourly_temperatures_celsius_per_hour) return Location_hourly_temperatures_celsius_per_hour +#initialise with Geneva, change if location /=default. +local_tempatures = { + '1': [0.2, -0.3, -0.5, -0.9, -1.1, -1.4, -1.5, -1.5, -1.1, 0.1, 1.5, + 2.8, 3.8, 4.4, 4.5, 4.4, 4.4, 3.9, 3.1, 2.7, 2.2, 1.7, 1.5, 1.1], + '2': [0.9, 0.3, 0.0, -0.5, -0.7, -1.1, -1.2, -1.1, -0.7, 0.8, 2.5, + 4.2, 5.4, 6.2, 6.3, 6.2, 6.1, 5.5, 4.5, 4.1, 3.5, 2.8, 2.5, 2.0], + '3': [4.2, 3.5, 3.1, 2.5, 2.1, 1.6, 1.5, 1.6, 2.2, 4.0, 6.3, 8.4, + 10.0, 11.1, 11.2, 11.1, 11.0, 10.2, 8.9, 8.3, 7.5, 6.7, 6.3, 5.6], + '4': [7.4, 6.7, 6.2, 5.5, 5.2, 4.7, 4.5, 4.6, 5.3, 7.2, 9.6, 11.9, + 13.7, 14.8, 14.9, 14.8, 14.7, 13.8, 12.4, 11.8, 10.9, 10.1, 9.6, 8.9], + '5': [11.8, 11.1, 10.6, 9.9, 9.5, 8.9, 8.8, 8.9, 9.6, 11.6, 14.2, 16.6, + 18.4, 19.6, 19.7, 19.6, 19.4, 18.6, 17.1, 16.5, 15.6, 14.6, 14.2, 13.4], + '6': [15.2, 14.4, 13.9, 13.2, 12.7, 12.2, 12.0, 12.1, 12.8, 15.0, 17.7, + 20.2, 22.1, 23.3, 23.5, 23.4, 23.2, 22.3, 20.8, 20.1, 19.1, 18.2, 17.7, 16.9], + '7': [17.6, 16.7, 16.1, 15.3, 14.9, 14.3, 14.1, 14.2, 15.0, 17.3, 20.2, + 23.0, 25.0, 26.3, 26.5, 26.4, 26.2, 25.2, 23.6, 22.8, 21.8, 20.8, 20.2, 19.4], + '8': [17.1, 16.2, 15.7, 14.9, 14.5, 13.9, 13.7, 13.8, 14.6, 16.9, 19.7, + 22.4, 24.4, 25.6, 25.8, 25.7, 25.5, 24.5, 22.9, 22.2, 21.2, 20.2, 19.7, 18.9], + '9': [13.4, 12.7, 12.2, 11.5, 11.2, 10.7, 10.5, 10.6, 11.3, 13.2, 15.6, + 17.9, 19.6, 20.8, 20.9, 20.8, 20.7, 19.8, 18.4, 17.8, 16.9, 16.1, 15.6, 14.9], + '10': [9.4, 8.8, 8.5, 7.9, 7.6, 7.2, 7.1, 7.2, 7.7, 9.3, 11.2, 13.0, + 14.4, 15.3, 15.4, 15.3, 15.2, 14.5, 13.4, 12.9, 12.2, 11.6, 11.2, 10.6], + '11': [4.0, 3.6, 3.3, 2.9, 2.6, 2.3, 2.2, 2.2, 2.7, 3.9, 5.5, 6.9, 8.0, + 8.7, 8.8, 8.7, 8.7, 8.1, 7.2, 6.8, 6.3, 5.7, 5.5, 5.0], + '12': [1.4, 1.0, 0.8, 0.4, 0.2, -0.0, -0.1, -0.1, 0.3, 1.3, 2.6, 3.8, + 4.7, 5.2, 5.3, 5.2, 5.2, 4.7, 4.0, 3.7, 3.2, 2.8, 2.6, 2.2] + } + # Geneva hourly temperatures as piecewise constant function (in Kelvin) Temperatures_hourly = { month: models.PiecewiseConstant(tuple(np.arange(25.)), tuple(273.15+np.array(temperatures))) - for month,temperatures in location_celcius_per_hour(calc_location).items() + for month,temperatures in local_tempatures.items() } # same temperatures on a finer temperature mesh Temperatures = { - month: Temperatures_hourly[month].refine(refine_factor=4) - for month,temperatures in location_celcius_per_hour(calc_location).items() + month: Temperatures_hourly[month].refine(refine_factor=10) + for month, temperatures in local_tempatures.items() } + + +