Canvas & Display

The whole frontend was programmed using pygame - a set of Python modules designed for writing video games. Pygame will create a graphical canvas, running in the loop, which will change its appearance according to user action.

Frontend Setup

The frontend class itself is defined in q100viz/frontend.py. Upon initialization of the frontend class, the pygame environment is created. Things like the display framerate, window position etc can be set here.

window position and size

You can set the window’s position using the os module:

frontend.py
  # window position (must be set before pygame.init!)
  if not run_in_main_window:
      os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (
          0, 2560)  # projection to the left

  # window size:
  canvas_size = session.config['CANVAS_SIZE']
  self.canvas = pygame.display.set_mode(canvas_size, NOFRAME)
  pygame.display.set_caption("q100viz")

For this setting, the monitors should be organized as follows:

[Image of two schematic monitors, above each other and aligned left]

The canvas is masked by a layer that defines the margins of the region of interest (ROI). The following list of points defines the extent of a masking polygon:

frontend.py
  self.mask_points = [[0, 0], [85.5, 0], [85.5, 82], [0, 82], [0, -50],
                  [-50, -50], [-50, 200], [200, 200], [200, -50], [0, -50]]

Finally, a seperate thread for UDP observation is started. Each table (“grid”) has a seperate communication thread. More about how communication between tag decoder, frontend and infoscreen works in the Communication section.

Frontend Projection

After initialization, the frontend will run in a loop to update the projection, evaluate keyboard input, handle the game modes and process slider changes. Finally, pygame.display.update() is called to actually do what it says, and a pygame.time.Clock is updated, using the defined framerate. We used a framerate of 12 FPS, because this is the maximum framerate used in the tag decoder.

Canvas Composition

The frontend image is composed of a set of layers, which are rendered ontop of each other in the following order:

  1. draw polygons to _gis.surface

  2. draw grid outĺine to grid.surface

  3. draw mask to session.viewport

  4. draw basemap to frontend.canvas

  5. draw mode-specific surface (what does this do?)

  6. render GIS layer: _gis.surface to frontend.canvas

  7. slider: draw polygons, icons and text to slider.surface

  8. draw grid.surface to frontend.canvas

  9. draw session.viewport to frontend.canvas

Note

More notes on how to use simple pygame features can be found in the Frontend/pygame section!

Drawing polygons and lines using GIS shapes

The GIS shapes are drawn using the functions of the custom GIS class:

  • draw_linestring_layer: draws GIS features as lines - in our case, the heating grid is drawn using lines.

the following functions are used to draw polygons from geographical data (and color them according to the selected feature):

  • draw_polygon_layer: simply draw polygons and fill them with a provided color

  • draw_polygon_layer_bool: draw polygons and fill them either in color A or B (true/false)

  • draw_polygon_layer_float: draw polygons and fill them with a color gradient with the end points of a float between 0 and 1

Using these functions, we can color buildings on the map and fill them with a color according to a certain attribute, e.g. mapping their relative heat consumption to a color gradient between red and green, or color them either green or red, when connected to the heat grid, or not. The functions always use the entire buildings-dataset as an input parameter and draws all contained polygons at the same time. They are regularly called in the loop function of the frontend:

frontend.py
# draw GIS layers:
if session.show_polygons:
    session._gis.draw_linestring_layer(
        self.canvas, session._gis.nahwaermenetz, (217, 9, 9), 3)
    session._gis.draw_buildings_connections(
        session.buildings.df)  # draw lines to closest heat grid

    # fill and lerp:
    if session.VERBOSE_MODE:
        session._gis.draw_polygon_layer_float(
          self.canvas, session.buildings.df, 0,
          (96, 205, 21),
          (213, 50, 21),
          'spec_heat_consumption')
    else:
        session._gis.draw_polygon_layer_bool(
            self.canvas, session.buildings.df, 0,
            (213, 50, 21),
            (96, 205, 21),
            'connection_to_heat_grid')

    # stroke black:
    session._gis.draw_polygon_layer_bool(
        self.canvas, session.buildings.df, 1,
        (0, 0, 0),
        (0, 0, 0),
        'connection_to_heat_grid')

    # stroke according to connection status:
    session._gis.draw_polygon_layer_bool(
        surface=self.canvas, df=session.buildings.df,
        stroke=1,
        fill_false=(0, 0, 0),
        fill_true=(0, 168, 78),
        fill_attr='connection_to_heat_grid')

    # color buildings if connection is not -1:
    # session.gis.draw_polygon_layer_connection_year(
    #     session.buildings.df,
    #     stroke=0,
    #     fill_true=(96, 205, 21),
    #     fill_false=(213, 50, 21),
    #     fill_attr='connection_to_heat_grid')

Hint

Filling a polygon is done by applying a stroke width of 0.

When the buildings are set to be connected to the heat grid, a line is shown between the polygon and the closest heat grid line. This is basically a tangent to that heat grid line at the closest point and is calculated in the function GIS.draw_buildings_connections.

Export Canvas to file

We used to export the rendered canvas to a png file each frame (if changes are ready), to further use it on the infoscreen. This is deprecated, but can be done via a code snippet like this:

insert this to frontend.py game loop to export the canvas to file:
# export canvas:
  if session.flag_export_canvas:
      # create a cropped output canvas and export:
      temp = pygame.Surface((1460, 630))
      temp.blit(session.gis.surface, (0,0))
      temp = pygame.transform.rotate(temp, 270)
      pygame.image.save(temp, '../data/canvas.png')
      session.flag_export_canvas = False # has to be set True when changes are received from cspy

Drawing of Sliders

The sliders have a bool called show_text that, when True, activates the display of the slider control texts. This variable can be used for the usage modes to define whether the slider controls shall be displayed.

Graphic Tools

Images

Pygame is able to load images onto Surface objects from PNG, JPG, GIF, and BMP image files. q100viz/graphics/graphictools.py contains an Image class that can be used to load and transform images according to the needs of the warped canvas. Images just have to be initialized and warped, before they can be rendered to the surface.

example code for rendering images, see interface.py for comparison.
# 1. load image:
img = Image("images/piechart_disabled.tif")

# 2. warp image:
img.warp((1920, 1080))

# 3. render image:
surface.blit(img.image, (x,y))

Graphs

The frontend software creates graphs from the simulation results using the matplotlib library. A toolkit is contained in q100viz/graphics/graphs.py, providing the following functions:

  • export_individual_emissions: exports emissions from csv-data-file for every iteration round to graph and exports png

  • export_individual_energy_expenses: exports energy expenses column of csv-data-file for every iteration round to graph, prepending historic energy prices. Finally, exports png

  • export_default_graph: exports default data to graph with gray curve

  • export_compared_emissions: exports all data for selected group buildings into one graph for total data view

  • export_neighborhood_emissions_connections: creates a bar plot for the total number of connections to the heat grid with an overlaying line plot of total emissions

  • export_compared_energy_costs: exports all data for selected group buildings into one graph for total data view

  • export_neighborhood_total_data: exports specified column of csv-data-file for every iteration round to graph and exports png

and some helpful conversion functions used to get the right units:

  • GAMA_time_to_datetime

  • grams_to_kg

  • grams_to_tons

  • rgb_to_hex

  • rgb_to_float_tuple

Attention

If verbose mode is activated, house addresses and absolte consumption data will be added to the graphs!