Skip to content

Qgz files created with this plugin do not contain a mapcanvas element #7

@spwoodcock

Description

@spwoodcock

Observation

  • I'm no expert in QGIS file formats / XML, so correct me if I'm wrong here.
  • I swapped over from the XLSFormConverter plugin to using this, for generating projects to upload to QFieldCloud.
  • The resulting projects load fine in QGIS and QField, but in the app.qfield.cloud UI I get an error:
Image
  • The resulting project is shown with a red dot, and cannot be viewed in QField when logged into the relevant account.

Analysis

  • Digging into the generated .qgs XML, I can see the mapcanvas element doesn't exist.
  • QFieldCloud's process_projectfile worker searches for this element by name to to extract the project extent and background colour.
  • Projects generated with this plugin omit referencing the mapcanvas, as it doesn't involve a GUI (while the XLSFormConverter plugin did have an attached GUI context).

So either:

  1. This plugin should add a mapcontext element.
  2. QFieldCloud qgz processing should fail gracefully if no project extent or background colour is found.

I used Opus 4.6 to create a function that injects the mapcanvas element. The tool I am working on uses this & the projects are now green in QFC:

def _inject_map_canvas(
    qgs_data: bytes, extent_bbox: list, log: logging.Logger
) -> tuple[bytes, bool]:
    """Inject <mapcanvas name="theMapCanvas"> if absent.

    QFieldCloud's process_projectfile worker searches for this element by name
    to extract the project extent and background colour.  Projects generated by
    xlsform2qgis omit it because QGIS only writes it when a QgsMapCanvas
    object is attached (GUI context only).
    """
    if b'name="theMapCanvas"' in qgs_data:
        return qgs_data, False

    # Grab the <spatialrefsys> block from <projectCrs> to reuse as <destinationsrs>
    crs_m = re.search(
        rb"<projectCrs>\s*(<spatialrefsys\b.*?</spatialrefsys>)\s*</projectCrs>",
        qgs_data,
        re.DOTALL,
    )
    if not crs_m:
        log.warning(
            "Cannot inject theMapCanvas: no <projectCrs> found in project XML"
        )
        return qgs_data, False

    srs_block = crs_m.group(1)

    # Derive map units from the CRS projection acronym
    acronym_m = re.search(rb"<projectionacronym>(.*?)</projectionacronym>", srs_block)
    acronym = acronym_m.group(1).decode() if acronym_m else ""
    units = "degrees" if acronym == "longlat" else "meters"

    xmin, ymin, xmax, ymax = extent_bbox
    canvas_xml = (
        f'\n  <mapcanvas name="theMapCanvas" annotationsVisible="1">\n'
        f"    <units>{units}</units>\n"
        f"    <extent>\n"
        f"      <xmin>{xmin}</xmin>\n"
        f"      <ymin>{ymin}</ymin>\n"
        f"      <xmax>{xmax}</xmax>\n"
        f"      <ymax>{ymax}</ymax>\n"
        f"    </extent>\n"
        f"    <rotation>0</rotation>\n"
        f"    <destinationsrs>\n"
        f"      {srs_block.decode()}\n"
        f"    </destinationsrs>\n"
        f"    <rendermaptile>0</rendermaptile>\n"
        f"  </mapcanvas>"
    ).encode()

    # Insert right after </verticalCrs> (preferred) or </projectCrs> (fallback)
    for anchor in (b"</verticalCrs>", b"</projectCrs>"):
        if anchor in qgs_data:
            updated = qgs_data.replace(anchor, anchor + canvas_xml, 1)
            log.info("Injected <mapcanvas name=\"theMapCanvas\"> into project XML")
            return updated, True

    log.warning("Could not find insertion point for theMapCanvas in project XML")
    return qgs_data, False

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions