Skip to content

Report Generator

CPLUS Report generator.

BaseScenarioReportGenerator

BaseScenarioReportGenerator(context, feedback=None)

Base class for generating a scenario report.

Source code in src/cplus_plugin/lib/reports/generator.py
def __init__(self, context: BaseReportContext, feedback: QgsFeedback = None):
    self._context = context
    self._feedback = context.feedback or feedback
    if self._feedback:
        self._feedback.canceled.connect(self._on_feedback_canceled)

    self._error_messages: typing.List[str] = []
    self._error_occurred = False
    self._layout = None
    self._project = None
    self._variable_register = LayoutVariableRegister()
    self._report_output_dir = ""
    self._output_layout_path = ""

context property

context

Returns the report context used by the generator.

Returns:

Type Description
ReportContext

Report context object used by the generator.

feedback property

feedback

Returns the feedback object for process update and cancellation.

Returns:

Type Description
QgsFeedback

Feedback object or None if not specified.

layout property

layout

Returns the layout object used to generate the report.

Returns:

Type Description
QgsPrintLayout

The layout object used to generate the report or None if the process was not successful.

output_dir property

output_dir

Creates, if it does not exist, the output directory where the report_templates will be saved.

Returns:

Type Description
str

Output directory where the report_templates will be saved.

output_layout_path property

output_layout_path

Absolute path to a temporary file containing the layout as a QPT file.

When this object is used within a QgsTask, it is recommended to use this layout path to reconstruct the layout rather calling the layout attribute since it was created in a separate thread.

Returns:

Type Description
str

Path to the layout template file.

run

run()

Initiates the report generation process and returns a result which contains information on whether the process succeeded or failed.

Returns:

Type Description
ReportResult

The result of the report generation process.

Source code in src/cplus_plugin/lib/reports/generator.py
def run(self) -> ReportResult:
    """Initiates the report generation process and returns
    a result which contains information on whether the
    process succeeded or failed.

    :returns: The result of the report generation process.
    :rtype: ReportResult
    """
    try:
        return self._run()
    except Exception as ex:
        # Last resort to capture general exceptions.
        exc_info = "".join(traceback.TracebackException.from_exception(ex).format())
        self._error_messages.append(exc_info)
        return self._get_failed_result()

set_label_font classmethod

set_label_font(label, size, bold=False, italic=False)

Set font properties of the given layout label item.

Parameters:

Name Type Description Default
label QgsLayoutItemLabel

Label item whose font properties will be updated.

required
size float

Point size of the font.

required
bold bool

True if font is to be bold, else False (default).

False
italic bool

True if font is to be in italics, else False (default).

False
Source code in src/cplus_plugin/lib/reports/generator.py
@classmethod
def set_label_font(
    cls,
    label: QgsLayoutItemLabel,
    size: float,
    bold: bool = False,
    italic: bool = False,
):
    """Set font properties of the given layout label item.

    :param label: Label item whose font properties will
    be updated.
    :type label: QgsLayoutItemLabel

    :param size: Point size of the font.
    :type size: int

    :param bold: True if font is to be bold, else
    False (default).
    :type bold: bool

    :param italic: True if font is to be in italics, else
    False (default).
    :type italic: bool
    """
    font = get_report_font(size, bold, italic)
    version = Qgis.versionInt()

    # Text format size unit
    if version < 33000:
        unit_type = QgsUnitTypes.RenderUnit.RenderPoints
    else:
        unit_type = Qgis.RenderUnit.Points

    # Label font setting option
    if version < 32400:
        label.setFont(font)
    else:
        txt_format = QgsTextFormat()
        txt_format.setFont(font)
        txt_format.setSize(size)
        txt_format.setSizeUnit(unit_type)
        label.setTextFormat(txt_format)

    label.refresh()

BaseScenarioReportGeneratorTask

BaseScenarioReportGeneratorTask(description, context)

Bases: QgsTask

Base proxy class for initiating the report generation process.

Source code in src/cplus_plugin/lib/reports/generator.py
def __init__(self, description: str, context: BaseReportContext):
    super().__init__(description)
    self._context = context
    self._result = None
    self._generator = BaseScenarioReportGenerator(
        self._context, self._context.feedback
    )
    self.layout_manager = QgsProject.instance().layoutManager()
    self.layout_manager.layoutAdded.connect(self._on_layout_added)

context property

context

Returns the report context used by the generator.

Returns:

Type Description
ReportContext

Report context object used by the generator.

result property

result

Returns the result object which contains information on whether the process succeeded or failed.

Returns:

Type Description
ReportResult

The result of the report generation process.

cancel

cancel()

Cancel the report generation task.

