diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cfe6a8..1094539 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,10 @@ jobs: # Since EnergyModelsGUI doesn't have binary dependencies, # only test on a subset of possible platforms. include: - - version: '1.11' # The latest point-release (Linux) + - version: '1' # The latest point-release (Linux) os: ubuntu-latest arch: x64 - #- version: '1.11' # The latest point-release (Windows) + #- version: '1' # The latest point-release (Windows) # os: windows-latest # arch: x64 - version: 'lts' # lts diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 6e363bd..80be4f4 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@latest with: - version: '1.11' + version: '1' - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev xsettingsd x11-xserver-utils - name: Install dependencies shell: julia --color=yes --project=docs/ {0} diff --git a/NEWS.md b/NEWS.md index 3ec2c07..c0ba14f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,10 +1,10 @@ # Release notes -## Version 0.5.17 (2025-11-09) +## Version 0.5.17 (2025-11-19) ### Bugfix -* Fix bug that made nodes/areas disappear when plotting too many objects (max z_level is 10000). +* Fix bug that made `Node`s/`Area`s disappear when plotting too many objects (max z_level is 10000). * Fix square_intersection function. ### Enhancements @@ -13,6 +13,14 @@ * Remove redundant `notify_component` function and `Observable`s (use the `@lift` macro instead). * Improve performance of updates to `ax_info`. * Add missing tests for show-function on the types `AbstractSystem` and `ProcInvData`, and improve code structure. +* Bumped Makie packages to latest versions (and adjusted the code to the breaking changes) which increased performance. +* Improved code performance. +* Toggeling of GeoMakie is also now available which improves performance if the background map is not required. + +# Adjustments + +* Skip warnings if a provided non-empty `id_to_icon_map` does not contain icons for all `Node`s/`Area`s (as it is fine to combine custom icon with the default generated icons). +* Introduced the new parametric type `PlotContainer` to replace `Dict{:Symbol, Any}` types used as container for plotted data. ## Version 0.5.16 (2025-09-24) diff --git a/Project.toml b/Project.toml index b36ad9d..46123e8 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "EnergyModelsGUI" uuid = "737a7361-d3b7-40e9-b1ac-59bee4c5ea2d" -authors = ["Jon Vegard Venås ", "Magnus Askeland ", "Shweta Tiwari "] version = "0.5.17" +authors = ["Jon Vegard Venås ", "Magnus Askeland ", "Shweta Tiwari "] [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" @@ -17,6 +17,7 @@ GeoJSON = "61d90e0f-e114-555e-ac52-39dfb47a3ef9" GeoMakie = "db073c08-6b98-4ee5-b6a4-5efafb3259c6" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" @@ -26,7 +27,6 @@ SparseVariables = "2749762c-80ed-4b14-8f33-f0736679b02b" TimeStruct = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c" XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" -InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" [weakdeps] EnergyModelsGeography = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" @@ -36,27 +36,27 @@ EMGExt = "EnergyModelsGeography" [compat] CSV = "0.10" -CairoMakie = "=0.12.18" -Colors = "0.12" -DataFrames = "1.7" -Dates = "1.9" +CairoMakie = "0.15" +Colors = "0.13" +DataFrames = "1" +Dates = "1" EnergyModelsBase = "0.9" EnergyModelsGeography = "0.11" EnergyModelsInvestments = "0.8" -FileIO = "1.16" -GLMakie = "=0.10.18" +FileIO = "1" +GLMakie = "0.13" GeoJSON = "0.8" -GeoMakie = "=0.7.12" +GeoMakie = "0.7.16" HTTP = "1.10" -ImageMagick = "1.3" +ImageMagick = "1" +InteractiveUtils = "1" IntervalSets = "<0.7.12" JuMP = "1.22" -Pkg = "1.9" -PrettyTables = "2.3" -Printf = "1.9" +Pkg = "1" +PrettyTables = "3" +Printf = "1" SparseVariables = "0.7" TimeStruct = "0.9" XLSX = "0.10" YAML = "0.4" -julia = "1.10" -InteractiveUtils = "1" +julia = "1.10, 1.11, 1.12" diff --git a/README.md b/README.md index c0b6474..f846f17 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,6 @@ Visualization of the results after simulations will be added at a later stage. The EnergyModelsGUI package has taken inspiration from the source code of [ModelingToolkitDesigner](https://github.com/bradcarman/ModelingToolkitDesigner.jl) as a starting point for development. -> [!WARNING] -> EnergyModelsGUI.jl currently does not support Julia verison 1.12 due to a breaking change in GLMakie. It is assumed that this issue will resolve soon in the future. - ## Usage If you already have constructed a `case` in EMX you can view this case with diff --git a/docs/generate_images.jl b/docs/generate_images.jl index 5b55b0f..59dc4fa 100644 --- a/docs/generate_images.jl +++ b/docs/generate_images.jl @@ -6,9 +6,14 @@ import EnergyModelsGUI: get_button, get_root_design, get_components, + get_component, get_selected_systems, + get_name, update!, - toggle_selection_color! + toggle_selection_color!, + select_data! + +include(joinpath(@__DIR__, "..", "examples", "generate_examples.jl")) """ create_colors_visualization_image() @@ -63,8 +68,26 @@ end Create figures of the GUI based on the EMI_geography.jl example to be used for docs and README.md. """ function create_EMI_geography_images() - include(joinpath(@__DIR__, "..", "examples", "generate_examples.jl")) - include(joinpath(@__DIR__, "..", "examples", "EMI_geography.jl")) + case, model = generate_example_data_geo() + optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) + m = create_model(case, model) + set_optimizer(m, optimizer) + optimize!(m) + + solution_summary(m) + + # Set folder where visualization info is saved and retrieved + design_path = joinpath(@__DIR__, "design", "EMI", "geography") + + # Run the GUI + gui = GUI( + case; + design_path, + model = m, + coarse_coast_lines = false, + scale_tot_opex = true, + scale_tot_capex = false, + ) # Create examples.png image path_to_results = joinpath(@__DIR__, "src", "figures") @@ -73,7 +96,6 @@ function create_EMI_geography_images() get_menu(gui, :export_type).selection[] = "png" export_button = get_button(gui, :export) open_button = get_button(gui, :open) - available_data_menu = get_menu(gui, :available_data) notify(export_button.clicks) mv( joinpath(path_to_results, "All.png"), @@ -82,15 +104,11 @@ function create_EMI_geography_images() ) # Create EMI_geography.png image - root_design = get_root_design(gui) - components = get_components(root_design) - component = components[1] # fetch the Oslo area - push!(get_selected_systems(gui), component) # Manually add to :selected_systems + oslo_area = get_component(get_root_design(gui), 1) + push!(get_selected_systems(gui), oslo_area) # Manually add to :selected_systems update!(gui) - toggle_selection_color!(gui, component, true) - available_data = [x[2][:name] for x ∈ collect(available_data_menu.options[])] - i_selected = findfirst(x -> x == "area_exchange", available_data) - available_data_menu.i_selected = i_selected # Select flow_out (CO2) + toggle_selection_color!(gui, oslo_area, true) + select_data!(gui, "area_exchange") notify(export_button.clicks) mv( joinpath(path_to_results, "All.png"), @@ -100,15 +118,13 @@ function create_EMI_geography_images() # Create EMI_geography_Oslo.png image notify(open_button.clicks) - sub_component = components[1].components[2] # fetch the Oslo area + sub_component = get_component(oslo_area, 5) # fetch node id 5 in Oslo area selected_systems = get_selected_systems(gui) empty!(selected_systems) push!(selected_systems, sub_component) # Manually add to :selected_systems update!(gui) toggle_selection_color!(gui, sub_component, true) - available_data = [x[2][:name] for x ∈ collect(available_data_menu.options[])] - i_selected = findfirst(x -> x == "cap_add", available_data) - available_data_menu.i_selected = i_selected # Select flow_out (CO2) + select_data!(gui, "cap_add") notify(export_button.clicks) mv( joinpath(path_to_results, "All.png"), diff --git a/docs/make.jl b/docs/make.jl index 722456e..45deb3f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -13,6 +13,7 @@ if isfile(news) end cp("NEWS.md", news) +ENV["EMX_TEST"] = true # Set flag for example scripts to check if they are run as part CI include("generate_images.jl") DocMeta.setdocmeta!( diff --git a/docs/src/figures/EMI_geography.png b/docs/src/figures/EMI_geography.png index 9b1a901..2543623 100644 Binary files a/docs/src/figures/EMI_geography.png and b/docs/src/figures/EMI_geography.png differ diff --git a/docs/src/figures/EMI_geography_Oslo.png b/docs/src/figures/EMI_geography_Oslo.png index 5203a95..fef7f91 100644 Binary files a/docs/src/figures/EMI_geography_Oslo.png and b/docs/src/figures/EMI_geography_Oslo.png differ diff --git a/ext/EMGExt/EMGExt.jl b/ext/EMGExt/EMGExt.jl index 04d7d93..eaf26bb 100644 --- a/ext/EMGExt/EMGExt.jl +++ b/ext/EMGExt/EMGExt.jl @@ -136,23 +136,10 @@ end Create a sub-system of `system` with the `element` as the availability node. """ function EMGUI.sub_system(system::EMGUI.SystemGeo, element::AbstractElement) - area_an::EMB.Node = availability_node(element) + # Get all nodes and links in the area directly or indirectly connected by `Link`s to `element`. + area_nodes::Vector{EMB.Node}, area_links::Vector{Link} = + EMG.nodes_in_area(element, get_links(system); n_nodes = length(get_nodes(system))) - # Allocate redundantly large vector (for efficiency) to collect all links and nodes - links::Vector{Link} = get_links(system) - area_links::Vector{Link} = Vector{Link}(undef, length(links)) - area_nodes::Vector{EMB.Node} = Vector{EMB.Node}( - undef, length(get_nodes(system)), - ) - - area_nodes[1] = area_an - - # Create counting indices for area_links and area_nodes respectively - indices::Vector{Int} = [1, 2] - - EMGUI.get_linked_nodes!(area_an, links, area_links, area_nodes, indices) - resize!(area_links, indices[1] - 1) - resize!(area_nodes, indices[2] - 1) return EMGUI.System( get_time_struct(system), get_products(system), @@ -160,7 +147,7 @@ function EMGUI.sub_system(system::EMGUI.SystemGeo, element::AbstractElement) area_nodes, area_links, element, - area_an, + availability_node(element), ) end diff --git a/src/datastructures.jl b/src/datastructures.jl index 7c9e696..edba484 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -5,6 +5,13 @@ Supertype for EnergyModelsGUI objects representing `Node`s/`Link`s/`Area`s/`Tran """ abstract type AbstractGUIObj end +""" + NothingDesign <: AbstractGUIObj + +Type for representing a non-existent design. +""" +struct NothingDesign <: AbstractGUIObj end + """ AbstractSystem @@ -119,11 +126,12 @@ energy system designs in Julia. - **`components::Vector{EnergySystemDesign}`** is the components of the system, stored as an array of EnergySystemDesign objects. - **`connections::Vector{Connection}`** are the connections between system parts. -- **`xy::Observable{<:Point2f}`** are the coordinates of the system, observed for changes. +- **`parent::AbstractGUIObj`** is the parent of the system. +- **`xy::Observable{<:Point2f}`** is the coordinate of the system, observed for changes. - **`icon::String`** is the optional (path to) icons associated with the system, stored as a string. -- **`color::Observable{Symbol}`** is the color of the system, observed for changes and - represented as a Symbol. The color is toggled to highlight system activation. +- **`color::Observable{RGBA{Float32}}`** is the color of the system, observed for changes. + The color is toggled to highlight system activation. - **`wall::Observable{Symbol}`** represents an aspect of the system's state, observed for changes and represented as a Symbol. - **`file::String`** is the filename or path associated with the `EnergySystemDesign`. @@ -137,9 +145,10 @@ mutable struct EnergySystemDesign <: AbstractGUIObj id_to_icon_map::Dict components::Vector{EnergySystemDesign} connections::Vector + parent::AbstractGUIObj xy::Observable{<:Point2f} icon::String - color::Observable{Symbol} + color::Observable{RGBA{Float32}} wall::Observable{Symbol} file::String inv_data::ProcInvData @@ -151,9 +160,10 @@ function EnergySystemDesign( id_to_icon_map::Dict, components::Vector{EnergySystemDesign}, connections::Vector, + parent::AbstractGUIObj, xy::Observable{<:Point2f}, icon::String, - color::Observable{Symbol}, + color::Observable{RGBA{Float32}}, wall::Observable{Symbol}, file::String, ) @@ -163,6 +173,7 @@ function EnergySystemDesign( id_to_icon_map, components, connections, + parent, xy, icon, color, @@ -186,7 +197,7 @@ Mutable type for providing a flexible data structure for connections between - **`to::EnergySystemDesign`** is the `EnergySystemDesign` to which the connection is linked to. - **`connection::AbstractElement`** is the EMX connection structure. -- **`colors::Vector{RGB}`** is the associated colors of the connection. +- **`colors::Vector{RGBA{Float32}}`** is the associated colors of the connection. - **`plots::Vector{Any}`** is a vector with all Makie object associated with this object. - **`invest_data::ProcInvData`** stores processed investment data. """ @@ -194,7 +205,7 @@ mutable struct Connection <: AbstractGUIObj from::EnergySystemDesign to::EnergySystemDesign connection::AbstractElement - colors::Vector{RGB} + colors::Vector{RGBA{Float32}} inv_data::ProcInvData plots::Vector{Any} end @@ -204,7 +215,7 @@ function Connection( connection::AbstractElement, id_to_color_map::Dict{Any,Any}, ) - colors::Vector{RGB} = get_resource_colors(connection, id_to_color_map) + colors::Vector{RGBA{Float32}} = get_resource_colors(connection, id_to_color_map) return Connection(from, to, connection, colors, ProcInvData(), Any[]) end @@ -264,7 +275,41 @@ mutable struct GUI vars::Dict{Symbol,Any} end +""" + PlotContainer{T} + +Type for storing plot-related data available from the "Data" menu. + +# Fields + +- **`name::String`**: is the reference name for the data. +- **`selection::Vector`**: is the indices used to extract the data to be plotted. +- **`field_data::Any`**: is the data from which plots are extracted based on selection. +- **`description::String`**: is the description to be used for the legend. +""" +struct PlotContainer{T} + name::String + selection::Vector + field_data::Any + description::String +end + +# Create aliases for different PlotContainer types +const JuMPContainer = PlotContainer{:JuMP} +const CaseDataContainer = PlotContainer{:CaseData} +const GlobalDataContainer = PlotContainer{:GlobalData} + +# Define standard colours in EMGUI +const BLACK = RGBA{Float32}(0.0, 0.0, 0.0, 1.0) +const WHITE = RGBA{Float32}(1.0, 1.0, 1.0, 1.0) +const GREEN2 = RGBA{Float32}(0.0, 0.93333334, 0.0, 1.0) +const RED = RGBA{Float32}(1.0, 0.0, 0.0, 1.0) +const YELLOW = RGBA{Float32}(1.0, 1.0, 0.0, 1.0) +const MAGENTA = RGBA{Float32}(1.0, 0.0, 1.0, 1.0) +const CYAN = RGBA{Float32}(0.0, 1.0, 1.0, 1.0) + Base.show(io::IO, obj::AbstractGUIObj) = dump(io, obj; maxdepth = 1) +Base.show(io::IO, ::NothingDesign) = print(io, "NothingDesign()") Base.show(io::IO, obj::ProcInvData) = dump(io, obj; maxdepth = 1) Base.show(io::IO, system::AbstractSystem) = dump(io, system; maxdepth = 1) Base.show(io::IO, gui::GUI) = dump(io, gui; maxdepth = 1) @@ -398,7 +443,7 @@ get_system(design::EnergySystemDesign) = design.system Returns the `parent` field of a `EnergySystemDesign` `design`. """ -get_parent(design::EnergySystemDesign) = get_parent(get_system(design)) +get_parent(design::EnergySystemDesign) = design.parent """ get_element(design::EnergySystemDesign) @@ -414,6 +459,22 @@ Returns the `components` field of a `EnergySystemDesign` `design`. """ get_components(design::EnergySystemDesign) = design.components +""" + get_component(designs::Vector{EnergySystemDesign}, id) + get_component(designs::EnergySystemDesign, id) + +Extract the component from a vector of `EnergySystemDesign`(s) that has a `parent` with +the given `id`. +""" +function get_component(designs::Vector{EnergySystemDesign}, id) + for design ∈ designs + if get_parent(get_system(design)).id == id + return design + end + end +end +get_component(designs::EnergySystemDesign, id) = get_component(get_components(designs), id) + """ get_connections(design::EnergySystemDesign) @@ -456,20 +517,6 @@ Returns the `file` field of a `EnergySystemDesign` `design`. """ get_file(design::EnergySystemDesign) = design.file -""" - get_inv_data(design::EnergySystemDesign) - -Returns the `inv_data` field of a `EnergySystemDesign` `design`. -""" -get_inv_data(design::EnergySystemDesign) = design.inv_data - -""" - get_plots(design::EnergySystemDesign) - -Returns the `plots` field of a `EnergySystemDesign` `design`. -""" -get_plots(design::EnergySystemDesign) = design.plots - """ EMB.get_time_struct(design::EnergySystemDesign) @@ -512,20 +559,6 @@ Returns the `colors` field of a `Connection` `conn`. """ get_colors(conn::Connection) = conn.colors -""" - get_inv_data(design::Connection) - -Returns the `inv_data` field of a `Connection` `design`. -""" -get_inv_data(design::Connection) = design.inv_data - -""" - get_plots(conn::Connection) - -Returns the `plots` field of a `Connection` `conn`. -""" -get_plots(conn::Connection) = conn.plots - """ get_inv_times(data::ProcInvData) get_inv_times(design::AbstractGUIObj) @@ -553,6 +586,20 @@ Returns a boolean indicator if investment has occured. has_invested(data::ProcInvData) = data.invested has_invested(design::AbstractGUIObj) = has_invested(get_inv_data(design)) +""" + get_inv_data(obj::AbstractGUIObj) + +Returns the `inv_data` field of a `AbstractGUIObj` `obj`. +""" +get_inv_data(obj::AbstractGUIObj) = obj.inv_data + +""" + get_plots(obj::AbstractGUIObj) + +Returns the `plots` field of a `AbstractGUIObj` `obj`. +""" +get_plots(obj::AbstractGUIObj) = obj.plots + """ get_fig(gui::GUI) @@ -729,3 +776,38 @@ EMB.get_time_struct(gui::GUI) = EMB.get_time_struct(get_design(gui)) Returns the `parent` field of a `GUI` `gui`. """ get_parent(gui::GUI) = get_parent(get_design(gui)) + +""" + get_system(gui::GUI) + +Returns the `system` field in the `design` field of a `GUI` `gui`. +""" +get_system(gui::GUI) = get_system(get_design(gui)) + +""" + get_name(container::PlotContainer) + +Returns the `name` field of a `PlotContainer` `container`. +""" +get_name(container::PlotContainer) = container.name + +""" + get_selection(container::PlotContainer) + +Returns the `selection` field of a `PlotContainer` `container`. +""" +get_selection(container::PlotContainer) = container.selection + +""" + get_field_data(container::PlotContainer) + +Returns the `field_data` field of a `PlotContainer` `container`. +""" +get_field_data(container::PlotContainer) = container.field_data + +""" + get_description(container::PlotContainer) + +Returns the `description` field of a `PlotContainer` `container`. +""" +get_description(container::PlotContainer) = container.description diff --git a/src/setup_GUI.jl b/src/setup_GUI.jl index 0c85854..396f0f4 100644 --- a/src/setup_GUI.jl +++ b/src/setup_GUI.jl @@ -38,6 +38,10 @@ to the old EnergyModelsX `case` dictionary. - **`scale_tot_capex::Bool=false`** divides total CAPEX quantities with the duration of the strategic period. - **`colormap::Vector=Makie.wong_colors()`** is the colormap used for plotting results. - **`tol::Float64=1e-12`** the tolerance for numbers close to machine epsilon precision. +- **`enable_data_inspector::Bool=true`** toggles the DataInspector functionality for + hovering objects to show information. +- **`use_geomakie::Bool=true`** toggles the use of GeoMakie for plotting geographical + designs when the `case` contains geographical information. !!! warning "Reading model results from CSV-files" Reading model results from a directory (*i.e.*, `model::String` implying that the results @@ -66,6 +70,8 @@ function GUI( scale_tot_capex::Bool = false, colormap::Vector = Makie.wong_colors(), tol::Float64 = 1e-8, + enable_data_inspector::Bool = true, + use_geomakie::Bool = true, ) # Generate the system topology: @info raw"Setting up the topology design structure" @@ -92,7 +98,7 @@ function GUI( :parent_scaling => 1.1, # Scale for enlargement of boxes around main boxes for nodes for parent systems :icon_scale => 0.9f0, # scale icons w.r.t. the surrounding box in fraction of Δh :two_way_sep_px => 10, # No pixels between set of lines for nodes having connections both ways - :selection_color => :green2, # Colors for box boundaries when selection objects + :selection_color => GREEN2, # Colors for box boundaries when selection objects :investment_lineStyle => Linestyle([1.0, 1.5, 2.0, 2.5] .* 5), # linestyle for investment connections and box boundaries for nodes :path_to_results => path_to_results, # Path to the location where axes[:results] can be exported :plotted_data => [], @@ -104,6 +110,7 @@ function GUI( :scale_tot_capex => scale_tot_capex, :colormap => colormap, :tol => tol, + :use_geomakie => use_geomakie, :autolimits => Dict( :results_op => true, :results_sc => true, @@ -183,6 +190,8 @@ function GUI( end screen = GLMakie.Screen(title = fig_title) + display(screen, fig) + ## Create the main structure for the EnergyModelsGUI gui::GUI = GUI( fig, screen, axes, legends, buttons, menus, toggles, root_design, design, @@ -206,15 +215,15 @@ function GUI( # Define all event functions in the GUI define_event_functions(gui) + # Update the placement of the title of the topology axis + notify(axes[:topo].finallimits) + # make sure all graphics is adapted to the spawned figure sizes notify(get_toggle(gui, :expand_all).active) # Enable inspector (such that hovering objects shows information) # Linewidth set to zero as this boundary is slightly laggy on movement - DataInspector(fig; range = 3, indicator_linewidth = 0) - - # display the figure - display(screen, fig) + DataInspector(fig; range = 3, indicator_linewidth = 0, enabled = enable_data_inspector) return gui end @@ -271,7 +280,7 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) vars[:plot_widths][1] / (vars[:plot_widths][2] - vars[:taskbar_height]) / 2 # Check whether or not to use lat-lon coordinates to construct the axis used for visualizing the topology - if isa(get_system(design), SystemGeo) + if isa(get_system(design), SystemGeo) && vars[:use_geomakie] # Set the source mapping for projection source::String = "+proj=merc +lon_0=0 +x_0=0 +y_0=0 +a=6378137 +b=6378137 +units=m +no_defs" # Set the destination mapping for projection @@ -281,7 +290,7 @@ function create_makie_objects(vars::Dict, design::EnergySystemDesign) gridlayout_topology_ax[1, 1]; source, dest, - alignmode = Outside(), + alignmode = Inside(), ) if vars[:coarse_coast_lines] # Use low resolution coast lines diff --git a/src/setup_topology.jl b/src/setup_topology.jl index 05d9ee1..895a5a0 100644 --- a/src/setup_topology.jl +++ b/src/setup_topology.jl @@ -15,7 +15,7 @@ the function initializes the `EnergySystemDesign`. - **`y::Float32=0.0f0`** is the initial y-coordinate of the system. - **`icon::String=""`** is the optional (path to) icons associated with the system, stored as a string. -- **`parent::Union{Symbol, Nothing}=nothing`** is a parent reference or indicator. +- **`parent::AbstractGUIObj=NothingDesign()`** is a parent EnergySystemDesign object. The function reads system configuration data from a TOML file specified by `design_path` (if it exists), initializes various internal fields, and processes connections and wall values. @@ -30,6 +30,7 @@ function EnergySystemDesign( x::Float32 = 0.0f0, y::Float32 = 0.0f0, icon::String = "", + parent::AbstractGUIObj = NothingDesign(), ) # Create the path to the file where existing design is stored (if any) file::String = design_file(system, design_path) @@ -59,6 +60,20 @@ function EnergySystemDesign( elements = get_children(system) parent_x, parent_y = xy[] # extract parent coordinates + design = EnergySystemDesign( + system, + id_to_color_map, + id_to_icon_map, + components, + connections, + parent, + xy, + icon, + Observable(BLACK), + Observable(:E), + file, + ) + # If system contains any components (i.e. !isnothing(elements)) add all components # (constructed as an EnergySystemDesign) to `components` if !isnothing(elements) @@ -99,7 +114,7 @@ function EnergySystemDesign( # Add child to `components` push!( - components, + design.components, EnergySystemDesign( this_sys; design_path, @@ -108,6 +123,7 @@ function EnergySystemDesign( x, y, icon = find_icon(this_sys, id_to_icon_map), + parent = design, ), ) end @@ -125,23 +141,12 @@ function EnergySystemDesign( # If `EnergySystemDesign`s found, create a new `Connection` if !isnothing(from) && !isnothing(to) - push!(connections, Connection(from, to, element, id_to_color_map)) + push!(design.connections, Connection(from, to, element, id_to_color_map)) end end end - return EnergySystemDesign( - system, - id_to_color_map, - id_to_icon_map, - components, - connections, - xy, - icon, - Observable(:black), - Observable(:E), - file, - ) + return design end function EnergySystemDesign(case::Case; kwargs...) return EnergySystemDesign(parse_case(case); kwargs...) diff --git a/src/utils_GUI/GUI_utils.jl b/src/utils_GUI/GUI_utils.jl index 047785d..a24c8a1 100644 --- a/src/utils_GUI/GUI_utils.jl +++ b/src/utils_GUI/GUI_utils.jl @@ -1,47 +1,44 @@ """ - toggle_selection_color!(gui::GUI, selection, selected::Bool) + toggle_selection_color!(gui::GUI, selection::EnergySystemDesign, selected::Bool) + toggle_selection_color!(gui::GUI, selection::Connection, selected::Bool) + toggle_selection_color!(gui::GUI, selection::Dict{Symbol,Any}, selected::Bool) Set the color of selection to `get_selection_color(gui)` if selected, and its original color otherwise using the argument `selected`. """ function toggle_selection_color!(gui::GUI, selection::EnergySystemDesign, selected::Bool) - if selected - selection.color[] = get_selection_color(gui) - else - selection.color[] = :black - end + selection.color[] = selected ? get_selection_color(gui) : BLACK end function toggle_selection_color!(gui::GUI, selection::Connection, selected::Bool) plots = selection.plots if selected for plot ∈ plots - for plot_sub ∈ plot - plot_sub.color = get_selection_color(gui) + selection_color = get_selection_color(gui) + if isa(plot, Makie.AbstractPlot) + plot.color = fill(selection_color, length(plot.color[])) + else + for plot_sub ∈ plot + plot_sub.color = selection_color + end end end else - colors::Vector{RGB} = selection.colors + colors::Vector{RGBA{Float32}} = selection.colors no_colors::Int64 = length(colors) for plot ∈ plots - for (i, plot_sub) ∈ enumerate(plot) - plot_sub.color = colors[((i-1)%no_colors)+1] + if isa(plot, Makie.AbstractPlot) + plot.color = colors + else + for (i, plot_sub) ∈ enumerate(plot) + plot_sub.color = colors[((i-1)%no_colors)+1] + end end end end end function toggle_selection_color!(gui::GUI, selection::Dict{Symbol,Any}, selected::Bool) - color = selected ? parse(Colorant, get_selection_color(gui)) : selection[:color] - plot = selection[:plot] - plot.color[] = color - - # Implement ugly hack to resolve bug in Makie for barplots due to legend updates - while !isempty(plot.plots) - plot = plot.plots[1] - plot.color[] = color - end - - # Implement hack to resolve bug in Makie for stairs/lines due to legend updates - selection[:color_obs][] = color + selection[:plot].color = selected ? get_selection_color(gui) : selection[:color] + update_legend!(gui) end """ @@ -71,79 +68,51 @@ function get_EMGUI_obj(plt) end """ - pick_component!(gui::GUI) + pick_component!(gui::GUI, ax_type::Symbol) + pick_component!(gui::GUI, plt::AbstractPlot, ax_type::Symbol) + pick_component!(gui::GUI, element::AbstractGUIObj, ::Symbol) + pick_component!(gui::GUI, element::Dict, ::Symbol) + pick_component!(gui::GUI, ::Nothing, ax_type::Symbol) -Check if a system is found under the mouse pointer and if it is an `EnergySystemDesign` -or a `Connection` and update state variables. +Check if a system is found under the mouse pointer and if it is an `AbstractGUIObj` (for +objects in the topology axis) or a `Dict` (for objects in the results axis). If found, +state variables are updated. Results in the topology axis are only cleared if `ax_type = :topo` +and in the results axis if `ax_type = :results`. """ -function pick_component!( - gui::GUI; - pick_topo_component = false, - pick_results_component = false, -) +function pick_component!(gui::GUI, ax_type::Symbol) plt, _ = pick(get_fig(gui)) - - pick_component!(gui, plt; pick_topo_component, pick_results_component) + pick_component!(gui, plt, ax_type) end -function pick_component!( - gui::GUI, plt::AbstractPlot; pick_topo_component = false, pick_results_component = false, -) - if pick_topo_component || pick_results_component - element = get_EMGUI_obj(plt) - pick_component!(gui, element; pick_topo_component, pick_results_component) - end +function pick_component!(gui::GUI, plt::AbstractPlot, ax_type::Symbol) + pick_component!(gui, get_EMGUI_obj(plt), ax_type) end -function pick_component!( - gui::GUI, - element::Union{EnergySystemDesign,Connection}; - pick_topo_component = false, - pick_results_component = false, -) - if isnothing(element) - clear_selection( - gui; clear_topo = pick_topo_component, clear_results = pick_results_component, - ) - else - push!(gui.vars[:selected_systems], element) - toggle_selection_color!(gui, element, true) - end +function pick_component!(gui::GUI, element::AbstractGUIObj, ::Symbol) + push!(gui.vars[:selected_systems], element) + toggle_selection_color!(gui, element, true) end -function pick_component!( - gui::GUI, element::Dict; pick_topo_component = false, pick_results_component = false, -) - if isnothing(element) - clear_selection( - gui; clear_topo = pick_topo_component, clear_results = pick_results_component, - ) - else - element[:selected] = true - toggle_selection_color!(gui, element, true) - end +function pick_component!(gui::GUI, element::Dict, ::Symbol) + element[:selected] = true + toggle_selection_color!(gui, element, true) end -function pick_component!( - gui::GUI, ::Nothing; pick_topo_component = false, pick_results_component = false, -) - clear_selection( - gui; clear_topo = pick_topo_component, clear_results = pick_results_component, - ) +function pick_component!(gui::GUI, ::Nothing, ax_type::Symbol) + clear_selection(gui, ax_type) end """ - clear_selection(gui::GUI; clear_topo=true, clear_results=true) + clear_selection(gui::GUI, ax_type::Symbol) -Clear the color selection of components within 'get_design(gui)' instance and reset the -`get_selected_systems(gui)` variable. +Clear the color selection of the topology axis if `ax_type = :topo`, and of the results axis +if `ax_type = :results`. """ -function clear_selection(gui::GUI; clear_topo = true, clear_results = true) - if clear_topo +function clear_selection(gui::GUI, ax_type::Symbol) + if ax_type == :topo selected_systems = get_selected_systems(gui) for selection ∈ selected_systems toggle_selection_color!(gui, selection, false) end empty!(selected_systems) update_available_data_menu!(gui, nothing) # Make sure the menu is updated - end - if clear_results + elseif ax_type == :results time_axis = get_menu(gui, :time).selection[] for selection ∈ get_selected_plots(gui, time_axis) selection[:selected] = false @@ -234,8 +203,8 @@ function initialize_available_data!(gui) system = get_system(design) model = get_model(gui) plotables = [nothing; vcat(get_elements_vec(system))...] # `nothing` here represents no selection - gui.vars[:available_data] = Dict{Any,Vector{Dict{Symbol,Any}}}( - element => Vector{Dict{Symbol,Any}}() for element ∈ plotables + gui.vars[:available_data] = Dict{Any,Vector{PlotContainer}}( + element => Vector{PlotContainer}() for element ∈ plotables ) # Find appearances of node/area/link/transmission in the model @@ -261,12 +230,11 @@ function initialize_available_data!(gui) element = mode_to_transmission[element] end - container = Dict( - :name => string(sym), - :is_jump_data => true, - :selection => selection, - :field_data => field_data, - :description => create_description(gui, "variables.$sym"), + container = JuMPContainer( + string(sym), + selection, + field_data, + create_description(gui, "variables.$sym"), ) push!(get_available_data(gui)[element], container) end @@ -294,12 +262,11 @@ function initialize_available_data!(gui) tot_opex .+= opex # add opex_field to available data - container = Dict( - :name => "opex_strategic", - :is_jump_data => false, - :selection => [element], - :field_data => StrategicProfile(opex), - :description => description, + container = GlobalDataContainer( + "opex_strategic", + [element], + StrategicProfile(opex), + description, ) push!(get_available_data(gui)[element], container) end @@ -323,12 +290,11 @@ function initialize_available_data!(gui) tot_capex .+= capex # add opex_field to available data - container = Dict( - :name => "capex_strategic", - :is_jump_data => false, - :selection => [element], - :field_data => StrategicProfile(capex), - :description => description, + container = GlobalDataContainer( + "capex_strategic", + [element], + StrategicProfile(capex), + description, ) push!(get_available_data(gui)[element], container) end @@ -339,12 +305,11 @@ function initialize_available_data!(gui) if scale_tot_opex description *= " (scaled to strategic period)" end - container = Dict( - :name => "tot_opex", - :is_jump_data => false, - :selection => [element], - :field_data => StrategicProfile(tot_opex), - :description => description, + container = GlobalDataContainer( + "tot_opex", + [element], + StrategicProfile(tot_opex), + description, ) push!(get_available_data(gui)[element], container) @@ -353,12 +318,11 @@ function initialize_available_data!(gui) if scale_tot_capex description *= " (scaled to year)" end - container = Dict( - :name => "tot_capex", - :is_jump_data => false, - :selection => [element], - :field_data => StrategicProfile(tot_capex), - :description => description, + container = GlobalDataContainer( + "tot_capex", + [element], + StrategicProfile(tot_capex), + description, ) push!(get_available_data(gui)[element], container) @@ -411,7 +375,7 @@ function initialize_available_data!(gui) for element ∈ plotables # Add timedependent input data (if available) if !isnothing(element) - available_data = Vector{Dict}(undef, 0) + available_data = Vector{PlotContainer}(undef, 0) for field_name ∈ fieldnames(typeof(element)) field = getfield(element, field_name) structure = String(nameof(typeof(element))) @@ -670,16 +634,16 @@ function update_descriptive_names!(gui::GUI) end """ - select_data(name::String, menu) + select_data!(gui::GUI, name::String) -Select the data with name `name` from the `menu` +Select the data with name `name` from the `available_data` menu. """ function select_data!(gui::GUI, name::String) # Fetch the available data menu object menu = get_menu(gui, :available_data) # Fetch all menu options - available_data = [x[2][:name] for x ∈ collect(menu.options[])] + available_data = [get_name(x[2]) for x ∈ collect(menu.options[])] # Find menu number for data with name `name` i_selected = findfirst(x -> x == name, available_data) diff --git a/src/utils_GUI/event_functions.jl b/src/utils_GUI/event_functions.jl index b6c9f33..50a98f9 100644 --- a/src/utils_GUI/event_functions.jl +++ b/src/utils_GUI/event_functions.jl @@ -133,10 +133,10 @@ function define_event_functions(gui::GUI) ctrl_is_pressed = get_var(gui, :ctrl_is_pressed)[] if mouse_within_axis(ax_topo, mouse_pos) if !ctrl_is_pressed && !isempty(get_selected_systems(gui)) - clear_selection(gui; clear_results = false) + clear_selection(gui, :topo) end - pick_component!(gui; pick_topo_component = true) + pick_component!(gui, :topo) if time_difference < double_click_threshold notify(get_button(gui, :open).clicks) return Consume(true) @@ -150,9 +150,9 @@ function define_event_functions(gui::GUI) if mouse_within_axis(ax_results, mouse_pos) time_axis = time_menu.selection[] if !ctrl_is_pressed && !isempty(get_selected_plots(gui, time_axis)) - clear_selection(gui; clear_topo = false) + clear_selection(gui, :results) end - pick_component!(gui; pick_results_component = true) + pick_component!(gui, :results) gui.vars[:autolimits][time_axis] = false return Consume(false) end @@ -250,7 +250,7 @@ function define_event_functions(gui::GUI) expand_all = get_var(gui, :expand_all), ) update_title!(gui) - clear_selection(gui) + clear_selection(gui, :topo) notify(get_button(gui, :reset_view).clicks) end end @@ -259,7 +259,7 @@ function define_event_functions(gui::GUI) # Navigate up button: Handle click on the navigate up button (go back to the root_design) on(get_button(gui, :up).clicks; priority = 10) do clicks - if !isa(get_parent(get_design(gui)), NothingElement) + if !isa(get_parent(get_system(gui)), NothingElement) get_vars(gui)[:expand_all] = get_toggle(gui, :expand_all).active[] plot_design!( gui, get_design(gui); visible = false, @@ -334,7 +334,7 @@ function define_event_functions(gui::GUI) selection[:visible] = false selection[:pinned] = false end - clear_selection(gui; clear_topo = false) + clear_selection(gui, :results) update_legend!(gui) return Consume(false) end diff --git a/src/utils_GUI/results_axis_utils.jl b/src/utils_GUI/results_axis_utils.jl index d3dc2d2..6427bfb 100644 --- a/src/utils_GUI/results_axis_utils.jl +++ b/src/utils_GUI/results_axis_utils.jl @@ -28,7 +28,7 @@ end key_str::String, pre_desc::String, selection::Vector, - available_data::Vector{Dict}, + available_data::Vector{PlotContainer}, gui::GUI, ) @@ -40,15 +40,14 @@ function add_description!( key_str::String, pre_desc::String, selection::Vector, - available_data::Vector{Dict}, + available_data::Vector{PlotContainer}, gui::GUI, ) - container = Dict( - :name => name, - :is_jump_data => false, - :selection => selection, - :field_data => field, - :description => create_description(gui, key_str; pre_desc), + container = CaseDataContainer( + name, + selection, + field, + create_description(gui, key_str; pre_desc), ) push!(available_data, container) end @@ -60,7 +59,7 @@ end key_str::String, pre_desc::String, selection::Vector, - available_data::Vector{Dict}, + available_data::Vector{PlotContainer}, gui::GUI, ) @@ -73,7 +72,7 @@ function add_description!( key_str::String, pre_desc::String, selection::Vector, - available_data::Vector{Dict}, + available_data::Vector{PlotContainer}, gui::GUI, ) for (dictname, dictvalue) ∈ field @@ -100,7 +99,7 @@ end key_str::String, pre_desc::String, selection::Vector, - available_data::Vector{Dict}, + available_data::Vector{PlotContainer}, gui::GUI, ) @@ -113,7 +112,7 @@ function add_description!( key_str::String, pre_desc::String, selection::Vector, - available_data::Vector{Dict}, + available_data::Vector{PlotContainer}, gui::GUI, ) for data ∈ field @@ -133,7 +132,7 @@ end ::String, pre_desc::String, element, - available_data::Vector{Dict}, + available_data::Vector{PlotContainer}, gui::GUI, ) @@ -146,7 +145,7 @@ function add_description!( key_str::String, pre_desc::String, selection::Vector, - available_data::Vector{Dict}, + available_data::Vector{PlotContainer}, gui::GUI, ) structure = get_nth_field(key_str, '.', 3) @@ -169,7 +168,7 @@ end """ get_data( model::Union{JuMP.Model, Dict}, - selection::Dict{Symbol, Any}, + selection::PlotContainer, T::TS.TimeStructure, sp::Int64, rp::Int64, @@ -180,12 +179,12 @@ Get the values from the JuMP `model`, or the input data, at `selection` for all restricted to strategic period `sp`, representative period `rp`, and scenario `sc`. """ function get_data( - model::Union{JuMP.Model,Dict}, selection::Dict, T::TS.TimeStructure, sp::Int64, + model::Union{JuMP.Model,Dict}, selection::PlotContainer, T::TS.TimeStructure, sp::Int64, rp::Int64, sc::Int64, ) - field_data = selection[:field_data] - if selection[:is_jump_data] - sym = Symbol(selection[:name]) + field_data = get_field_data(selection) + if isa(selection, JuMPContainer) + sym = Symbol(get_name(selection)) i_T, type = get_time_axis(model[sym]) else type = nested_eltype(field_data) @@ -296,31 +295,26 @@ function get_jump_axis_types(data::DataFrame) end """ - create_label(selection::Vector{Any}) + create_label(selection::PlotContainer) Return a label for a given `selection` to be used in the get_menus(gui)[:available_data] menu. """ -function create_label(selection::Dict{Symbol,Any}) - label::String = - (selection[:is_jump_data] || isempty(selection[:name])) ? "" : "Case data: " - if haskey(selection, :description) - if isempty(selection[:name]) - label *= selection[:description] - else - label *= selection[:description] * " ($(selection[:name]))" - end +function create_label(selection::PlotContainer) + label::String = isa(selection, CaseDataContainer) ? "Case data: " : "" + if isempty(get_name(selection)) + label *= get_description(selection) else - label *= selection[:name] + label *= get_description(selection) * " ($(get_name(selection)))" end otherRes::Bool = false - for select ∈ selection[:selection] + for select ∈ get_selection(selection) if isa(select, Resource) if !otherRes label *= " (" otherRes = true end label *= "$(select)" - if select != selection[:selection][end] + if select != get_selection(selection)[end] label *= ", " end end @@ -383,11 +377,8 @@ function update_plot!(gui::GUI, element) selection = available_data_menu.selection[] if !isnothing(selection) && selection != "no options" xlabel = "Time" - if haskey(selection, :description) - ylabel = selection[:description] - else - ylabel = selection[:name] - end + ylabel = get_description(selection) + sp = period_menu.selection[] rp = representative_period_menu.selection[] sc = scenario_menu.selection[] @@ -480,18 +471,17 @@ function update_plot!(gui::GUI, element) n_visible = length(get_visible_data(gui, time_axis)) + 1 colormap = get_var(gui, :colormap) i = (n_visible - 1 % length(colormap)) + 1 - color = Observable(parse(Colorant, colormap[i])) + color = colormap[i] if time_axis == :results_op plot = stairs!(ax, points; step = :pre, label = label, color = color) plot.color = color - plot.plots[1].color = color else plot = barplot!( ax, points; dodge = n_visible * ones(Int, length(points)), n_dodge = n_visible, - strokecolor = :black, + strokecolor = BLACK, strokewidth = 1, label = label, color = color, @@ -499,12 +489,11 @@ function update_plot!(gui::GUI, element) end new_data = Dict( :plot => plot, - :name => selection[:name], - :selection => selection[:selection], + :name => get_name(selection), + :selection => get_selection(selection), :t => periods, :y => y_values, - :color => color[], - :color_obs => color, + :color => color, :pinned => false, :visible => true, :selected => false, @@ -520,8 +509,8 @@ function update_plot!(gui::GUI, element) plot[1][] = points plot.visible[] = true # If it has been hidden after a "Remove Plot" action plot.label[] = label - overwritable[:name] = selection[:name] - overwritable[:selection] = selection[:selection] + overwritable[:name] = get_name(selection) + overwritable[:selection] = get_selection(selection) overwritable[:t] = periods overwritable[:y] = y_values overwritable[:pinned] = false @@ -533,13 +522,10 @@ function update_plot!(gui::GUI, element) overwritable[:xticks] = xticks end update_barplot_dodge!(gui) - if all(y_values .≈ 0) && !(time_axis == :results_op) - # Deactivate inspector for bars to avoid issue with wireframe when selecting - # a bar with values being zero - toggle_inspector!(plot, false) - else - toggle_inspector!(plot, true) - end + + # Deactivate inspector for bars to avoid issue with wireframe when selecting + # a bar with values being zero + plot.inspectable = !(all(y_values .≈ 0) && !(time_axis == :results_op)) if isnothing(get_results_legend(gui)) # Initialize the legend box gui.legends[:results] = axislegend( @@ -610,44 +596,14 @@ end """ update_limits!(ax::Axis) -Adjust limits automatically to take into account legend and machine epsilon issues. +Adjust limits automatically to avoid legend box overlapping the data. """ function update_limits!(ax::Axis) - # Fetch all y-values in the axis - barplots = getfirst(x -> isa(x, Makie.BarPlot) && x.visible[], ax.scene.plots) - if isnothing(barplots) - xy = vcat([p[1][] for p ∈ get_vis_plots(ax)]...) - y = [pt[2] for pt ∈ xy] - if isempty(y) - return nothing - end - x = [pt[1] for pt ∈ xy] - - # Calculate the width of distribution of the data in the vertical direction - max_x = maximum(x) - min_x = minimum(x) - max_y = maximum(y) - min_y = minimum(y) - ywidth = max_y - min_y - xwidth = max_x - min_x - - # Do the following for data with machine epsilon precision noice around zero that causes - # the warning "Warning: No strict ticks found" and the the bug related to issue #4266 in Makie - if abs(ywidth) < 1e-13 - ywidth = 2 * max(1.0, max_y) - yorigin = 0.0 - else - yorigin = min_y - ywidth * 0.04 - ywidth += 2 * ywidth * 0.04 - end + autolimits!(ax) + yorigin = ax.finallimits[].origin[2] + ywidth = ax.finallimits[].widths[2] - xlims!(ax, min_x - xwidth * 0.04, max_x + xwidth * 0.04) - else - autolimits!(ax) - yorigin = ax.finallimits[].origin[2] - ywidth = ax.finallimits[].widths[2] - end - # try to avoid legend box overlapping data + # try to avoid legend box overlapping the plots ylims!(ax, yorigin, yorigin + ywidth * 1.1) end diff --git a/src/utils_GUI/topo_axis_utils.jl b/src/utils_GUI/topo_axis_utils.jl index b45b578..2d38c72 100644 --- a/src/utils_GUI/topo_axis_utils.jl +++ b/src/utils_GUI/topo_axis_utils.jl @@ -148,11 +148,17 @@ function plot_design!( if get_design(gui) == design update_distances!(gui) end - for component ∈ get_components(design), plot ∈ component.plots + for component ∈ get_components(design), plot ∈ get_plots(component) plot.visible = visible end - for connection ∈ get_connections(design), plots ∈ get_plots(connection), plot ∈ plots - plot.visible = visible + for connection ∈ get_connections(design), plots ∈ get_plots(connection) + if isa(plots, Makie.AbstractPlot) # handle the arrowheads (scatter! object) + plots.visible = visible + else # handle the lines (vector of line! objects) + for plot_sub ∈ plots + plot_sub.visible = visible + end + end end end @@ -162,43 +168,7 @@ end Draws lines between connected nodes/areas in GUI `gui` using EnergySystemDesign `design`. """ function connect!(gui::GUI, design::EnergySystemDesign) - # Find optimal placement of label by finding the wall that has the least number of connections connections = get_connections(design) - components = get_components(design) - for component ∈ components - linked_to_component::Vector{Connection} = filter( - x -> get_parent(get_system(component)).id == get_element(x).to.id, - connections, - ) - linked_from_component::Vector{Connection} = filter( - x -> get_parent(get_system(component)).id == get_element(x).from.id, - connections, - ) - on(component.xy; priority = 4) do _ - angles::Vector{Float32} = vcat( - [ - angle(component, linked_component.from) for - linked_component ∈ linked_to_component - ], - [ - angle(component, linked_component.to) for - linked_component ∈ linked_from_component - ], - ) - min_angle_diff::Vector{Float32} = fill(Inf, 4) - for i ∈ eachindex(min_angle_diff) - for angle ∈ angles - Δθ::Float32 = angle_difference(angle, (i - 1) * Float32(π) / 2) - if min_angle_diff[i] > Δθ - min_angle_diff[i] = Δθ - end - end - end - walls::Vector{Symbol} = [:E, :N, :W, :S] - component.wall[] = walls[argmax(min_angle_diff)] - end - notify(component.xy) - end for conn ∈ connections # Check if link between two elements goes in both directions (two_way) @@ -239,25 +209,28 @@ function connect!(gui::GUI, connection::Connection, two_way::Bool) end # Allocate and store objects - line_connections::Vector{Any} = Vector{Any}(undef, 0) - arrow_heads::Vector{Any} = Vector{Any}(undef, 0) - push!(get_plots(connection), line_connections) - push!(get_plots(connection), arrow_heads) linestyle = get_linestyle(gui, connection) + linewidth = get_var(gui, :connection_linewidth) + markersize = get_var(gui, :markersize) + two_way_sep_px = get_var(gui, :two_way_sep_px) + line_sep_px = get_var(gui, :line_sep_px) + parent_scaling = get_var(gui, :parent_scaling) from_xy = connection.from.xy to_xy = connection.to.xy Δh = get_var(gui, :Δh) - for j ∈ 1:no_colors - triple = @lift begin + triple = @lift begin + xy_midpoints = Vector{Point2f}(fill(Point2f(0.0f0, 0.0f0), no_colors)) + θs = Vector{Float32}(fill(0.0f0, no_colors)) + pts_lines = + Vector{Vector{Point2f}}(fill(fill(Point2f(0.0f0, 0.0f0), 2), no_colors)) + for j ∈ 1:no_colors lines_shift::Point2f = - pixel_to_data(gui, get_var(gui, :connection_linewidth)) .+ - pixel_to_data(gui, get_var(gui, :line_sep_px)) - two_way_sep::Point2f = pixel_to_data(gui, get_var(gui, :two_way_sep_px)) - markersize_lengths::Point2f = pixel_to_data( - gui, get_var(gui, :markersize), - ) + pixel_to_data(gui, linewidth) .+ + pixel_to_data(gui, line_sep_px) + two_way_sep::Point2f = pixel_to_data(gui, two_way_sep_px) + markersize_lengths::Point2f = pixel_to_data(gui, markersize) xy_1::Point2f = $from_xy xy_2::Point2f = $to_xy @@ -267,23 +240,21 @@ function connect!(gui::GUI, connection::Connection, two_way::Bool) sinθ::Float32 = sin(θ) cosϕ::Float32 = -sinθ # where ϕ = θ+π/2 sinϕ::Float32 = cosθ + + # Create directional vectors in the direction of θ and ϕ dirϕ::Point2f = Point2f(cosϕ, sinϕ) dirθ::Point2f = Point2f(cosθ, sinθ) Δ::Float32 = $Δh / 2 # half width of a box if !isempty(get_components(connection.from)) - Δ *= get_var(gui, :parent_scaling) + Δ *= parent_scaling end - xy_start::Point2f = xy_1 - xy_end::Point2f = xy_2 - xy_midpoint::Point2f = xy_2 # The midpoint of the end of all lines (for arrow head) - - xy_start += (j - 1) * lines_shift .* dirϕ - xy_end += (j - 1) * lines_shift .* dirϕ - xy_midpoint += (no_colors - 1) / 2 * lines_shift .* dirϕ + xy_start::Point2f = xy_1 + (j - 1) * lines_shift .* dirϕ + xy_end::Point2f = xy_2 + (j - 1) * lines_shift .* dirϕ + xy_midpoint::Point2f = xy_2 + (no_colors - 1) / 2 * lines_shift .* dirϕ # The midpoint of the end of all lines (for arrow head) - if two_way + if two_way # separate the opposite directed lines by two_way_sep xy_start += two_way_sep / 2 .* dirϕ xy_end += two_way_sep / 2 .* dirϕ xy_midpoint += two_way_sep / 2 .* dirϕ @@ -295,42 +266,53 @@ function connect!(gui::GUI, connection::Connection, two_way::Bool) -xy_start[1] * cosθ - xy_start[2] * sinθ + xy_midpoint[1] * cosθ + xy_midpoint[2] * sinθ - minimum(markersize_lengths) - pts_line = Point2f[xy_start, parm*dirθ+xy_start] - # return the objects into a tuple Observable - (xy_midpoint, θ, pts_line) + xy_midpoints[j] = xy_midpoint + θs[j] = θ + pts_lines[j] = Point2f[xy_start, parm*dirθ+xy_start] end - xy_midpoint_j = @lift $triple[1] - θⱼ = @lift $triple[2] - pts_line_j = @lift $triple[3] - sctr = scatter!( - get_axes(gui)[:topo], - xy_midpoint_j; - marker = arrow_parts[j], - markersize = get_var(gui, :markersize), - rotation = θⱼ, - color = colors[j], - inspectable = false, - ) + # return the objects into a tuple Observable + (xy_midpoints, θs, pts_lines) + end + + # Extract observables from the tuple + xy_midpoints = @lift $triple[1] + θs = @lift $triple[2] + + ax = get_ax(gui, :topo) + sctr = scatter!( + ax, + xy_midpoints; + marker = arrow_parts, + markersize = get_var(gui, :markersize), + rotation = θs, + color = colors, + inspectable = false, + ) + Makie.translate!(sctr, 0, 0, get_var(gui, :z_translate_lines)) + sctr.kw[:EMGUI_obj] = connection + push!(get_plots(connection), sctr) + + lns_arr::Vector{Makie.AbstractPlot} = Makie.AbstractPlot[] # to store the line plots + + for j ∈ 1:no_colors + pts_lines = @lift $triple[3][j] lns = lines!( - get_axes(gui)[:topo], - pts_line_j; + ax, + pts_lines; color = colors[j], - linewidth = get_var(gui, :connection_linewidth), + linewidth = linewidth, linestyle = linestyle[j], inspector_label = (self, i, p) -> get_hover_string(connection), inspectable = true, ) - Makie.translate!(sctr, 0, 0, get_var(gui, :z_translate_lines)) Makie.translate!(lns, 0, 0, get_var(gui, :z_translate_lines)) - - sctr.kw[:EMGUI_obj] = connection lns.kw[:EMGUI_obj] = connection - - push!(arrow_heads, sctr) - push!(line_connections, lns) + push!(lns_arr, lns) end + push!(get_plots(connection), lns_arr) + get_vars(gui)[:z_translate_lines] += 0.0001f0 end @@ -366,7 +348,7 @@ end Get the line style for an Connection `connection` based on its properties. """ function get_linestyle(gui::GUI, connection::Connection) - # Check of connection is a transmission + # Check if connection is a transmission linestyles = get_linestyle(gui, get_element(connection)) if !isempty(linestyles) return linestyles @@ -416,13 +398,14 @@ function draw_box!(gui::GUI, design::EnergySystemDesign) white_rect2 = poly!( get_axes(gui)[:topo], rect; - color = :white, - inspectable = false, + color = WHITE, + inspectable = true, strokewidth = get_var(gui, :linewidth), strokecolor = design.color, linestyle = linestyle, ) # Create a white background rectangle to hide lines from connections + add_inspector_to_poly!(white_rect2, (self, i, p) -> get_hover_string(design)) Makie.translate!(white_rect2, 0.0f0, 0.0f0, get_var(gui, :z_translate_components)) get_vars(gui)[:z_translate_components] += 0.0001f0 push!(design.plots, white_rect2) @@ -435,8 +418,8 @@ function draw_box!(gui::GUI, design::EnergySystemDesign) white_rect = poly!( get_axes(gui)[:topo], rect; - color = :white, - inspectable = false, + color = WHITE, + inspectable = true, strokewidth = get_var(gui, :linewidth), strokecolor = design.color, linestyle = linestyle, @@ -456,67 +439,78 @@ end Draw an icon for EnergySystemDesign `design`. """ function draw_icon!(gui::GUI, design::EnergySystemDesign) + ax = get_axes(gui)[:topo] if isempty(design.icon) # No path to an icon has been found node::EMB.Node = get_ref_element(design) - colors_input::Vector{RGB} = get_resource_colors( + colors_input::Vector{RGBA{Float32}} = get_resource_colors( inputs(node), design.id_to_color_map, ) - colors_output::Vector{RGB} = get_resource_colors( + colors_output::Vector{RGBA{Float32}} = get_resource_colors( outputs(node), design.id_to_color_map, ) - geometry::Symbol = if isa(node, Source) - :rect + all_colors = vcat(colors_input, colors_output) + no_circle_points::Int64 = 100 + geometry::Symbol, no_points::Int64 = if isa(node, Source) + (:rect, 5) elseif isa(node, Sink) - :circle + (:circle, no_circle_points+2) else # assume NetworkNode - :triangle + (:triangle, 4) end - for (j, colors) ∈ enumerate([colors_input, colors_output]) - no_colors::Int64 = length(colors) - for (i, color) ∈ enumerate(colors) - θᵢ::Float32 = 0.0f0 - θᵢ₊₁::Float32 = 0.0f0 - - # Check if node is a NetworkNode (if so, devide disc into two where - # left side is for input and right side is for output) - if isa(node, NetworkNode) - θᵢ = (-1)^(j + 1) * π / 2 + π * (i - 1) / no_colors - θᵢ₊₁ = (-1)^(j + 1) * π / 2 + π * i / no_colors - else - θᵢ = 2π * (i - 1) / no_colors - θᵢ₊₁ = 2π * i / no_colors - end - Δh = get_var(gui, :Δh) - xy = design.xy - sector = @lift get_sector_points(; - c = $xy, - Δ = $Δh * get_var(gui, :icon_scale) / 2, - θ₁ = θᵢ, - θ₂ = θᵢ₊₁, - geometry = geometry, - ) - - network_poly = poly!( - get_axes(gui)[:topo], sector; color = color, inspectable = false, - ) - if isa(node, Sink) || isa(node, Source) - add_inspector_to_poly!( - network_poly, (self, i, p) -> get_hover_string(design), + + no_polygons::Int64 = length(all_colors) + xy = design.xy + Δh = get_var(gui, :Δh) + icon_scale = get_var(gui, :icon_scale) + node_isa_networknode::Bool = isa(node, NetworkNode) + + poly_points_obs::Observable{Vector{Vector{Point2f}}} = @lift begin + poly_points::Vector{Vector{Point2f}} = Vector{Vector{Point2f}}( + fill(fill(Point2f(0, 0), no_points), no_polygons), + ) + idx = 1 + for (j, colors) ∈ enumerate([colors_input, colors_output]) + no_colors::Int64 = length(colors) + for i ∈ 1:no_colors + θᵢ::Float32 = 0.0f0 + θᵢ₊₁::Float32 = 0.0f0 + + # Check if node is a NetworkNode (if so, devide disc into two where + # left side is for input and right side is for output) + if node_isa_networknode + θᵢ = (-1)^(j + 1) * π / 2 + π * (i - 1) / no_colors + θᵢ₊₁ = (-1)^(j + 1) * π / 2 + π * i / no_colors + else + θᵢ = 2π * (i - 1) / no_colors + θᵢ₊₁ = 2π * i / no_colors + end + poly_points[idx] = get_sector_points(; + c = $xy, + Δ = $Δh * icon_scale / 2, + θ₁ = θᵢ, + θ₂ = θᵢ₊₁, + geometry = geometry, + steps = no_circle_points, ) + idx += 1 end - Makie.translate!( - network_poly, - 0.0f0, - 0.0f0, - get_var(gui, :z_translate_components), - ) - network_poly.kw[:EMGUI_obj] = design - push!(design.plots, network_poly) end + poly_points end - if isa(node, NetworkNode) + polys = poly!(ax, poly_points_obs; color = all_colors, inspectable = true) + add_inspector_to_poly!(polys, (self, i, p) -> get_hover_string(design)) + Makie.translate!( + polys, + 0.0f0, + 0.0f0, + get_var(gui, :z_translate_components), + ) + polys.kw[:EMGUI_obj] = design + push!(design.plots, polys) + + if node_isa_networknode # Add a center box to separate input resources from output resources Δh = get_var(gui, :Δh) xy = design.xy @@ -527,10 +521,10 @@ function draw_icon!(gui::GUI, design::EnergySystemDesign) end center_box = poly!( - get_axes(gui)[:topo], + ax, box; - color = :white, - inspectable = false, + color = WHITE, + inspectable = true, strokewidth = get_var(gui, :linewidth), ) @@ -555,11 +549,12 @@ function draw_icon!(gui::GUI, design::EnergySystemDesign) yo_image = @lift ($xy[2] - $Δh * scale / 2) .. ($xy[2] + $Δh * scale / 2) icon_image = image!( - get_axes(gui)[:topo], + ax, xo_image, yo_image, rotr90(FileIO.load(design.icon)); - inspectable = false, + inspectable = true, + inspector_label = (self, i, p) -> get_hover_string(design), ) Makie.translate!(icon_image, 0.0f0, 0.0f0, get_var(gui, :z_translate_components)) icon_image.kw[:EMGUI_obj] = design @@ -574,44 +569,72 @@ end Add a label to an `EnergySystemDesign` component. """ function draw_label!(gui::GUI, component::EnergySystemDesign) + connections = get_connections(get_parent(component)) + id = get_parent(get_system(component)).id + linked_to_component::Vector{Connection} = filter( + x -> id == get_element(x).to.id, + connections, + ) + linked_from_component::Vector{Connection} = filter( + x -> id == get_element(x).from.id, + connections, + ) + scale = 0.7 Δh = get_var(gui, :Δh) xy = component.xy + walls::Vector{Symbol} = [:E, :N, :W, :S] + # Find optimal placement of label by finding the wall that has the least number of connections tuple = @lift begin - shift_xy = if component.wall[] == :E + angles::Vector{Float32} = vcat( + [ + angle(component, linked_component.from) for + linked_component ∈ linked_to_component + ], + [ + angle(component, linked_component.to) for + linked_component ∈ linked_from_component + ], + ) + min_angle_diff::Vector{Float32} = fill(Inf, 4) + for i ∈ eachindex(min_angle_diff) + for angle ∈ angles + Δθ::Float32 = angle_difference(angle, (i - 1) * Float32(π) / 2) + if min_angle_diff[i] > Δθ + min_angle_diff[i] = Δθ + end + end + end + wall = walls[argmax(min_angle_diff)] + shift_xy = if wall == :E Point2f($Δh * scale, 0.0f0) - elseif component.wall[] == :S + elseif wall == :S Point2f(0.0f0, -$Δh * scale) - elseif component.wall[] == :W + elseif wall == :W Point2f(-$Δh * scale, 0.0f0) - elseif component.wall[] == :N + elseif wall == :N Point2f(0.0f0, $Δh * scale) end xy_label = $xy + shift_xy - alignment = get_text_alignment(component.wall[]) + alignment = get_text_alignment(wall) (xy_label, alignment) end # Extract observables from the tuple - xy_label_o = @lift $tuple[1] - alignment_o = @lift $tuple[2] + xy_label_obs = @lift $tuple[1] + alignment_obs = @lift $tuple[2] node = get_element(component) - if has_invested(component) - font_color = :red - else - font_color = :black - end label_text = text!( get_axes(gui)[:topo], - xy_label_o; + xy_label_obs; text = get_element_label(node), - align = alignment_o, + align = alignment_obs, fontsize = get_var(gui, :fontsize), inspectable = false, - color = font_color, + color = has_invested(component) ? RED : BLACK, ) Makie.translate!(label_text, 0.0f0, 0.0f0, get_var(gui, :z_translate_components)) get_vars(gui)[:z_translate_components] += 0.0001f0 @@ -683,7 +706,7 @@ end Update the title of `get_axes(gui)[:topo]` based on `get_design(gui)`. """ function update_title!(gui::GUI) - parent = get_parent(gui) + parent = get_parent(get_system(gui)) title_obs = get_var(gui, :title) title_obs[] = if isa(parent, NothingElement) "top_level" diff --git a/src/utils_gen/export_utils.jl b/src/utils_gen/export_utils.jl index 5883777..bbe0659 100644 --- a/src/utils_gen/export_utils.jl +++ b/src/utils_gen/export_utils.jl @@ -280,10 +280,10 @@ function export_to_repl(gui::GUI) t = vis_plots[1][:t] data = Matrix{Any}(undef, length(t), length(vis_plots) + 1) data[:, 1] = t - header = ( + header = [ Vector{Any}(undef, length(vis_plots) + 1), Vector{Any}(undef, length(vis_plots) + 1), - ) + ] header[1][1] = "t" header[2][1] = "(" * string(nameof(eltype(t))) * ")" for (j, vis_plot) ∈ enumerate(vis_plots) @@ -292,7 +292,7 @@ function export_to_repl(gui::GUI) header[2][j+1] = join([string(x) for x ∈ vis_plots[j][:selection]], ", ") end println("\n") # done in order to avoid the prompt shifting the topspline of the table - pretty_table(data; header = header) + pretty_table(data; column_labels = header) end else model = get_model(gui) diff --git a/src/utils_gen/structures_utils.jl b/src/utils_gen/structures_utils.jl index baa45f4..5fa8809 100644 --- a/src/utils_gen/structures_utils.jl +++ b/src/utils_gen/structures_utils.jl @@ -19,7 +19,10 @@ end Get a list of loaded packages. """ -loaded() = [String(n) for n ∈ names(Main, imported = true) if getfield(Main, n) isa Module] +loaded() = [ + String(n) for n ∈ names(Main, imported = true) if + isdefined(Main, n) && getfield(Main, n) isa Module +] """ place_nodes_in_circle(total_nodes::Int64, current_node::Int64, r::Float32, xₒ::Float32, yₒ::Float32) @@ -34,6 +37,19 @@ function place_nodes_in_circle(n::Int64, i::Int64, r::Float32, xₒ::Float32, y return x, y end +""" + parse_color(type::Type, color::Colorant) + parse_color(type::Type, color::Any) + +Parse `color` into the type `type`. +""" +function parse_color(type::Type, color::Colorant) + return type(color) +end +function parse_color(type::Type, color::Any) + return parse(type, color) +end + """ set_colors(products::Vector{<:Resource}, id_to_color_map::Dict) @@ -68,25 +84,16 @@ function set_colors(products::Vector{<:Resource}, id_to_color_map::Dict) ) # Create a seed based on the existing colors - seed::Vector{RGB} = [ - parse(Colorant, hex_color) for hex_color ∈ values(complete_id_to_color_map) + seed::Vector{RGB{Float32}} = [ + parse_color(RGB{Float32}, color) for color ∈ values(complete_id_to_color_map) ] # Add non-desired colors to the seed - foul_colors = [ - "#FFFF00", # Yellow - "#FF00FF", # Magenta - "#00FFFF", # Cyan - "#00FF00", # Green - "#000000", # Black - "#FFFFFF", # White - ] - for color ∈ values(foul_colors) - push!(seed, parse(Colorant, color)) - end + non_desired_colors = [:yellow, :magenta, :cyan, :green1, :black, :white] + append!(seed, parse_color(RGB{Float32}, color) for color ∈ non_desired_colors) # Create new colors for the missing resources - products_colors::Vector{RGB} = distinguishable_colors( + products_colors::Vector{RGBA{Float32}} = distinguishable_colors( length(missing_product_colors), seed; dropseed = true, ) @@ -185,14 +192,13 @@ through `id_to_icon_map`. function find_icon(system::AbstractSystem, id_to_icon_map::Dict) icon::String = "" if !isempty(id_to_icon_map) - supertype::DataType = find_type_field(id_to_icon_map, get_parent(system)) - if haskey(id_to_icon_map, get_parent(system).id) - icon = id_to_icon_map[get_parent(system).id] - elseif supertype != Nothing - icon = id_to_icon_map[supertype] - else - @warn("Could not find $(get_parent(system).id) in id_to_icon_map \ - nor the type $(typeof(get_parent(system))). Using default setup instead") + parent::AbstractElement = get_parent(system) + type::DataType = find_type_field(id_to_icon_map, parent) + id = parent.id + if haskey(id_to_icon_map, id) + icon = id_to_icon_map[id] + elseif type != Nothing + icon = id_to_icon_map[type] end end return icon @@ -244,66 +250,14 @@ function save_design(design_dict::Dict, file::String) return YAML.write_file(file, design_dict) end -""" - get_linked_nodes!( - node::EMB.Node, - links::Vector{Link}, - area_links::Vector{Link}, - area_nodes::Vector{EMB.Node}, - indices::Vector{Int}, - ) - -Recursively find all nodes connected (directly or indirectly) to `node` in a system of `links` -and store the found links in `area_links` and nodes in `area_nodes`. - -Here, `indices` contains the indices where the next link and node is to be stored, -respectively. -""" -function get_linked_nodes!( - node::EMB.Node, - links::Vector{Link}, - area_links::Vector{Link}, - area_nodes::Vector{EMB.Node}, - indices::Vector{Int}, -) - for link ∈ links - if node ∈ [link.from, link.to] && - (indices[1] == 1 || !(link ∈ area_links[1:(indices[1]-1)])) - area_links[indices[1]] = link - indices[1] += 1 - - new_node_added::Bool = false - if node == link.from && !(link.to ∈ area_nodes[1:(indices[2]-1)]) - area_nodes[indices[2]] = link.to - new_node_added = true - elseif node == link.to && !(link.from ∈ area_nodes[1:(indices[2]-1)]) - area_nodes[indices[2]] = link.from - new_node_added = true - end - - # Recursively add other nodes - if new_node_added - indices[2] += 1 - get_linked_nodes!( - area_nodes[indices[2]-1], - links, - area_links, - area_nodes, - indices, - ) - end - end - end -end - """ get_resource_colors(resources::Vector{Resource}, id_to_color_map::Dict{Any,Any}) Get the colors linked the the resources in `resources` based on the mapping `id_to_color_map`. """ function get_resource_colors(resources::Vector{<:Resource}, id_to_color_map::Dict{Any,Any}) - hexColors::Vector{Any} = [id_to_color_map[resource.id] for resource ∈ resources] - return [parse(Colorant, hex_color) for hex_color ∈ hexColors] + colors::Vector{Any} = [id_to_color_map[resource.id] for resource ∈ resources] + return [parse_color(RGBA{Float32}, color) for color ∈ colors] end """ @@ -319,10 +273,10 @@ end """ get_resource_colors(::Vector{Any}, ::Dict{Any,Any}) -Return empty RGB vector for empty input. +Return empty RGBA{Float32} vector for empty input. """ function get_resource_colors(::Vector{Any}, ::Dict{Any,Any}) - return Vector{RGB}(undef, 0) + return Vector{RGBA{Float32}}(undef, 0) end """ diff --git a/src/utils_gen/topo_utils.jl b/src/utils_gen/topo_utils.jl index 49a951c..3bb642d 100644 --- a/src/utils_gen/topo_utils.jl +++ b/src/utils_gen/topo_utils.jl @@ -92,7 +92,7 @@ given the minimum and maximum coordinates `min_x`, `min_y`, `max_x`, and `max_y` function find_min_max_coordinates( design::EnergySystemDesign, min_x::Number, max_x::Number, min_y::Number, max_y::Number, ) - if !isa(get_parent(design), NothingElement) + if !isa(get_parent(get_system(design)), NothingElement) x, y = design.xy[][1], design.xy[][2] min_x = min(min_x, x) max_x = max(max_x, x) @@ -239,20 +239,6 @@ function get_sector_points(; end end -""" - toggle_inspector!(p::Makie.AbstractPlot, toggle::Bool) - -Toggle the inspector of a Makie plot `p` using the boolean `toggle`. -""" -function toggle_inspector!(p::Makie.AbstractPlot, toggle::Bool) - for p_sub ∈ p.plots - if :plots ∈ fieldnames(typeof(p_sub)) - toggle_inspector!(p_sub, toggle) - end - p_sub.inspectable[] = toggle - end -end - """ add_inspector_to_poly!(p::Makie.AbstractPlot, inspector_label::Function) @@ -264,6 +250,5 @@ function add_inspector_to_poly!(p::Makie.AbstractPlot, inspector_label::Function add_inspector_to_poly!(p_sub, inspector_label) end p_sub.inspector_label = inspector_label - p_sub.inspectable[] = true end end diff --git a/test/test_interactivity.jl b/test/test_interactivity.jl index 5c2846f..eac8aba 100644 --- a/test/test_interactivity.jl +++ b/test/test_interactivity.jl @@ -1,10 +1,5 @@ import EnergyModelsGUI: - get_root_design, - get_components, - get_connections, - get_menu, - get_button, - update!, + get_component, toggle_selection_color!, get_plots, get_selection_color, @@ -18,8 +13,6 @@ import EnergyModelsGUI: update_info_box!, update_available_data_menu!, update_sub_system_locations!, - pick_component!, - clear_selection, get_design, get_vis_plots, get_plotted_data, @@ -32,6 +25,11 @@ root_design = get_root_design(gui) components = get_components(root_design) connections = get_connections(root_design) +area1 = get_component(components, 1) +area2 = get_component(components, 2) +area3 = get_component(components, 3) +area4 = get_component(components, 4) + time_menu = get_menu(gui, :time) available_data_menu = get_menu(gui, :available_data) period_menu = get_menu(gui, :period) @@ -84,47 +82,50 @@ end # Test color toggling @testset "Toggle colors" begin - area1 = components[1] # fetch Area 1 - plt_area1 = area1.plots[1] - pick_component!(gui, plt_area1; pick_topo_component = true) + pick_component!(gui, get_plots(area1)[1], :topo) update!(gui) @test area1.color[] == get_selection_color(gui) - pick_component!(gui, nothing; pick_topo_component = true) - @test area1.color[] == :black + pick_component!(gui, nothing, :topo) # deselect + @test area1.color[] == EMGUI.BLACK - node2 = get_components(components[1])[2] # fetch node El 1 - plt_node2 = node2.plots[1] - pick_component!(gui, plt_node2; pick_topo_component = true) + node2 = get_component(area1, "El 1") # fetch node El 1 + pick_component!(gui, get_plots(node2)[1], :topo) update!(gui) @test node2.color[] == get_selection_color(gui) - pick_component!(gui, nothing; pick_topo_component = true) - @test node2.color[] == :black + pick_component!(gui, nothing, :topo) # deselect + @test node2.color[] == EMGUI.BLACK connection1 = connections[1] # fetch the Area 1 - Area 2 transmission - plt_connection1 = connection1.plots[1][1] - pick_component!(gui, plt_connection1; pick_topo_component = true) + plt_connection1 = get_plots(connection1)[1] + pick_component!(gui, plt_connection1, :topo) + update!(gui) + @test plt_connection1.color[][1] == get_selection_color(gui) + pick_component!(gui, nothing, :topo) # deselect update!(gui) - @test get_plots(connection1)[1][1].color[] == get_selection_color(gui) - pick_component!(gui, nothing; pick_topo_component = true) - @test get_plots(connection1)[1][1].color[] == connection1.colors[1] + @test plt_connection1.color[][1] == connection1.colors[1] - link1 = get_connections(components[1])[5] # fetch the link to heat pump - plt_link1 = link1.plots[1][1] - pick_component!(gui, plt_link1; pick_topo_component = true) + link1 = get_connections(area1)[5] # fetch the link to heat pump + plt_link1_sctr = get_plots(link1)[1] + pick_component!(gui, plt_link1_sctr, :topo) update!(gui) - @test get_plots(link1)[1][1].color[] == get_selection_color(gui) - pick_component!(gui, nothing; pick_topo_component = true) - for plot ∈ link1.plots + @test plt_link1_sctr.color[][1] == get_selection_color(gui) + pick_component!(gui, nothing, :topo) # deselect + for plot ∈ get_plots(link1) # This only tests one color (should probably add a test with more colors/`Resource`s) for (i, color) ∈ enumerate(link1.colors) - @test plot[i].color[] == color + if isa(plot, EMGUI.AbstractPlot) + @test plot.color[][i] == color + else + for plot_sub ∈ plot + @test plot_sub.color[] == color + end + end end end end # Test the open button functionality - area1 = components[1] # fetch Area 1 @testset "get_button(gui,:open).clicks" begin - pick_component!(gui, area1; pick_topo_component = true) # Select Area 1 + pick_component!(gui, area1, :topo) # Select Area 1 notify(get_button(gui, :open).clicks) # Open Area 1 @test get_var(gui, :title)[] == "top_level.Area 1" end @@ -136,22 +137,20 @@ end end # Test the align horz. button (aligning nodes horizontally) - area2 = components[2] # fetch Area 2 @testset "get_button(gui,:align_horizontal).clicks" begin - pick_component!(gui, area1; pick_topo_component = true) # Select Area 1 - pick_component!(gui, area2; pick_topo_component = true) # Select Area 2 + pick_component!(gui, area1, :topo) # Select Area 1 + pick_component!(gui, area2, :topo) # Select Area 2 notify(get_button(gui, :align_horizontal).clicks) # Align Area 1 and 2 horizontally - @test get_xy(components[1])[][2] == get_xy(components[2])[][2] + @test get_xy(area1)[][2] == get_xy(area2)[][2] end # Test the align vert. button (aligning nodes horizontally) - area3 = components[3] # fetch Area 3 - clear_selection(gui; clear_topo = true) + clear_selection(gui, :topo) @testset "get_button(gui,:align_vertical).clicks" begin - pick_component!(gui, area2; pick_topo_component = true) # Select Area 2 - pick_component!(gui, area3; pick_topo_component = true) # Select Area 3 + pick_component!(gui, area2, :topo) # Select Area 2 + pick_component!(gui, area3, :topo) # Select Area 3 notify(get_button(gui, :align_vertical).clicks) # Align Area 2 and 3 vertically - @test get_xy(components[2])[][1] == get_xy(components[3])[][1] + @test get_xy(area2)[][1] == get_xy(area3)[][1] end # Test the save button functionality @@ -163,9 +162,10 @@ end end notify(get_button(gui, :save).clicks) # click the save button area4_dict = YAML.load_file(joinpath(design_folder, "test_Area 4.yml")) - sub_components = get_components(components[4]) - @test area4_dict["n_Solar Power"]["x"] ≈ get_xy(sub_components[2])[][1] atol = 1e-5 - @test area4_dict["n_Battery"]["y"] ≈ get_xy(sub_components[3])[][2] atol = 1e-5 + solar_power = get_component(area4, "Solar Power") + battery = get_component(area4, "Battery") + @test area4_dict["n_Solar Power"]["x"] ≈ get_xy(solar_power)[][1] atol = 1e-5 + @test area4_dict["n_Battery"]["y"] ≈ get_xy(battery)[][2] atol = 1e-5 # Clean up files rm(joinpath(design_folder, "test_top_level.yml")) @@ -192,12 +192,13 @@ end @testset "get_toggle(gui,:expand_all).active" begin # Test if node n_El 1 became invisible get_toggle(gui, :expand_all).active = false - sub_components = get_components(components[1]) - @test !get_plots(sub_components[2])[1].visible[] + n_el_1 = get_component(area1, "El 1") # fetch the n_El 1 node + + @test !get_plots(n_el_1)[1].visible[] # Test if node n_El 1 became visible get_toggle(gui, :expand_all).active = true - @test get_plots(sub_components[2])[1].visible[] + @test get_plots(n_el_1)[1].visible[] end # Run through all components @@ -207,9 +208,9 @@ end end @testset "get_menu(gui,:period).i_selected" begin - clear_selection(gui; clear_topo = true) - sub_component = get_components(components[2])[2] # fetch the n_Power supply node - pick_component!(gui, sub_component; pick_topo_component = true) + clear_selection(gui, :topo) + sub_component = get_component(area2, "Power supply") # fetch the n_Power supply node + pick_component!(gui, sub_component, :topo) update!(gui) select_data!(gui, "cap_use") time_axis = time_menu.selection[] @@ -228,9 +229,9 @@ end end @testset "get_menu(gui,:representative_period).i_selected" begin - clear_selection(gui; clear_topo = true) - sub_component = get_components(components[1])[4] # fetch the Heating 1 node - pick_component!(gui, sub_component; pick_topo_component = true) + clear_selection(gui, :topo) + heating1 = get_component(area1, "Heating 1") # fetch the Heating 1 node + pick_component!(gui, heating1, :topo) update!(gui) select_data!(gui, "flow_in") time_axis = time_menu.selection[] @@ -290,15 +291,15 @@ end end @testset "pin_plot_button.clicks" begin - clear_selection(gui; clear_topo = true) - sub_component = get_components(components[4])[2] # fetch the Solar Power node - pick_component!(gui, sub_component; pick_topo_component = true) + clear_selection(gui, :topo) + sub_component = get_component(area4, "Solar Power") # fetch the Solar Power node + pick_component!(gui, sub_component, :topo) update!(gui) select_data!(gui, "profile") time_axis = time_menu.selection[] notify(pin_plot_button.clicks) - sub_component2 = components[3].components[2] # fetch the EV charger node - pick_component!(gui, sub_component2; pick_topo_component = true) # Select Area 1 + sub_component2 = get_component(area3, "EV charger") # fetch the EV charger node + pick_component!(gui, sub_component2, :topo) # Select Area 1 update!(gui) select_data!(gui, "cap") notify(pin_plot_button.clicks) @@ -363,7 +364,7 @@ end @testset "get_button(gui,:remove_plot).clicks" begin time_axis = time_menu.selection[] element = get_visible_data(gui, time_axis)[1] - pick_component!(gui, element; pick_results_component = true) + pick_component!(gui, element, :results) notify(get_button(gui, :remove_plot).clicks) notify(get_button(gui, :remove_plot).clicks) # test redundant clicks @@ -371,7 +372,7 @@ end end @testset "get_button(gui,:clear_all).clicks" begin - clear_selection(gui; clear_topo = true) + clear_selection(gui, :topo) update_available_data_menu!(gui, nothing) # Make sure the menu is updated select_data!(gui, "emissions_strategic") notify(pin_plot_button.clicks) @@ -383,8 +384,8 @@ end end @testset "Test plotting of representative periods from JuMP" begin - sub_component = get_components(components[4])[3] # fetch the Battery node - pick_component!(gui, sub_component; pick_topo_component = true) + sub_component = get_component(area4, "Battery") # fetch the Battery node + pick_component!(gui, sub_component, :topo) update!(gui) get_menu(gui, :period).i_selected = 3 select_data!(gui, "stor_level_Δ_rp") @@ -425,8 +426,8 @@ end [[get_nodes, get_links], [get_areas, get_transmissions]], ) gui2 = GUI(case2; id_to_icon_map, scenarios_labels = ["Scenario 1"]) - components = get_components(get_root_design(gui2)) - @test isempty(get_components(components[4])[3].id_to_icon_map["Battery"]) + components2 = get_components(get_root_design(gui2)) + @test isempty(get_components(components2[4])[3].id_to_icon_map["Battery"]) EMGUI.close(gui2) end @@ -461,7 +462,7 @@ end # Test plotting over scenarios sink = get_components(get_root_design(gui3))[2] - pick_component!(gui3, sink; pick_topo_component = true) + pick_component!(gui3, sink, :topo) update!(gui3) select_data!(gui3, "penalty.deficit") @test get_ax(gui3, :results).scene.plots[3][1][][4][2] ≈ 200000 atol = 1e-5 @@ -472,23 +473,23 @@ end @testset "Test SP(RP(OP))" begin _, _, _, gui3 = run_test_case(; use_rp = true, use_sc = false) available_data_menu = get_menu(gui3, :available_data) + ax_results = get_ax(gui3, :results) # Test plotting over operational periods select_data!(gui3, "emissions_total") get_menu(gui3, :representative_period).i_selected = 2 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 1.048 atol = 1e-5 + @test ax_results.scene.plots[1][1][][24][2] ≈ 1.048 atol = 1e-5 # Test plotting over strategic periods select_data!(gui3, "emissions_strategic") - @test get_ax(gui3, :results).scene.plots[2][1][][2][2] ≈ 20601.06 atol = 1e-5 - + @test ax_results.scene.plots[2][1][][2][2] ≈ 20601.06 atol = 1e-5 # Test plotting over representative periods get_menu(gui3, :period).i_selected = 3 sink = get_components(get_root_design(gui3))[2] - pick_component!(gui3, sink; pick_topo_component = true) + pick_component!(gui3, sink, :topo) update!(gui3) select_data!(gui3, "penalty.deficit") - @test get_ax(gui3, :results).scene.plots[3][1][][2][2] ≈ 2.0e6 atol = 1e-5 + @test ax_results.scene.plots[3][1][][2][2] ≈ 2.0e6 atol = 1e-5 EMGUI.close(gui3) end @@ -496,43 +497,55 @@ end @testset "Test SP(RP(SC(OP)))" begin _, _, _, gui3 = run_test_case(; use_rp = true, use_sc = true) available_data_menu = get_menu(gui3, :available_data) + ax_results = get_ax(gui3, :results) + period_menu = get_menu(gui3, :period) + representative_period_menu = get_menu(gui3, :representative_period) + scenario_menu = get_menu(gui3, :scenario) # Test plotting over operational periods select_data!(gui3, "emissions_total") # Test updating menu with non-tensorial timestructure - get_menu(gui3, :period).i_selected = 3 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 1.965 atol = 1e-5 - get_menu(gui3, :representative_period).i_selected = 2 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 1.2576 atol = 1e-5 - get_menu(gui3, :representative_period).i_selected = 1 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 1.965 atol = 1e-5 - get_menu(gui3, :scenario).i_selected = 4 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.393 atol = 1e-5 - get_menu(gui3, :period).i_selected = 2 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.262 atol = 1e-5 - get_menu(gui3, :representative_period).i_selected = 2 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 1.048 atol = 1e-5 - get_menu(gui3, :scenario).i_selected = 3 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.4192 atol = 1e-5 - get_menu(gui3, :period).i_selected = 1 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.1965 atol = 1e-5 + period_menu.i_selected = 3 + @test ax_results.scene.plots[1][1][][24][2] ≈ 1.965 atol = 1e-5 - get_menu(gui3, :period).i_selected = 3 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 0.9825 atol = 1e-5 - get_menu(gui3, :representative_period).i_selected = 2 - @test get_ax(gui3, :results).scene.plots[1][1][][24][2] ≈ 3.7728 atol = 1e-5 + representative_period_menu.i_selected = 2 + @test ax_results.scene.plots[1][1][][24][2] ≈ 1.2576 atol = 1e-5 + + representative_period_menu.i_selected = 1 + @test ax_results.scene.plots[1][1][][24][2] ≈ 1.965 atol = 1e-5 + + scenario_menu.i_selected = 4 + @test ax_results.scene.plots[1][1][][24][2] ≈ 0.393 atol = 1e-5 + + period_menu.i_selected = 2 + @test ax_results.scene.plots[1][1][][24][2] ≈ 0.262 atol = 1e-5 + + representative_period_menu.i_selected = 2 + @test ax_results.scene.plots[1][1][][24][2] ≈ 1.048 atol = 1e-5 + + scenario_menu.i_selected = 3 + @test ax_results.scene.plots[1][1][][24][2] ≈ 0.4192 atol = 1e-5 + + period_menu.i_selected = 1 + @test ax_results.scene.plots[1][1][][24][2] ≈ 0.1965 atol = 1e-5 + + period_menu.i_selected = 3 + @test ax_results.scene.plots[1][1][][24][2] ≈ 0.9825 atol = 1e-5 + + representative_period_menu.i_selected = 2 + @test ax_results.scene.plots[1][1][][24][2] ≈ 3.7728 atol = 1e-5 # Test plotting over strategic periods select_data!(gui3, "emissions_strategic") - @test get_ax(gui3, :results).scene.plots[2][1][][2][2] ≈ 7648.6446 atol = 1e-5 + @test ax_results.scene.plots[2][1][][2][2] ≈ 7648.6446 atol = 1e-5 # Test plotting over scenarios sink = get_components(get_root_design(gui3))[2] - pick_component!(gui3, sink; pick_topo_component = true) + pick_component!(gui3, sink, :topo) update!(gui3) select_data!(gui3, "penalty.deficit") - @test get_ax(gui3, :results).scene.plots[3][1][][2][2] ≈ 4.0e6 atol = 1e-5 + @test ax_results.scene.plots[3][1][][2][2] ≈ 4.0e6 atol = 1e-5 EMGUI.close(gui3) end end diff --git a/test/utils.jl b/test/utils.jl index 211b261..77b638b 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -1,3 +1,13 @@ +import EnergyModelsGUI: + get_root_design, + get_components, + get_connections, + get_menu, + get_button, + update!, + pick_component!, + clear_selection + """ run_through_all(gui::GUI) @@ -12,7 +22,7 @@ function run_through_all( run_through_all( gui, - EMGUI.get_root_design(gui), + get_root_design(gui), break_after_first, 1, sleep_time, @@ -32,14 +42,13 @@ function run_through_all( sleep_time::Float64, ) indent_spacing = " " - available_data_menu = EMGUI.get_menu(gui, :available_data) - for component ∈ EMGUI.get_components(design) + available_data_menu = get_menu(gui, :available_data) + for component ∈ get_components(design) @info indent_spacing^level * - "Running through component $(EMGUI.get_ref_element(component))" - EMGUI.clear_selection(gui; clear_topo = true) - EMGUI.pick_component!(gui, component; pick_topo_component = true) - - EMGUI.update!(gui) + "Running through component $(get_ref_element(component))" + clear_selection(gui, :topo) + pick_component!(gui, component, :topo) + update!(gui) run_through_menu( available_data_menu, indent_spacing, @@ -47,21 +56,21 @@ function run_through_all( break_after_first, sleep_time, ) - if !isempty(component.components) # no sub system found - notify(EMGUI.get_button(gui, :open).clicks) + if !isempty(get_components(component)) # no sub system found + notify(get_button(gui, :open).clicks) run_through_all(gui, component, break_after_first, level + 1, sleep_time) - notify(EMGUI.get_button(gui, :up).clicks) + notify(get_button(gui, :up).clicks) end if break_after_first break end end - for connection ∈ EMGUI.get_connections(design) + for connection ∈ get_connections(design) @info indent_spacing^level * - "Running through connection $(EMGUI.get_element(connection))" - EMGUI.clear_selection(gui; clear_topo = true) - EMGUI.pick_component!(gui, connection; pick_topo_component = true) - EMGUI.update!(gui) + "Running through connection $(get_element(connection))" + clear_selection(gui, :topo) + pick_component!(gui, connection, :topo) + update!(gui) run_through_menu( available_data_menu, indent_spacing,