Source code in src/cplus_plugin/lib/reports/generator.py
def cancel(self):
    """Cancel the report generation task."""
    if self._context.feedback:
        self._context.feedback.cancel()

    super().cancel()

run

run()

Initiates the report generation process and returns a result indicating whether the process succeeded or failed.

Returns:

Type Description
bool

True if the report generation process succeeded or False it if failed.

Source code in src/cplus_plugin/lib/reports/generator.py
def run(self) -> bool:
    """Initiates the report generation process and returns
    a result indicating whether the process succeeded or
    failed.

    :returns: True if the report generation process succeeded
    or False it if failed.
    :rtype: bool
    """
    if self.isCanceled():
        return False

    if self._context.project_file:
        self._result = self._generator.run()
    else:
        msg = tr("Unable to serialize current project for report generation.")
        msgs: typing.List[str] = [msg]
        scenario_identifier = None
        if hasattr(self._context, "scenario"):
            scenario_identifier = self._context.scenario.uuid
        self._result = ReportResult(False, scenario_identifier, "", tuple(msgs))

    return self._result.success

DuplicatableRepeatPageReportGenerator

DuplicatableRepeatPageReportGenerator(context, feedback=None)

Bases: BaseScenarioReportGenerator

Incorporates extra functionality duplicating a repeat page.

Subclass must have _repeat_gae and _repeat_page_num members.

Source code in src/cplus_plugin/lib/reports/generator.py
def __init__(self, context: BaseReportContext, feedback: QgsFeedback = None):
    self._context = context
    self._feedback = context.feedback or feedback
    if self._feedback:
        self._feedback.canceled.connect(self._on_feedback_canceled)

    self._error_messages: typing.List[str] = []
    self._error_occurred = False
    self._layout = None
    self._project = None
    self._variable_register = LayoutVariableRegister()
    self._report_output_dir = ""
    self._output_layout_path = ""

duplicate_repeat_page

duplicate_repeat_page(position)

Duplicates the repeat page and adds it to the layout at the given position.

Parameters:

Name Type Description Default
position int

Zero-based position to insert the duplicated page. If the position is greater than the number of pages, then the duplicated page will be inserted at the end of the layout.

required

Returns:

Type Description
bool

True if the page was successfully duplicated else False.

Source code in src/cplus_plugin/lib/reports/generator.py
def duplicate_repeat_page(self, position: int) -> bool:
    """Duplicates the repeat page and adds it to the layout
    at the given position.

    :param position: Zero-based position to insert the duplicated page. If
    the position is greater than the number of pages, then the
    duplicated page will be inserted at the end of the layout.
    :type position: int

    :returns: True if the page was successfully duplicated else False.
    :rtype: bool
    """
    if self._repeat_page is None:
        return False

    if self._layout is None:
        return False

    if self._repeat_page_num == -1:
        tr_msg = "Repeat page not found in page collection"
        self._error_messages.append(tr_msg)
        return False

    new_page = QgsLayoutItemPage(self._layout)
    new_page.attemptResize(self._repeat_page.sizeWithUnits())
    new_page.setPageStyleSymbol(self._repeat_page.pageStyleSymbol().clone())

    # Insert empty repeat page at the given position
    if position < self._layout.pageCollection().pageCount():
        self._layout.pageCollection().insertPage(new_page, position)
    else:
        # Add at the end
        position = self._layout.pageCollection().pageCount()
        self._layout.pageCollection().addPage(new_page)

    doc = QtXml.QDomDocument()
    el = doc.createElement("CopyItems")
    ctx = QgsReadWriteContext()
    repeat_page_items = self._layout.pageCollection().itemsOnPage(
        self._repeat_page_num
    )
    for item in repeat_page_items:
        item.writeXml(el, doc, ctx)
        doc.appendChild(el)

    # Clear element identifier references
    nodes = doc.elementsByTagName("LayoutItem")
    for n in range(nodes.count()):
        node = nodes.at(n)
        if node.isElement():
            node.toElement().removeAttribute("uuid")

    page_ref_point = self._layout.pageCollection().pagePositionToLayoutPosition(
        position, QgsLayoutPoint(0, 0)
    )
    _ = self._layout.addItemsFromXml(el, doc, ctx, page_ref_point, True)

    return True

get_dimension_for_repeat_item

get_dimension_for_repeat_item(repeat_item)

Calculates the number of rows and columns for rendering items based on the size of CPLUS repeat item. It also determines the recommended width and height of the repeat area.

Parameters:

Name Type Description Default
repeat_item CplusMapRepeatItem

The map repeat item where the items will be rendered.

required

Returns:

Type Description
RepeatAreaDimension

A recommended number of rows and columns respectively for rendering the repeat items as well the recommended dimension of the repeat area.

Source code in src/cplus_plugin/lib/reports/generator.py
def get_dimension_for_repeat_item(
    self, repeat_item: CplusMapRepeatItem
) -> typing.Optional[RepeatAreaDimension]:
    """Calculates the number of rows and columns for rendering
    items based on the size of CPLUS repeat item. It also
    determines the recommended width and height of the repeat
    area.

    :param repeat_item: The map repeat item where the items will
    be rendered.
    :type repeat_item: CplusMapRepeatItem

    :returns: A recommended number of rows and columns respectively
    for rendering the repeat items as well the recommended dimension
    of the repeat area.
    :rtype: RepeatAreaDimension
    """
    num_rows, num_cols = -1, -1
    if MINIMUM_ITEM_HEIGHT <= 0 or MINIMUM_ITEM_WIDTH <= 0:
        tr_msg = tr("Minimum repeat item dimensions cannot be used")
        self._error_messages.append(tr_msg)
        return None

    repeat_size = repeat_item.sizeWithUnits()
    repeat_width = repeat_size.width()
    repeat_height = repeat_size.height()

    repeat_ref_point = repeat_item.pagePositionWithUnits()
    repeat_ref_x = repeat_ref_point.x()
    repeat_ref_y = repeat_ref_point.y()

    # Determine number of columns
    num_cols = -1
    adjusted_item_width = MINIMUM_ITEM_WIDTH
    if repeat_width < MINIMUM_ITEM_WIDTH:
        tr_msg = tr("Repeat item width is too small to render the model items")
        self._error_messages.append(tr_msg)
        return None

    else:
        num_cols = int(repeat_width // MINIMUM_ITEM_WIDTH)
        bleed_item_width = (
            repeat_width - (num_cols * MINIMUM_ITEM_WIDTH)
        ) / num_cols
        adjusted_item_width = MINIMUM_ITEM_WIDTH + bleed_item_width

    # Determine number of rows
    num_rows = -1
    adjusted_item_height = MINIMUM_ITEM_HEIGHT
    if repeat_height < MINIMUM_ITEM_HEIGHT:
        tr_msg = tr("Repeat item height is too small to render the model items")
        self._error_messages.append(tr_msg)
        return None

    else:
        num_rows = int(repeat_height // MINIMUM_ITEM_HEIGHT)
        bleed_item_height = (
            repeat_height - (num_rows * MINIMUM_ITEM_HEIGHT)
        ) / num_rows
        adjusted_item_height = MINIMUM_ITEM_HEIGHT + bleed_item_height

    return RepeatAreaDimension(
        num_rows, num_cols, adjusted_item_width, adjusted_item_height
    )

ScenarioAnalysisReportGenerator

ScenarioAnalysisReportGenerator(context, feedback=None)

Bases: DuplicatableRepeatPageReportGenerator

Generator for CPLUS scenario analysis report.

Source code in src/cplus_plugin/lib/reports/generator.py
def __init__(self, context: ReportContext, feedback: QgsFeedback = None):
    super().__init__(context, feedback)
    self._repeat_page = None
    self._repeat_page_num = -1
    self._repeat_item = None
    self._reference_layer_group = None
    self._scenario_layer = None
    self._area_processing_feedback = None
    self._activities_area = {}
    self._pixel_area_info = {}

    if self._feedback:
        self._feedback.canceled.connect(self._on_feedback_cancelled)

output_dir property

output_dir

Creates, if it does not exist, the output directory where the analysis report_templates will be saved. This is relative to the base directory and scenario output sub-folder.

Returns:

Type Description
str

Output directory where the analysis report_templates will be saved.

repeat_page property

repeat_page

Returns the page item that will be repeated based on the number of activities in the scenario.

A repeat page is a layout page item that contains the first instance of a CplusMapRepeatItem.

Returns:

Type Description
QgsLayoutItemPage

Page item containing a CplusMapRepeatItem or None if not found.

export_to_pdf

export_to_pdf()

Exports the layout to a PDF file in the output directory using the layout name as the file name.

Returns:

Type Description
bool

True if the layout was successfully exported else False.

Source code in src/cplus_plugin/lib/reports/generator.py
def export_to_pdf(self) -> bool:
    """Exports the layout to a PDF file in the output
    directory using the layout name as the file name.

    :returns: True if the layout was successfully exported else False.
    :rtype: bool
    """
    if self._layout is None or self._project is None or not self.output_dir:
        return False

    exporter = QgsLayoutExporter(self._layout)
    pdf_path = f"{self.output_dir}/{self._layout.name()}.pdf"
    result = exporter.exportToPdf(pdf_path, QgsLayoutExporter.PdfExportSettings())
    if result == QgsLayoutExporter.ExportResult.Success:
        return True
    else:
        tr_msg = tr("Could not export layout to PDF")
        self._error_messages.append(f"{tr_msg} {pdf_path}.")
        return False

ScenarioAnalysisReportGeneratorTask

ScenarioAnalysisReportGeneratorTask(description, context)

Bases: BaseScenarioReportGeneratorTask

Proxy class for initiating the report generation process.

Source code in src/cplus_plugin/lib/reports/generator.py
def __init__(self, description: str, context: ReportContext):
    super().__init__(description, context)
    self._generator = ScenarioAnalysisReportGenerator(
        context, self._context.feedback
    )

finished

finished(result)

If successful, add the layout to the project.

Parameters:

Name Type Description Default
result bool

Flag indicating if the result of the report generation process. True if successful, else False.

required
Source code in src/cplus_plugin/lib/reports/generator.py
def finished(self, result: bool):
    """If successful, add the layout to the project.

    :param result: Flag indicating if the result of the
    report generation process. True if successful,
    else False.
    :type result: bool
    """
    if len(self._result.messages) > 0:
        log(
            f"Warnings and errors occurred when generating the "
            f"report for {self._context.scenario.name} "
            f"scenario. See details below:",
            info=False,
        )
        for err in self._result.messages:
            err_msg = f"{self._context.scenario.name} - {err}\n"
            log(err_msg, info=False)

    if result:
        log(
            f"Successfully generated the report for "
            f"{self._context.scenario.name} scenario."
        )

        layout_path = self._generator.output_layout_path
        if not layout_path:
            log("Output layout could not be saved.", info=False)
            return

        feedback = self._context.feedback
        project = QgsProject.instance()
        layout = _load_layout_from_file(layout_path, project)
        if layout is None:
            log("Could not load layout from file.", info=False)
            return

        # Zoom the extents of map items in the layout then export to PDF
        self._zoom_map_items_to_current_extents(layout)
        project.layoutManager().addLayout(layout)
        project.write()

        if feedback is not None:
            feedback.setProgress(100)

ScenarioComparisonReportGenerator

ScenarioComparisonReportGenerator(context, feedback=None)

Bases: DuplicatableRepeatPageReportGenerator

Generator for CPLUS scenario comparison reports.

Source code in src/cplus_plugin/lib/reports/generator.py
def __init__(
    self, context: ScenarioComparisonReportContext, feedback: QgsFeedback = None
):
    super().__init__(context, feedback)

    # Repeat item for half page one
    self._page_one_repeat_item = None

    # For duplicating page
    self._repeat_page_item = None

    self._repeat_page = None
    self._repeat_page_num = -1

    self._area_calculation_reference = 25

    self._comparison_info = ScenarioComparisonTableInfo(self._context.results)
    self._comparison_info.feedback.progressChanged.connect(
        self._on_area_calculation_changed
    )

output_dir property

output_dir

Creates, if it does not exist, the output directory where the comparison report_templates will be saved. This is relative to the base directory and comparison reports sub-folder.

Returns:

Type Description
str

Output directory where the scenario report_templates will be saved.

ScenarioComparisonReportGeneratorTask

ScenarioComparisonReportGeneratorTask(description, context)

Bases: BaseScenarioReportGeneratorTask

Proxy class for initiating the generation of scenario comparison reports.

Source code in src/cplus_plugin/lib/reports/generator.py
def __init__(self, description: str, context: ScenarioComparisonReportContext):
    super().__init__(description, context)
    self._generator = ScenarioComparisonReportGenerator(
        context, self._context.feedback
    )

finished

finished(result)

If successful, add the layout to the project.

Parameters:

Name Type Description Default
result bool

Flag indicating if the result of the report generation process. True if successful, else False.

required
Source code in src/cplus_plugin/lib/reports/generator.py
def finished(self, result: bool):
    """If successful, add the layout to the project.

    :param result: Flag indicating if the result of the
    report generation process. True if successful,
    else False.
    :type result: bool
    """
    if len(self._result.messages) > 0:
        log(
            f"Warnings and errors occurred when generating the "
            f"scenario comparison report. See details below:",
            info=False,
        )
        for err in self._result.messages:
            err_msg = f"Comparison report - {err}\n"
            log(err_msg, info=False)

    if result:
        log(f"Successfully generated the scenario comparison report.")

        layout_path = self._generator.output_layout_path
        if not layout_path:
            log("Output layout could not be saved.", info=False)
            return

        feedback = self._context.feedback
        project = QgsProject.instance()
        layout = _load_layout_from_file(layout_path, project)
        if layout is None:
            log("Could not load layout from file.", info=False)
            return

        project.layoutManager().addLayout(layout)

        if feedback is not None:
            feedback.setProgress(100)

Last update: October 2, 2024
Back to top