+ Developed by the World Bank, GEEST (The Gender Enabling Environments Spatial Tool) is a powerful and user-friendly open-source spatial mapping tool that enables a comprehensive analysis of how various spatial factors influence women's employment and business opportunities in any geographic area of interest.
+
+ By using this tool, you can identify key areas for intervention, make data-driven decisions, and ultimately work toward creating more equitable job opportunities for women across different regions.
Developed by the World Bank, GEEST (The Gender Enabling Environments Spatial Tool) is a powerful and user-friendly open-source spatial mapping tool that enables a comprehensive analysis of how various spatial factors influence women's employment and business opportunities in any geographic area of interest.
From 14363367144e0efc032975ce33c230faadb917f1 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 11:55:18 +0200
Subject: [PATCH 05/56] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 34c18819..68c745b1 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# Welcome to GEEST
-1
+
Developed by the World Bank, GEEST (The Gender Enabling Environments Spatial Tool) is a powerful and user-friendly open-source spatial mapping tool that enables a comprehensive analysis of how various spatial factors influence women's employment and business opportunities in any geographic area of interest.
From 3638509d1c27e124d0a754b74d5b36a9859c32a5 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 12:00:37 +0200
Subject: [PATCH 06/56] Update accessibility.md
---
docs/userguide/accessibility.md | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/docs/userguide/accessibility.md b/docs/userguide/accessibility.md
index d8f83640..94874f83 100644
--- a/docs/userguide/accessibility.md
+++ b/docs/userguide/accessibility.md
@@ -51,6 +51,17 @@ This tool evaluates how easily women can access essential services and amenities
onclick="window.open(this.src, '_blank')">
The Accessibility Dimension evaluates women’s daily mobility by examining their access to essential services. Levels of enablement for work access in this dimension are determined by service areas, which represent the geographic zones that facilities like childcare, supermarkets, universities, banks, and clinics can serve based on proximity. The nearer these facilities are to where women live, the more supportive and enabling the environment becomes for their participation in the workforce. For more information on data input used from open sources, please refer to the Data Collection section.
The Accessibility Dimension evaluates women’s daily mobility by examining their access to essential services. Levels of enablement for work access in this dimension are determined by service areas, which represent the geographic zones that facilities like childcare, supermarkets, universities, banks, and clinics can serve based on proximity. The nearer these facilities are to where women live, the more supportive and enabling the environment becomes for their participation in the workforce. For more information on data input used from open sources, please refer to the Data Collection section.
@@ -52,16 +52,14 @@ This tool evaluates how easily women can access essential services and amenities
Women's Travel Patterns factor is scored based on the default distance to facilities:
-
-| Distance to Facilities (meters) | Score |
-|---------------------------------|-------|
-| 0 - 400 | 5 |
-| 401 - 800 | 4 |
-| 801 - 1,200 | 3 |
-| 1,201 - 1,500 | 2 |
-| 1,501 - 2,000 | 1 |
-| Over 2,000 | 0 |
-
+| Distance (m) | Score |
+|--------------|-------|
+| 0 - 400 | 5 |
+| 401 - 800 | 4 |
+| 801 - 1,200 | 3 |
+| 1,201 - 1,500| 2 |
+| 1,501 - 2,000| 1 |
+| Over 2,000 | 0 |
**Process Women's Travel Patterns factors**
From 023b4443e40fcc24289b2d636e432da96a4d666d Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 12:23:51 +0200
Subject: [PATCH 09/56] Update accessibility.md
---
docs/userguide/accessibility.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/userguide/accessibility.md b/docs/userguide/accessibility.md
index be838a4b..37b9fba4 100644
--- a/docs/userguide/accessibility.md
+++ b/docs/userguide/accessibility.md
@@ -16,7 +16,7 @@ This tool evaluates how easily women can access essential services and amenities
**Travel mode**: The user can select walking or driving as a travel mode, and it is recommended that the same travel mode should be selected for all accessibility factors. The default travel mode is walking due to its inclusive nature.
-**Measurement**: The default measurement for travel is distance in meters, which is most appropriate for walking. These thresholds1 are based on evidence from the literature at the factor level and are designed to provide consistency across analyses. If driving is selected as a travel mode, time in minutes is a more appropriate measurement.
+**Measurement**: The default measurement for travel is distance in meters, which is most appropriate for walking. These thresholds are based on evidence from the literature at the factor level and are designed to provide consistency across analyses. If driving is selected as a travel mode, time in minutes is a more appropriate measurement.
---
From 053d61325223d8c89d7f6dafd7611bae2dc4ee5b Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 12:26:42 +0200
Subject: [PATCH 10/56] Update accessibility.md
---
docs/userguide/accessibility.md | 41 +++++++++++++++++++++++++--------
1 file changed, 32 insertions(+), 9 deletions(-)
diff --git a/docs/userguide/accessibility.md b/docs/userguide/accessibility.md
index 37b9fba4..89b78b59 100644
--- a/docs/userguide/accessibility.md
+++ b/docs/userguide/accessibility.md
@@ -51,15 +51,38 @@ This tool evaluates how easily women can access essential services and amenities
onclick="window.open(this.src, '_blank')">
-Women's Travel Patterns factor is scored based on the default distance to facilities:
-| Distance (m) | Score |
-|--------------|-------|
-| 0 - 400 | 5 |
-| 401 - 800 | 4 |
-| 801 - 1,200 | 3 |
-| 1,201 - 1,500| 2 |
-| 1,501 - 2,000| 1 |
-| Over 2,000 | 0 |
+Default Women's Travel Patterns thresholds:
+
+
+
Distance to Facilities (meters)
+
Score
+
+
+
0 - 400
+
5
+
+
+
401 - 800
+
4
+
+
+
801 - 1,200
+
3
+
+
+
1,201 - 1,500
+
2
+
+
+
1,501 - 2,000
+
1
+
+
+
Over 2,000
+
0
+
+
+
**Process Women's Travel Patterns factors**
From 9d33eb47bc50ef088375182a32693780d3185dc5 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 12:29:36 +0200
Subject: [PATCH 11/56] Update accessibility.md
---
docs/userguide/accessibility.md | 48 +++++++++++++++++----------------
1 file changed, 25 insertions(+), 23 deletions(-)
diff --git a/docs/userguide/accessibility.md b/docs/userguide/accessibility.md
index 89b78b59..eb452863 100644
--- a/docs/userguide/accessibility.md
+++ b/docs/userguide/accessibility.md
@@ -52,38 +52,40 @@ This tool evaluates how easily women can access essential services and amenities
Default Women's Travel Patterns thresholds:
-
-
-
Distance to Facilities (meters)
-
Score
+
+
+
+
Distance to Facilities (meters)
+
Score
-
-
0 - 400
-
5
+
+
0 - 400
+
5
-
-
401 - 800
-
4
+
+
401 - 800
+
4
-
-
801 - 1,200
-
3
+
+
801 - 1,200
+
3
-
-
1,201 - 1,500
-
2
+
+
1,201 - 1,500
+
2
-
-
1,501 - 2,000
-
1
+
+
1,501 - 2,000
+
1
-
-
Over 2,000
-
0
+
+
Over 2,000
+
0
+
**Process Women's Travel Patterns factors**
Back in the Data Processing Interface:
@@ -338,7 +340,7 @@ If the results do not immediately appear in the Layer Panel after processing the
-Women's Travel Patterns factor is scored based on the distance to facilities: 0 to 400 meters: score 5 | 401 to 800 meters: score 4 | 801 to 1,200 meters: score 3 | 1,201 to 1,500 meters: score 2 | 1,501 to 2,000 meters: score 1 | Over 2,000 meters: score 0
+
Access to Public Transport factor is scored based on proximity: 0 to 250 meters: score 5 | 251 to 500 meters: score 4 | 501 to 750 meters: score 3 | 751 to 1,000 meters: score 2 | 1,001 to 1,250 meters: score 1 | Over 1,250 meters: score 0
From ef78471164a9c92f524bcba6bae45f2c5db9e725 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 12:38:30 +0200
Subject: [PATCH 12/56] Update accessibility.md
---
docs/userguide/accessibility.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/userguide/accessibility.md b/docs/userguide/accessibility.md
index eb452863..73477225 100644
--- a/docs/userguide/accessibility.md
+++ b/docs/userguide/accessibility.md
@@ -53,7 +53,7 @@ This tool evaluates how easily women can access essential services and amenities
Default Women's Travel Patterns thresholds:
-
+
Distance to Facilities (meters)
Score
From c35f0363b5e1561f1d3123ab7bec8e18e4720f08 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 12:41:06 +0200
Subject: [PATCH 13/56] Update accessibility.md
---
docs/userguide/accessibility.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/userguide/accessibility.md b/docs/userguide/accessibility.md
index 73477225..a089b2fd 100644
--- a/docs/userguide/accessibility.md
+++ b/docs/userguide/accessibility.md
@@ -53,7 +53,7 @@ This tool evaluates how easily women can access essential services and amenities
Default Women's Travel Patterns thresholds:
-
+
Distance to Facilities (meters)
Score
@@ -84,7 +84,7 @@ This tool evaluates how easily women can access essential services and amenities
-
+
**Process Women's Travel Patterns factors**
From 78ecd086f2c71dd37a9e061ba9e070d6c05fb5ac Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 12:44:23 +0200
Subject: [PATCH 14/56] Update accessibility.md
---
docs/userguide/accessibility.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/docs/userguide/accessibility.md b/docs/userguide/accessibility.md
index a089b2fd..ed2a0255 100644
--- a/docs/userguide/accessibility.md
+++ b/docs/userguide/accessibility.md
@@ -52,10 +52,9 @@ This tool evaluates how easily women can access essential services and amenities
Default Women's Travel Patterns thresholds:
-
-
Distance to Facilities (meters)
+
Distance to Facilities (meters)
Score
From d2554401720008d7a89b8500435f2fc50f2bcaa7 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 12:46:39 +0200
Subject: [PATCH 15/56] Update accessibility.md
---
docs/userguide/accessibility.md | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/docs/userguide/accessibility.md b/docs/userguide/accessibility.md
index ed2a0255..120ae7fe 100644
--- a/docs/userguide/accessibility.md
+++ b/docs/userguide/accessibility.md
@@ -52,34 +52,35 @@ This tool evaluates how easily women can access essential services and amenities
Default Women's Travel Patterns thresholds:
-
+
+
-
Distance to Facilities (meters)
-
Score
+
Distance to Facilities (meters)
+
Score
0 - 400
-
5
+
5
401 - 800
-
4
+
4
801 - 1,200
-
3
+
3
1,201 - 1,500
-
2
+
2
1,501 - 2,000
-
1
+
1
Over 2,000
-
0
+
0
From 8c552e56f750d343be69b38e39ee3325257031c9 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 12:55:12 +0200
Subject: [PATCH 16/56] Update accessibility.md
---
docs/userguide/accessibility.md | 160 ++++++++++++++++++++++++++++----
1 file changed, 140 insertions(+), 20 deletions(-)
diff --git a/docs/userguide/accessibility.md b/docs/userguide/accessibility.md
index 120ae7fe..e5a6583c 100644
--- a/docs/userguide/accessibility.md
+++ b/docs/userguide/accessibility.md
@@ -138,6 +138,41 @@ The successful completion of the process is indicated by the green checkmark wid
onclick="window.open(this.src, '_blank')">
+Default Access to Public Transport thresholds:
+
+
+
+
Distance to Public Transport stops (meters)
+
Score
+
+
+
0 - 250
+
5
+
+
+
251 - 500
+
4
+
+
+
501 - 750
+
3
+
+
+
751 - 1,000
+
2
+
+
+
1,001 - 1,250
+
1
+
+
+
Over 1,250
+
0
+
+
+
+
+
**Process Access to Public Transport factor**
Back in the Data Processing Interface:
@@ -182,6 +217,41 @@ The process should be successfully completed and indicated by a green checkmark
onclick="window.open(this.src, '_blank')">
+Default Access to Health Facilities thresholds:
+
+
+
+
Distance to Health Facilities (meters)
+
Score
+
+
+
0 - 2,000
+
5
+
+
+
2,001 - 4,000
+
4
+
+
+
4,001 - 6,000
+
3
+
+
+
6,001 - 8,000
+
2
+
+
+
8,001 - 10,000
+
1
+
+
+
Over 10,000
+
0
+
+
+
+
+
**Process Access to Health Facilities factor**
Back in the Data Processing Interface:
@@ -215,6 +285,41 @@ The process should be successfully completed and indicated by a green checkmark
onclick="window.open(this.src, '_blank')">
+Default Access to Education and Training Facilities thresholds:
+
+
+
+
Distance to Facilities (meters)
+
Score
+
+
+
0 - 2,000
+
5
+
+
+
2,001 - 4,000
+
4
+
+
+
4,001 - 6,000
+
3
+
+
+
6,001 - 8,000
+
2
+
+
+
8,001 - 10,000
+
1
+
+
+
Over 10,000
+
0
+
+
+
+
+
**Process Access to Education and Training Facilities factor**
Back in the Data Processing Interface:
@@ -248,6 +353,41 @@ The process should be successfully completed and indicated by a green checkmark
onclick="window.open(this.src, '_blank')">
+Default Access to Financial Facilities thresholds:
+
+
+
+
Distance to Financial Facilities (meters)
+
Score
+
+
+
0 - 400
+
5
+
+
+
401 - 800
+
4
+
+
+
801 - 1,200
+
3
+
+
+
1,201 - 2,000
+
2
+
+
+
2,001 - 3,000
+
1
+
+
+
Over 3,000
+
0
+
+
+
+
+
**Process Access to Financial Facilities factor**
Back in the Data Processing Interface:
@@ -334,23 +474,3 @@ If the results do not immediately appear in the Layer Panel after processing the
- **Input Accuracy**: Ensure all input datasets are carefully entered/selected and correspond to the correct factors and/or subfactors. Incorrect data will impact the outputs and subsequent analysis.
- **Weight Adjustment**: Assign weights thoughtfully to reflect the importance of each factor in the overall analysis. After making changes, always balance the weights to ensure they sum up correctly.
-
-
-1 Thresholds
-
From 0e19e3f67653db3ecb2ed1a955fc1616989dc855 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 12:56:07 +0200
Subject: [PATCH 17/56] Update accessibility.md
---
docs/userguide/accessibility.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/docs/userguide/accessibility.md b/docs/userguide/accessibility.md
index e5a6583c..0468512b 100644
--- a/docs/userguide/accessibility.md
+++ b/docs/userguide/accessibility.md
@@ -439,8 +439,6 @@ After completing the process, the outputs are automatically added to the Layer P
The outputs consist of all factors and subfactors, as well as the aggregation of these into the final Accessibility output. All scores are assessed on a scale from 0 to 5, categorized as follows: ≤ 0.5 (Not Enabling) | 0.5–1.5 (Very Low Enablement) | 1.5–2.5 (Low Enablement) | 2.5–3.5 (Moderately Enabling) | 3.5–4.5 (Enabling) | 4.5–5.0 (Highly Enabling).
-[Not working - Need to be amended]
-
The outputs are stored under the Accessibility folder within the project folder created during the setup phase as raster files. These files can be shared and further utilized for various purposes, such as visualization in QGIS or other GIS software, integration into reports, overlaying with other spatial datasets, or performing advanced geospatial analyses, such as identifying priority areas or conducting trend analysis based on the scores.
If the results do not immediately appear in the Layer Panel after processing the Accessibility Dimension, you can resolve this by either adding them manually from the folder path or by right-clicking on the Accessibility Dimension and selecting **Add to map** from the context menu:
From 7354ec3017d499784eec3bf551312201f98d88bb Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 13:08:55 +0200
Subject: [PATCH 18/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index 8e772564..7b04516f 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -46,6 +46,16 @@ For certain factors, **multiple data input options** are available depending on
onclick="window.open(this.src, '_blank')">
+Active transport factor is calculated based on four subfactors averaged across the raster cells:
+
+| Subfactor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
+|----------------------|------------------|-----------------------|-------------------------|-------------------------|-------------------------|-------------------------|
+| **Street Crossings** | None | N/A | N/A | 1 crossing | N/A | 2+ crossings |
+| **Cycle Paths** | None | N/A | N/A | 1 cycle path | N/A | 2+ paths |
+| **Footpaths** | None | N/A | N/A | 1 path | N/A | 2+ paths |
+| **Block Sizes** | None | >1 km | 751m - 1 km | 501m - 750m | 251m - 500m | <250m |
+
+
**Process Active Transport factors**
Back in the Data Processing Interface:
@@ -96,7 +106,7 @@ The successful completion of the process is indicated by the green checkmark wid
onclick="window.open(this.src, '_blank')">
-> - 3️⃣ Using **Nighttime Lights data** as input; VIIRS Nighttime Lights raster may be used as proxy data for streetlight locations; select the layer already loaded in the QGIS Layer Panel from the dropdown menu or manually enter the file path for the (**raster data**) corresponding to the streetlights data by clicking the three-dot button; this layer will be used for processing:
+> - 3️⃣ Using **Nighttime Lights data** as input; VIIRS Nighttime Lights raster may be used as proxy data for presence of area illumination at night time; select the layer already loaded in the QGIS Layer Panel from the dropdown menu or manually enter the file path for the (**raster data**) corresponding to NTL by clicking the three-dot button; this layer will be used for processing:
-> - 2️⃣ Using **ACLED data** as input; select ACLED data in CSV format representing fragility, conflict, and violence events; a buffer is required to estimate the spatial impact of these events, with a default radius of 5000m; if the specific impact radius of an event is known, it should be applied instead; a pop-up will appear to validate the CSV format.
+> - 2️⃣ Using **ACLED data** as input; select ACLED data in CSV format representing fragility, conflict, and violence events; this indicator is structured by assigning scores to rasters based on their overlap with buffers indicating different types of events. Using point locations of FCV events, generate circular buffers with a default radius of 5 km to estimate the spatial impact of these events. If the impact radius of an event is known, it should be used instead; a pop-up will appear to validate the CSV format.
**Locate Environmental Hazards Section**
@@ -432,8 +442,6 @@ After completing the process, the outputs are automatically added to the Layer P
The outputs consist of all factors and subfactors, as well as the aggregation of these into the final Place Characterization output. All scores are assessed on a scale from 0 to 5, categorized as follows: ≤ 0.5 (Not Enabling) | 0.5–1.5 (Very Low Enablement) | 1.5–2.5 (Low Enablement) | 2.5–3.5 (Moderately Enabling) | 3.5–4.5 (Enabling) | 4.5–5.0 (Highly Enabling).
-[Not working - Need to be amended]
-
The outputs are stored under the Place Characterization folder within the project folder created during the setup phase as raster files. These files can be shared and further utilized for various purposes, such as visualization in QGIS or other GIS software, integration into reports, overlaying with other spatial datasets, or performing advanced geospatial analyses, such as identifying priority areas or conducting trend analysis based on the scores.
If the results do not immediately appear in the Layer Panel after processing the Place Characterization Dimension, you can resolve this by either adding them manually from the folder path or by right-clicking on the Place Characterization Dimension and selecting **Add to map** from the context menu:
From e900696a9268a7457dd0efac43a1f92fe0291d8d Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 13:29:19 +0200
Subject: [PATCH 19/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index 7b04516f..c74a6bdc 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -46,7 +46,7 @@ For certain factors, **multiple data input options** are available depending on
onclick="window.open(this.src, '_blank')">
-Active transport factor is calculated based on four subfactors averaged across the raster cells:
+Active transport factor is calculated based on the four subfactors averaged across the raster cells:
| Subfactor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
|----------------------|------------------|-----------------------|-------------------------|-------------------------|-------------------------|-------------------------|
@@ -129,6 +129,14 @@ The successful completion of the process is indicated by the green checkmark wid
> - 🚫 **Exclude Unused Factors (optional)**: If this factor is not intended to be included in the process, uncheck the **Use** button associated with it.
> - ✅ **Finalize**: Once all settings are configured, click OK to confirm and proceed to the next step.
+Safety is calculated by generating 20-meter buffers around streetlights.
+
+| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
+|----------|------------------------|-------------------------|-------------------------|-------------------------|-------------------------|-------------------------|
+| **Safety** | No overlap | 1-19% intersection | 20-39% intersection | 40-59% intersection | 60-79% intersection | 80-100% intersection |
+
+Note: Use nighttime light data only if streetlight data is unavailable.
+
**Process Safety factor**
Back in the Data Processing Interface:
From d9fcd6d0105bc8c07a41c11cd4b332eec4809e6a Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 13:33:22 +0200
Subject: [PATCH 20/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index c74a6bdc..1edeeea4 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -129,13 +129,13 @@ The successful completion of the process is indicated by the green checkmark wid
> - 🚫 **Exclude Unused Factors (optional)**: If this factor is not intended to be included in the process, uncheck the **Use** button associated with it.
> - ✅ **Finalize**: Once all settings are configured, click OK to confirm and proceed to the next step.
-Safety is calculated by generating 20-meter buffers around streetlights.
+Safety is calculated by generating 20-meter buffers around streetlights using the default thresholds:
| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
|----------|------------------------|-------------------------|-------------------------|-------------------------|-------------------------|-------------------------|
| **Safety** | No overlap | 1-19% intersection | 20-39% intersection | 40-59% intersection | 60-79% intersection | 80-100% intersection |
-Note: Use nighttime light data only if streetlight data is unavailable.
+Note: Use nighttime light data only if streetlight data is unavailable.
**Process Safety factor**
From d82d057b23c64b700f45931734199248b5b24dd0 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 13:35:28 +0200
Subject: [PATCH 21/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index 1edeeea4..a1ff22f3 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -191,6 +191,13 @@ The successful completion of the process is indicated by the green checkmark wid
> - 🚫 **Exclude Unused Factor (optional)**: If this factor is not intended to be included in the process, uncheck the **Use** button associated with it.
> - ✅ **Finalize**: Once all settings are configured, click OK to confirm and proceed to the next step.
+FCV is structured by assigning scores to raster cells based on their overlap with buffers representing different types of events. Using point locations of FCV (Fragility, Conflict, and Violence) events, create circular buffers with a radius of 5 km to estimate the spatial impact. If a specific event's impact radius is known, it should be applied instead. Raster cells intersecting with these default buffers are scored as follows:
+
+| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
+|----------|----------------------|-----------------------------|---------------------------------|---------|-----------------------------|------------------------------|
+| **FCV** | Overlaps with buffers for battles and explosions | Overlaps with buffers for explosions and remote violence | Overlaps with buffers for violence against civilians | N/A | Overlaps with buffers for protests and riots | No overlap with any event |
+
+
**Process FCV factor**
Back in the Data Processing Interface:
From f92a7c0dae49b2f51e61bf0ecc3eb789934eb3e5 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:00:02 +0200
Subject: [PATCH 22/56] Update placecharacterization.md
From 701f1de715dbbdb8b99762e4616180b662d91ee0 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:04:32 +0200
Subject: [PATCH 23/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index a1ff22f3..3292794c 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -252,6 +252,8 @@ The successful completion of the process is indicated by the green checkmark wid
> - 🚫 **Exclude Unused Factor (optional)**: If this factor is not intended to be included in the process, uncheck the **Use** button associated with it.
> - ✅ **Finalize**: Once all settings are configured, click OK to confirm and proceed to the next step.
+Education reclassifies the input data to a standardized scale from 0 to 5 using a linear scaling process. In this scale, a score of 5 represents areas where all women have a university degree, while a score of 0 represents areas where no women have a university degree.
+
**Process Education factor**
Back in the Data Processing Interface:
@@ -306,6 +308,8 @@ The successful completion of the process is indicated by the green checkmark wid
> - 🚫 **Exclude Unused Factor (optional)**: If this factor is not intended to be included in the process, uncheck the **Use** button associated with it.
> - ✅ **Finalize**: Once all settings are configured, click OK to confirm and proceed to the next step.
+Digital Inclusion reclassifies input data to a standardized scale of 0 to 5 using a linear scaling process, where 5 represents areas where 100% of households have internet access, and 0 represents areas where no households have internet access.
+
**Process Digital Inclusion factor**
Back in the Data Processing Interface:
@@ -357,6 +361,17 @@ The thresholds for defining hazard levels are based on a predefined scoring list
onclick="window.open(this.src, '_blank')">
+Environmental Hazards reclassifies input data to a standardized scale of 0 to 5 using a linear scaling process, where 5 represents areas with no environmental hazards and 0 represents areas with the highest level of hazard.
+
+| Factor | Class 0 | Class 1 | Class 2 | Class 3 | Class 4 | Class 5 |
+|------------------------------------------|--------------------|----------------------|----------------------|----------------------|----------------------|----------------------|
+| **Number of Fires per km²** | >8 | 5–8 | 2–5 | 1–2 | 0–1 | 0 or No Data |
+| **Floods Data** | 720–900 cm | 540–720 cm | 360–540 cm | 180–360 cm | <180 cm | No Data or 0 |
+| **Landslide Data** | Severe | High-Moderate2 (4) | Moderate (3) | Low-Moderate1 (2) | Slight (1) | No Data or 0 |
+| **Tropical Cyclone Frequency (100 Years)** | >100 events | 75–100 events | 50–75 events | 25–50 events | <25 events | No Data or 0 |
+| **Drought Data** | 4–5 | 3–4 | 2–3 | 1–2 | 0–1 | No Data or 0 |
+
+
**Process Environmental Hazards factors**
Back in the Data Processing Interface:
@@ -398,6 +413,12 @@ The successful completion of the process is indicated by the green checkmark wid
onclick="window.open(this.src, '_blank')">
+Water Sanitation is assessed based on the presence of water and sanitation facilities within a raster cell, applying a default 1000m buffer. The scoring is as follows:
+
+| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
+|-------------------------|---------------------------|---------|---------|-----------------------------|---------|--------------------------------|
+| **Water Sanitation** | No water points | N/A | N/A | 1 water point | N/A | 2 or more water points |
+
**Process Water sanitation factor**
Back in the Data Processing Interface:
From a7d0e4e1c7fa506e79ca143dccc45608b3c7de94 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:09:48 +0200
Subject: [PATCH 24/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 49 -------------------------
1 file changed, 49 deletions(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index 3292794c..6e40e94f 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -16,8 +16,6 @@ Place Characterization factors refer to the following indicators:
- **Environmental Hazards:** characterizes areas based on their vulnerability to natural disasters.
- **Water sanitation:** assesses the availability and accessibility of clean water and sanitation facilities.
-The default thresholds1 are listed in the footnote.
-
For certain factors, **multiple data input options** are available depending on the data's format and availability.
### Input Place Characterization factors
@@ -510,50 +508,3 @@ If the results do not immediately appear in the Layer Panel after processing the
- **Input Accuracy**: Ensure all input datasets are carefully entered/selected and correspond to the correct factors and/or subfactors. Incorrect data will impact the outputs and subsequent analysis.
- **Weight Adjustment**: Assign weights thoughtfully to reflect the importance of each factor in the overall analysis. After making changes, always balance the weights to ensure they sum up correctly.
-
-
- 1: Default thresholds
-
-
- Active transport factor is calculated based on four factors averaged across the raster cells:
-
- Street Crossings scores: (score 0 = none, score 3 = 1 crossing, score 5 = 2+ crossings)
- Cycle Paths scores: (score 0 = none, score 3 = 1 cycle path, score 5 = 2+ paths)
- Footpaths scores: (score 0 = none, score 3 = 1 path, score 5 = 2+ paths)
- Block Sizes scores: (score 0 = none, score 1 = >1 km, score 2 = 751m-1 km, score 3 = 501m-750m, score 4 = 251m-500m, score 5 = <250m)
-
- Safety is calculated by generating 20-meter buffers around streetlights. Raster cells where 80-100% of their area intersects with these buffers are assigned a score of 5. Cells with 60-79% intersection receive a score of 4, 40-59% a score of 3, 20-39% a score of 2, and 1-19% a score of 1. Cells with no overlap are scored as 0. Note: Use nighttime light data only if streetlight data is unavailable.
-
- FCV is structured by assigning scores to raster cells based on their overlap with buffers representing different types of events. Using point locations of FCV (Fragility, Conflict, and Violence) events, create circular buffers with a radius of 5 km to estimate the spatial impact. If a specific event's impact radius is known, it should be applied instead. Raster cells intersecting with these buffers are scored as follows:
-
-
Rasters overlapping with buffers for battles and explosions: score 0
-
Rasters overlapping with buffers for explosions and remote violence: score 1
-
Rasters overlapping with buffers for violence against civilians: score 2
-
Rasters overlapping with buffers for protests and riots: score 4
-
Areas with no overlap with any event: score 5
-
-
- Education reclassifies the input data to a standardized scale from 0 to 5 using a linear scaling process. In this scale, a score of 5 represents areas where all women have a university degree, while a score of 0 represents areas where no women have a university degree.
-
- Digital Inclusion reclassifies input data to a standardized scale of 0 to 5 using a linear scaling process, where 5 represents areas where 100% of households have internet access, and 0 represents areas where no households have internet access.
-
- Environmental Hazards reclassifies input data to a standardized scale of 0 to 5 using a linear scaling process, where 5 represents areas with no environmental hazards and 0 represents areas with the highest level of hazard.
-
-Number of Fires per km² is classified into GEEST classes as follows: 0 or No Data: class 5 | 0 to 1: class 4 | 1 to 2: class 3 | 2 to 5: class 2 | 5 to 8: class 1 | Greater than 8: class 0
-
-Floods data is classified into GEEST classes as follows: No Data or 0: class 5 | Less than 180 cm: class 4 | 180–360 cm: class 3 | 360–540 cm: class 2 | 540–720 cm: class 1 | 720–900 cm: class 0
-
-Landslide data is classified into GEEST classes as follows: No Data or 0: class 5 | Slight (1): class 4 | Low Moderate1 (2): class 3 | Moderate (3): class 2 | High-Moderate2 (4): class 1 | Severe (5): class 0
-
-Tropical Cyclone frequency per 100 years is classified into GEEST classes as follows: No Data or 0: class 5 | Less than 25 events: class 4 | 25–50 events: class 3 | 50–75 events: class 2 | 75–100 events: class 1 | More than 100 events: class 0
-
-Drought data is classified into GEEST classes as follows: No Data or 0: class 5 | 0–1: class 4 | 1–2: class 3 | 2–3: class 2 | 3–4: class 1 | 4–5: class 0
-
- Water Sanitation is assessed based on the presence of water and sanitation facilities within a raster cell, applying a default 1000m buffer. The scoring is as follows:
-
-The final aggregation of all three dimensions is carried out by applying weights to each dimension, ensuring a balanced and comprehensive representation. This tab provides users with deeper insights by identifying regions where conditions are optimal—or at least favorable—for women to secure employment in specific sectors or to enhance existing job sites. The outputs from this tab assign a combined classification score to the input raster and extract aggregated polygons or administrative units intersecting these regions. Additionally, GEEST highlights key infrastructure investments that could boost women's participation in the workforce. Population data can also be incorporated into the analysis to provide more nuanced insights that account for both population levels and enablement. This process integrates proximity data, classification scores, population data and weighted dimensions to support informed decision-making.
+The final aggregation of all three dimensions is carried out by applying weights to each dimension, ensuring a balanced and comprehensive representation. This tab provides users with deeper insights by identifying regions where conditions are optimal—or at least favorable—for women to access job opportunities in a specific sector. The outputs from this tab assign a combined classification score to the input raster, aggregate results at desired adminstrative level and extract aggregated polygons or administrative units intersecting these regions. Additionally, GEEST highlights key infrastructure investments that could boost women's participation in the workforce. Population data can also be incorporated into the analysis to provide more nuanced insights that account for both population levels and enablement. This process integrates proximity data, classification scores, population data and weighted dimensions to support informed decision-making.
-### Processing all 3 dimensions (WEE Score)
+### Computing the Women’s Enablement Environments Indicator (WEE)
---
Before accessing the full insights tab, it is necessary to aggregate all the factors processed in the previous steps. This is achieved by assigning weights to each dimension, resulting in the WEE (Women’s Enablement Environments) output. The output is categorized into five classes: highly enabling, enabling, moderately enabling, low enablement and very low enablement.
@@ -103,14 +103,12 @@ After completing the process, the outputs are automatically added to the Layer P
The outputs consist of all WEE score outputs (aggreagated at administrative level, by population or by job distribution). All scores are assessed on a scale from 0 to 15, categorized as follows: 14 - 15: Highly enabling, high population | 13 - 14: Highly enabling, medium population | 12 - 13: Highly enabling, low population | 11 - 12: Enabling, high population | 10 - 11: Enabling, medium population | 9 - 10: Enabling, low population | 8 - 9: Moderately enabling, high population | 7 - 8: Moderately enabling, medium population | 6 - 7: Moderately enabling, low population | 5 - 6: Low enablement, high population | 4 - 5: Low enablement, medium population | 3 - 4: Low enablement, low population | 2 - 3: Very low enablement, high population | 1 - 2: Very low enablement, medium population | 0 - 1: Very low enablement, low population.
-[Not working - Need to be amended]
-
The outputs are stored within the project folder created during the setup phase as raster files. These files can be shared and further utilized for various purposes, such as visualization in QGIS or other GIS software, integration into reports, overlaying with other spatial datasets, or performing advanced geospatial analyses, such as identifying priority areas or conducting trend analysis based on the scores.
If the results do not immediately appear in the Layer Panel after processing the WEE Scores, you can resolve this by either adding them manually from the folder path or by right-clicking on the WEE Score and selecting **Add to map** from the context menu.
-**🖥️ Key Features of WEE Score**
+**🖥️ Key attributes of WEE Score tab**
Date: Thu, 6 Feb 2025 14:18:22 +0200
Subject: [PATCH 26/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index 6e40e94f..6c7138e5 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -191,9 +191,9 @@ The successful completion of the process is indicated by the green checkmark wid
FCV is structured by assigning scores to raster cells based on their overlap with buffers representing different types of events. Using point locations of FCV (Fragility, Conflict, and Violence) events, create circular buffers with a radius of 5 km to estimate the spatial impact. If a specific event's impact radius is known, it should be applied instead. Raster cells intersecting with these default buffers are scored as follows:
-| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
-|----------|----------------------|-----------------------------|---------------------------------|---------|-----------------------------|------------------------------|
-| **FCV** | Overlaps with buffers for battles and explosions | Overlaps with buffers for explosions and remote violence | Overlaps with buffers for violence against civilians | N/A | Overlaps with buffers for protests and riots | No overlap with any event |
+| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
+|----------|----------------------|-----------------------------|---------------------------------|----------------|-----------------------------|------------------------------|
+| **FCV** | Overlaps with buffers for battles and explosions | Overlaps with buffers for explosions and remote violence | Overlaps with buffers for violence against civilians | N/A | Overlaps with buffers for protests and riots | No overlap with any event |
**Process FCV factor**
From f5e51d0d0997b03325541fadf7cc2d520c028afc Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:23:17 +0200
Subject: [PATCH 27/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index 6c7138e5..89a37f22 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -191,8 +191,8 @@ The successful completion of the process is indicated by the green checkmark wid
FCV is structured by assigning scores to raster cells based on their overlap with buffers representing different types of events. Using point locations of FCV (Fragility, Conflict, and Violence) events, create circular buffers with a radius of 5 km to estimate the spatial impact. If a specific event's impact radius is known, it should be applied instead. Raster cells intersecting with these default buffers are scored as follows:
-| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
-|----------|----------------------|-----------------------------|---------------------------------|----------------|-----------------------------|------------------------------|
+| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
+|----------|----------------------|-----------------------------|---------------------------------|-------------|-----------------------------|------------------------------|
| **FCV** | Overlaps with buffers for battles and explosions | Overlaps with buffers for explosions and remote violence | Overlaps with buffers for violence against civilians | N/A | Overlaps with buffers for protests and riots | No overlap with any event |
From fc403626afa17b7facbad8f64ec3e6aa4eb57040 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:23:54 +0200
Subject: [PATCH 28/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index 89a37f22..d0841abb 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -191,8 +191,8 @@ The successful completion of the process is indicated by the green checkmark wid
FCV is structured by assigning scores to raster cells based on their overlap with buffers representing different types of events. Using point locations of FCV (Fragility, Conflict, and Violence) events, create circular buffers with a radius of 5 km to estimate the spatial impact. If a specific event's impact radius is known, it should be applied instead. Raster cells intersecting with these default buffers are scored as follows:
-| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
-|----------|----------------------|-----------------------------|---------------------------------|-------------|-----------------------------|------------------------------|
+| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
+|----------|----------------------|-----------------------------|---------------------------------|---------------------|-----------------------------|------------------------------|
| **FCV** | Overlaps with buffers for battles and explosions | Overlaps with buffers for explosions and remote violence | Overlaps with buffers for violence against civilians | N/A | Overlaps with buffers for protests and riots | No overlap with any event |
From 195fb5a31304290050296e65e3c98cc6411ec52a Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:26:45 +0200
Subject: [PATCH 29/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index d0841abb..e01be364 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -191,8 +191,8 @@ The successful completion of the process is indicated by the green checkmark wid
FCV is structured by assigning scores to raster cells based on their overlap with buffers representing different types of events. Using point locations of FCV (Fragility, Conflict, and Violence) events, create circular buffers with a radius of 5 km to estimate the spatial impact. If a specific event's impact radius is known, it should be applied instead. Raster cells intersecting with these default buffers are scored as follows:
-| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
-|----------|----------------------|-----------------------------|---------------------------------|---------------------|-----------------------------|------------------------------|
+| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
+|----------|----------------------|---------------------------|---------------------------------|---------------------|-----------------------------|------------------------------|
| **FCV** | Overlaps with buffers for battles and explosions | Overlaps with buffers for explosions and remote violence | Overlaps with buffers for violence against civilians | N/A | Overlaps with buffers for protests and riots | No overlap with any event |
From a1d61bd7813d7b86f531eb60f1bff90e1a52fae0 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:28:08 +0200
Subject: [PATCH 30/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index e01be364..f0c3efac 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -193,7 +193,7 @@ The successful completion of the process is indicated by the green checkmark wid
| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
|----------|----------------------|---------------------------|---------------------------------|---------------------|-----------------------------|------------------------------|
-| **FCV** | Overlaps with buffers for battles and explosions | Overlaps with buffers for explosions and remote violence | Overlaps with buffers for violence against civilians | N/A | Overlaps with buffers for protests and riots | No overlap with any event |
+| **FCV** | battles and explosions | explosions and remote violence | violence against civilians | N/A | protests and riots | No overlap with any event |
**Process FCV factor**
From 61d2e12aa4aebac3bfaff4feea36a3190ff799fb Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:29:56 +0200
Subject: [PATCH 31/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index f0c3efac..f6eba218 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -193,7 +193,7 @@ The successful completion of the process is indicated by the green checkmark wid
| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
|----------|----------------------|---------------------------|---------------------------------|---------------------|-----------------------------|------------------------------|
-| **FCV** | battles and explosions | explosions and remote violence | violence against civilians | N/A | protests and riots | No overlap with any event |
+| **FCV** | battles and explosions | explosions and remote violence | violence against civilians | N/A | protests and riots | No overlap with any event |
**Process FCV factor**
From d8063476b58a4eb0dead234df3fbe3cd6e4a3186 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Thu, 6 Feb 2025 12:31:05 +0000
Subject: [PATCH 32/56] Publish in QGIS repo Fixes kartoza/GEEST2#33
---
admin.py | 2 +-
config.json | 4 +-
geest/LICENSE | 220 ++++
geest/test-old/__init__.py | 5 -
geest/test-old/qgis_interface.py | 226 ----
geest/test-old/tenbytenraster.asc | 19 -
geest/test-old/tenbytenraster.asc.aux.xml | 13 -
geest/test-old/tenbytenraster.keywords | 1 -
geest/test-old/tenbytenraster.lic | 18 -
geest/test-old/tenbytenraster.prj | 1 -
geest/test-old/tenbytenraster.qml | 26 -
geest/test-old/test_animation_controller.py | 1317 -------------------
geest/test-old/test_init.py | 75 --
geest/test-old/test_movie_creator.py | 217 ---
geest/test-old/test_qgis_environment.py | 35 -
geest/test-old/test_translations.py | 57 -
geest/test-old/utilities.py | 106 --
shell.nix | 3 +
18 files changed, 226 insertions(+), 2119 deletions(-)
create mode 100644 geest/LICENSE
delete mode 100644 geest/test-old/__init__.py
delete mode 100644 geest/test-old/qgis_interface.py
delete mode 100644 geest/test-old/tenbytenraster.asc
delete mode 100644 geest/test-old/tenbytenraster.asc.aux.xml
delete mode 100644 geest/test-old/tenbytenraster.keywords
delete mode 100644 geest/test-old/tenbytenraster.lic
delete mode 100644 geest/test-old/tenbytenraster.prj
delete mode 100644 geest/test-old/tenbytenraster.qml
delete mode 100644 geest/test-old/test_animation_controller.py
delete mode 100644 geest/test-old/test_init.py
delete mode 100644 geest/test-old/test_movie_creator.py
delete mode 100644 geest/test-old/test_qgis_environment.py
delete mode 100644 geest/test-old/test_translations.py
delete mode 100644 geest/test-old/utilities.py
diff --git a/admin.py b/admin.py
index 5fcaa0b8..dc551607 100644
--- a/admin.py
+++ b/admin.py
@@ -197,7 +197,7 @@ def build(
icon_path = copy_icon(output_directory)
if icon_path is None:
_log("Could not copy icon", context=context)
- compile_resources(context, output_directory)
+ # compile_resources(context, output_directory)
add_requirements_file(context, output_directory)
generate_metadata(context, output_directory)
return output_directory
diff --git a/config.json b/config.json
index 2bfc8c48..064fea8f 100644
--- a/config.json
+++ b/config.json
@@ -6,14 +6,14 @@
"icon": "icon.png",
"experimental": true,
"deprecated": false,
- "homepage": "",
+ "homepage": "https://github.com/worldbank/GEEST",
"tracker": "https://github.com/worldbank/GEEST/issues",
"repository": "https://github.com/worldbank/GEEST",
"tags": [],
"category": ["plugins"],
"hasProcessingProvider": "no",
"about": "GEEST was built for the practical implementation of the Geospatial Women’s Employment Analytical Framework (GeoWEAF), developed by the World Bank, which identifies the location-specific factors affecting women's access to jobs. This framework categorizes regions based on their level of support for enabling women’s access to employment. It identifies 15 critical, spatially varying factors that influence women’s job prospects, divided into three main dimensions: Contextual, Accessibility, and Place Characterization.",
- "author": "Kartoza for/and The World Bank",
+ "author": "Kartoza for and with The World Bank",
"email": "info@kartoza.com, gost@worldbank.org",
"description": "Gender Enabling Environments Spatial Tool",
"version": "0.5.5",
diff --git a/geest/LICENSE b/geest/LICENSE
new file mode 100644
index 00000000..6f64e751
--- /dev/null
+++ b/geest/LICENSE
@@ -0,0 +1,220 @@
+# GNU GENERAL PUBLIC LICENSE
+### Version 3, 29 June 2007
+
+[GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html) (GPL-3.0-only)
+
+## Preamble
+
+The GNU General Public License is a free, copyleft license for software and other kinds of works.
+
+The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
+
+For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute, and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
+
+Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
+
+Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
+
+The precise terms and conditions for copying, distribution and modification follow.
+
+## TERMS AND CONDITIONS
+
+### 0. Definitions.
+
+"This License" refers to version 3 of the GNU General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the Program.
+
+To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
+
+### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+
+### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+
+### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
+
+### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive**`LICENSE.md`** continued:
+```markdown
+terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
+
+### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
+
+- a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
+
+### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
+
+- a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
+- d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
+
+### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the**`LICENSE.md`** continued:
+```markdown
+above requirements apply either way.
+
+### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
+
+### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
+
+### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
+
+### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
+
+A contributor's essential patent claims are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
+
+### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
+
+### 13. Use with the GNU Affero General Public License.
+
+Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
+
+### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
+
+### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVE**`LICENSE.md`** continued:
+```markdown
+YS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
+
+## END OF TERMS AND CONDITIONS
+
+### How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
+
+
diff --git a/geest/test-old/__init__.py b/geest/test-old/__init__.py
deleted file mode 100644
index 884794d9..00000000
--- a/geest/test-old/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""
-import qgis libs so that we set the correct sip api version
-"""
-
-import qgis # NOQA
diff --git a/geest/test-old/qgis_interface.py b/geest/test-old/qgis_interface.py
deleted file mode 100644
index 2f4e5285..00000000
--- a/geest/test-old/qgis_interface.py
+++ /dev/null
@@ -1,226 +0,0 @@
-# coding=utf-8
-"""QGIS plugin implementation.
-
-.. note:: This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
-.. note:: This source code was copied from the 'postgis viewer' application
- with original authors:
- Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk
- Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org
- Copyright (c) 2014 Tim Sutton, tim@linfiniti.com
-
-"""
-
-__author__ = "tim@linfiniti.com"
-__revision__ = "$Format:%H$"
-__date__ = "10/01/2011"
-__copyright__ = (
- "Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk and "
- "Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org"
- "Copyright (c) 2014 Tim Sutton, tim@linfiniti.com"
-)
-
-import logging
-from typing import List
-from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QSize
-from qgis.PyQt.QtWidgets import QDockWidget
-from qgis.core import QgsProject, QgsMapLayer
-from qgis.gui import QgsMapCanvas, QgsMessageBar
-
-LOGGER = logging.getLogger("QGIS")
-
-
-# noinspection PyMethodMayBeStatic,PyPep8Naming
-class QgisInterface(QObject):
- """Class to expose QGIS objects and functions to plugins.
-
- This class is here for enabling us to run unit tests only,
- so most methods are simply stubs.
- """
-
- currentLayerChanged = pyqtSignal(QgsMapLayer)
-
- def __init__(self, canvas: QgsMapCanvas):
- """Constructor
- :param canvas:
- """
- QObject.__init__(self)
- self.canvas = canvas
- # Set up slots so we can mimic the behaviour of QGIS when layers
- # are added.
- LOGGER.debug("Initialising canvas...")
- # noinspection PyArgumentList
- QgsProject.instance().layersAdded.connect(self.addLayers)
- # noinspection PyArgumentList
- QgsProject.instance().layerWasAdded.connect(self.addLayer)
- # noinspection PyArgumentList
- QgsProject.instance().removeAll.connect(self.removeAllLayers)
-
- # For processing module
- self.destCrs = None
-
- self.message_bar = QgsMessageBar()
-
- def addLayers(self, layers: List[QgsMapLayer]):
- """Handle layers being added to the registry so they show up in canvas.
-
- :param layers: list list of map layers that were added
-
- .. note:: The QgsInterface api does not include this method,
- it is added here as a helper to facilitate testing.
- """
- # LOGGER.debug('addLayers called on qgis_interface')
- # LOGGER.debug('Number of layers being added: %s' % len(layers))
- # LOGGER.debug('Layer Count Before: %s' % len(self.canvas.layers()))
- current_layers = self.canvas.layers()
- final_layers = []
- for layer in current_layers:
- final_layers.append(layer)
- for layer in layers:
- final_layers.append(layer)
-
- self.canvas.setLayers(final_layers)
- # LOGGER.debug('Layer Count After: %s' % len(self.canvas.layers()))
-
- def addLayer(self, layer: QgsMapLayer):
- """Handle a layer being added to the registry so it shows up in canvas.
-
- :param layer: list list of map layers that were added
-
- .. note: The QgsInterface api does not include this method, it is added
- here as a helper to facilitate testing.
-
- .. note: The addLayer method was deprecated in QGIS 1.8 so you should
- not need this method much.
- """
- pass # pylint: disable=unnecessary-pass
-
- @pyqtSlot()
- def removeAllLayers(self): # pylint: disable=no-self-use
- """Remove layers from the canvas before they get deleted."""
- self.canvas.setLayers([])
-
- def newProject(self): # pylint: disable=no-self-use
- """Create new project."""
- # noinspection PyArgumentList
- QgsProject.instance().clear()
-
- # ---------------- API Mock for QgsInterface follows -------------------
-
- def zoomFull(self):
- """Zoom to the map full extent."""
- pass # pylint: disable=unnecessary-pass
-
- def zoomToPrevious(self):
- """Zoom to previous view extent."""
- pass # pylint: disable=unnecessary-pass
-
- def zoomToNext(self):
- """Zoom to next view extent."""
- pass # pylint: disable=unnecessary-pass
-
- def zoomToActiveLayer(self):
- """Zoom to extent of active layer."""
- pass # pylint: disable=unnecessary-pass
-
- def addVectorLayer(self, path: str, base_name: str, provider_key: str):
- """Add a vector layer.
-
- :param path: Path to layer.
- :type path: str
-
- :param base_name: Base name for layer.
- :type base_name: str
-
- :param provider_key: Provider key e.g. 'ogr'
- :type provider_key: str
- """
- pass # pylint: disable=unnecessary-pass
-
- def addRasterLayer(self, path: str, base_name: str):
- """Add a raster layer given a raster layer file name
-
- :param path: Path to layer.
- :type path: str
-
- :param base_name: Base name for layer.
- :type base_name: str
- """
- pass # pylint: disable=unnecessary-pass
-
- def activeLayer(self) -> QgsMapLayer: # pylint: disable=no-self-use
- """Get pointer to the active layer (layer selected in the legend)."""
- # noinspection PyArgumentList
- layers = QgsProject.instance().mapLayers()
- for item in layers:
- return layers[item]
-
- def addToolBarIcon(self, action):
- """Add an icon to the plugins toolbar.
-
- :param action: Action to add to the toolbar.
- :type action: QAction
- """
- pass # pylint: disable=unnecessary-pass
-
- def removeToolBarIcon(self, action):
- """Remove an action (icon) from the plugin toolbar.
-
- :param action: Action to add to the toolbar.
- :type action: QAction
- """
- pass # pylint: disable=unnecessary-pass
-
- def addToolBar(self, name):
- """Add toolbar with specified name.
-
- :param name: Name for the toolbar.
- :type name: str
- """
- pass # pylint: disable=unnecessary-pass
-
- def mapCanvas(self) -> QgsMapCanvas:
- """Return a pointer to the map canvas."""
- return self.canvas
-
- def mainWindow(self):
- """Return a pointer to the main window.
-
- In case of QGIS it returns an instance of QgisApp.
- """
- pass # pylint: disable=unnecessary-pass
-
- def addDockWidget(self, area, dock_widget: QDockWidget):
- """Add a dock widget to the main window.
-
- :param area: Where in the ui the dock should be placed.
- :type area:
-
- :param dock_widget: A dock widget to add to the UI.
- :type dock_widget: QDockWidget
- """
- pass # pylint: disable=unnecessary-pass
-
- def legendInterface(self):
- """Get the legend."""
- return self.canvas
-
- def iconSize(self, dockedToolbar) -> int:
- """
- Returns the toolbar icon size.
- :param dockedToolbar: If True, the icon size
- for toolbars contained within docks is returned.
- """
- if dockedToolbar:
- return QSize(16, 16)
-
- return QSize(24, 24)
-
- def messageBar(self) -> QgsMessageBar:
- """
- Return the message bar of the main app
- """
- return self.message_bar
diff --git a/geest/test-old/tenbytenraster.asc b/geest/test-old/tenbytenraster.asc
deleted file mode 100644
index 96a0ee1f..00000000
--- a/geest/test-old/tenbytenraster.asc
+++ /dev/null
@@ -1,19 +0,0 @@
-NCOLS 10
-NROWS 10
-XLLCENTER 1535380.000000
-YLLCENTER 5083260.000000
-DX 10
-DY 10
-NODATA_VALUE -9999
-0 1 2 3 4 5 6 7 8 9
-0 1 2 3 4 5 6 7 8 9
-0 1 2 3 4 5 6 7 8 9
-0 1 2 3 4 5 6 7 8 9
-0 1 2 3 4 5 6 7 8 9
-0 1 2 3 4 5 6 7 8 9
-0 1 2 3 4 5 6 7 8 9
-0 1 2 3 4 5 6 7 8 9
-0 1 2 3 4 5 6 7 8 9
-0 1 2 3 4 5 6 7 8 9
-CRS
-NOTES
diff --git a/geest/test-old/tenbytenraster.asc.aux.xml b/geest/test-old/tenbytenraster.asc.aux.xml
deleted file mode 100644
index cfb15788..00000000
--- a/geest/test-old/tenbytenraster.asc.aux.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
- Point
-
-
-
- 9
- 4.5
- 0
- 2.872281323269
-
-
-
diff --git a/geest/test-old/tenbytenraster.keywords b/geest/test-old/tenbytenraster.keywords
deleted file mode 100644
index 8be3f615..00000000
--- a/geest/test-old/tenbytenraster.keywords
+++ /dev/null
@@ -1 +0,0 @@
-title: Tenbytenraster
diff --git a/geest/test-old/tenbytenraster.lic b/geest/test-old/tenbytenraster.lic
deleted file mode 100644
index 83455332..00000000
--- a/geest/test-old/tenbytenraster.lic
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- Tim Sutton, Linfiniti Consulting CC
-
-
-
- tenbytenraster.asc
- 2700044251
- Yes
- Tim Sutton
- Tim Sutton (QGIS Source Tree)
- Tim Sutton
- This data is publicly available from QGIS Source Tree. The original
- file was created and contributed to QGIS by Tim Sutton.
-
-
-
diff --git a/geest/test-old/tenbytenraster.prj b/geest/test-old/tenbytenraster.prj
deleted file mode 100644
index a30c00a5..00000000
--- a/geest/test-old/tenbytenraster.prj
+++ /dev/null
@@ -1 +0,0 @@
-GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
\ No newline at end of file
diff --git a/geest/test-old/tenbytenraster.qml b/geest/test-old/tenbytenraster.qml
deleted file mode 100644
index 85247d44..00000000
--- a/geest/test-old/tenbytenraster.qml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
-
diff --git a/geest/test-old/test_animation_controller.py b/geest/test-old/test_animation_controller.py
deleted file mode 100644
index e8307ca7..00000000
--- a/geest/test-old/test_animation_controller.py
+++ /dev/null
@@ -1,1317 +0,0 @@
-# coding=utf-8
-"""GUI Utils Test.
-
-.. note:: This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
-"""
-
-# pylint: disable=too-many-lines
-
-__author__ = "(C) 2018 by Nyall Dawson"
-__date__ = "20/04/2018"
-__copyright__ = "Copyright 2018, North Road"
-# This will get replaced with a git SHA1 when you do a git archive
-__revision__ = "$Format:%H$"
-
-import unittest
-
-from qgis.PyQt.QtCore import QSize, QEasingCurve
-from qgis.core import (
- QgsMapSettings,
- QgsRectangle,
- QgsCoordinateReferenceSystem,
- QgsReferencedRectangle,
- QgsVectorLayer,
- QgsFeature,
- QgsGeometry,
- QgsPointXY,
-)
-
-from animation_workbench.core import AnimationController, MapMode
-from .utilities import get_qgis_app
-
-QGIS_APP = get_qgis_app()
-
-
-class AnimationControllerTest(unittest.TestCase):
- """Test AnimationController works."""
-
- # pylint: disable=too-many-statements
-
- def test_fixed_extent(self):
- """
- Test a fixed extent job
- """
- map_settings = QgsMapSettings()
- map_settings.setExtent(QgsRectangle(1, 2, 3, 4))
- map_settings.setDestinationCrs(QgsCoordinateReferenceSystem("EPSG:4326"))
- map_settings.setOutputSize(QSize(400, 300))
- extent = QgsReferencedRectangle(
- map_settings.extent(), map_settings.destinationCrs()
- )
- controller = AnimationController.create_fixed_extent_controller(
- map_settings=map_settings,
- output_mode="1280:720",
- feature_layer=None,
- output_extent=extent,
- total_frames=5,
- frame_rate=10,
- )
-
- it = controller.create_jobs()
- # should be 5 frames
- job = next(it)
- self.assertEqual(job.map_settings.extent(), map_settings.extent())
- self.assertEqual(job.map_settings.frameRate(), 10)
- self.assertEqual(job.map_settings.currentFrame(), 0)
- self.assertAlmostEqual(job.map_settings.scale(), 1122330, delta=120000)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 1122330,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 0
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_rate"), 10
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 5
- )
- job = next(it)
- self.assertEqual(job.map_settings.extent(), map_settings.extent())
- self.assertAlmostEqual(job.map_settings.scale(), 1122330, delta=120000)
- self.assertEqual(job.map_settings.frameRate(), 10)
- self.assertEqual(job.map_settings.currentFrame(), 1)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 1122330,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_rate"), 10
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 5
- )
- job = next(it)
- self.assertEqual(job.map_settings.extent(), map_settings.extent())
- self.assertAlmostEqual(job.map_settings.scale(), 1122330, delta=120000)
- self.assertEqual(job.map_settings.frameRate(), 10)
- self.assertEqual(job.map_settings.currentFrame(), 2)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 1122330,
- delta=1200003,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_rate"), 10
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 5
- )
- job = next(it)
- self.assertEqual(job.map_settings.extent(), map_settings.extent())
- self.assertAlmostEqual(job.map_settings.scale(), 1122330, delta=120000)
- self.assertEqual(job.map_settings.frameRate(), 10)
- self.assertEqual(job.map_settings.currentFrame(), 3)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 1122330,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 3
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_rate"), 10
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 5
- )
- job = next(it)
- self.assertEqual(job.map_settings.extent(), map_settings.extent())
- self.assertAlmostEqual(job.map_settings.scale(), 1122330, delta=120000)
- self.assertEqual(job.map_settings.frameRate(), 10)
- self.assertEqual(job.map_settings.currentFrame(), 4)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 1122330,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 4
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_rate"), 10
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 5
- )
-
- with self.assertRaises(StopIteration):
- next(it)
-
- def test_fixed_extent_with_layer(self):
- """
- Test a fixed extent job with a layer
- """
-
- vl = QgsVectorLayer("Point?crs=EPSG:4326&field=name:string", "vl", "memory")
- self.assertTrue(vl.isValid())
-
- f = QgsFeature(vl.fields())
- f["name"] = "f1"
- f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1, 2)))
- self.assertTrue(vl.dataProvider().addFeature(f))
-
- f["name"] = "f2"
- f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10, 20)))
- self.assertTrue(vl.dataProvider().addFeature(f))
-
- map_settings = QgsMapSettings()
- map_settings.setExtent(QgsRectangle(1, 2, 3, 4))
- map_settings.setDestinationCrs(QgsCoordinateReferenceSystem("EPSG:4326"))
- map_settings.setOutputSize(QSize(400, 300))
- extent = QgsReferencedRectangle(
- map_settings.extent(), map_settings.destinationCrs()
- )
- controller = AnimationController.create_fixed_extent_controller(
- map_settings=map_settings,
- output_mode=None, # Will use map canvas dimensions
- feature_layer=vl,
- output_extent=extent,
- total_frames=2,
- frame_rate=10,
- )
-
- it = controller.create_jobs()
- # should be 4 frames
- job = next(it)
- self.assertEqual(job.map_settings.extent(), map_settings.extent())
- self.assertEqual(job.map_settings.frameRate(), 10)
- self.assertEqual(job.map_settings.currentFrame(), 0)
- self.assertAlmostEqual(job.map_settings.scale(), 2693593, delta=120000)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 2693593,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 0
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_rate"), 10
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_hover_frame"),
- 0,
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature_id"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature").id(),
- 1,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature_id"),
- 1,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_frames"),
- 2,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Hovering",
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 1)
- # self.assertEqual(job.map_settings.expressionContext().variable('total_frame_count'), 4)
- job = next(it)
- self.assertEqual(job.map_settings.extent(), map_settings.extent())
- self.assertAlmostEqual(job.map_settings.scale(), 2693593, delta=120000)
- self.assertEqual(job.map_settings.frameRate(), 10)
- self.assertEqual(job.map_settings.currentFrame(), 1)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 2693593,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_rate"), 10
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_hover_frame"),
- 1,
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature_id"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature").id(),
- 1,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature_id"),
- 1,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_frames"),
- 2,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Hovering",
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 1)
- # self.assertEqual(job.map_settings.expressionContext().variable('total_frame_count'), 4)
- job = next(it)
- self.assertEqual(job.map_settings.extent(), map_settings.extent())
- self.assertAlmostEqual(job.map_settings.scale(), 2693593, delta=120000)
- self.assertEqual(job.map_settings.frameRate(), 10)
- self.assertEqual(job.map_settings.currentFrame(), 2)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 2693593,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_rate"), 10
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_hover_frame"),
- 0,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature_id"), 1
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("next_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("next_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature").id(),
- 2,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature_id"),
- 2,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_frames"),
- 2,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Hovering",
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- # self.assertEqual(job.map_settings.expressionContext().variable('total_frame_count'), 4)
- job = next(it)
- self.assertEqual(job.map_settings.extent(), map_settings.extent())
- self.assertAlmostEqual(job.map_settings.scale(), 2693593, delta=120000)
- self.assertEqual(job.map_settings.frameRate(), 10)
- self.assertEqual(job.map_settings.currentFrame(), 3)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 2693593,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 3
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_rate"), 10
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_hover_frame"),
- 1,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature_id"), 1
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("next_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("next_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature").id(),
- 2,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature_id"),
- 2,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_frames"),
- 2,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Hovering",
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- # self.assertEqual(job.map_settings.expressionContext().variable('total_frame_count'), 4)
-
- with self.assertRaises(StopIteration):
- next(it)
-
- def test_planar(self):
- """
- Test a planar job
- """
-
- vl = QgsVectorLayer("Point?crs=EPSG:4326&field=name:string", "vl", "memory")
- self.assertTrue(vl.isValid())
-
- f = QgsFeature(vl.fields())
- f["name"] = "f1"
- f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1, 2)))
- self.assertTrue(vl.dataProvider().addFeature(f))
-
- f["name"] = "f2"
- f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10, 20)))
- self.assertTrue(vl.dataProvider().addFeature(f))
-
- map_settings = QgsMapSettings()
- map_settings.setExtent(QgsRectangle(1, 2, 3, 4))
- map_settings.setDestinationCrs(QgsCoordinateReferenceSystem("EPSG:4326"))
- map_settings.setOutputSize(QSize(400, 300))
- controller = AnimationController.create_moving_extent_controller(
- map_settings=map_settings,
- output_mode=None, # Will use map canvas dimensions
- mode=MapMode.PLANAR,
- feature_layer=vl,
- travel_duration=2,
- hover_duration=1,
- min_scale=2000000,
- max_scale=1000000,
- pan_easing=QEasingCurve(QEasingCurve.Type.Linear),
- zoom_easing=QEasingCurve(QEasingCurve.Type.Linear),
- frame_rate=2,
- )
-
- it = controller.create_jobs()
-
- job = next(it)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 0)
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 1, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 2, 2)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 959999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 0
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature_id"), 1
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature_id"), 2
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("from_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("from_feature_id")
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("to_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("to_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_hover_frame"),
- 0,
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_travel_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_frames"),
- 2,
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("travel_frames")
- )
-
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 1)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 8
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Hovering",
- )
- job = next(it)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 1, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 2, 2)
- # Changed from 44
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 1)
- # Changed from 44
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 959999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 1
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature_id"), 1
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature_id"), 2
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("from_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("from_feature_id")
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("to_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("to_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_hover_frame"),
- 1,
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_travel_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_frames"),
- 2,
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("travel_frames")
- )
-
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 1)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 8
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Hovering",
- )
-
- # now we start panning
- job = next(it)
- # Changed from 44
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 1, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 2, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 2)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 959999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 2
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature_id"), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature_id"), 2
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature_id")
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("next_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("next_feature_id")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_hover_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_travel_frame"), 0
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("hover_frames"))
- self.assertEqual(
- job.map_settings.expressionContext().variable("travel_frames"), 4
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 8
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Travelling",
- )
-
- job = next(it)
- self.assertAlmostEqual(job.map_settings.scale(), 1599999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 4, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 8, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 3)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 1599999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 3
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature_id")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature_id")
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("next_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("next_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature_id"), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature_id"), 2
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_hover_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_travel_frame"), 1
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("hover_frames"))
- self.assertEqual(
- job.map_settings.expressionContext().variable("travel_frames"), 4
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 8
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Travelling",
- )
-
- job = next(it)
- # was 16666
- self.assertAlmostEqual(job.map_settings.scale(), 1599999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 7, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 14, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 4)
- # Was 1666666
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 1599999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 4
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature_id")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature_id")
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("next_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("next_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature_id"), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature_id"), 2
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_hover_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_travel_frame"), 2
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("hover_frames"))
- self.assertEqual(
- job.map_settings.expressionContext().variable("travel_frames"), 4
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 8
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Travelling",
- )
-
- job = next(it)
- # was 10
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 10, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 20, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 5)
- # was 10
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 959999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 5
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature_id")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("previous_feature_id")
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("next_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("next_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature_id"), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature_id"), 2
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_hover_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_travel_frame"), 3
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("hover_frames"))
- self.assertEqual(
- job.map_settings.expressionContext().variable("travel_frames"), 4
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 8
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Travelling",
- )
-
- # back to hovering
-
- job = next(it)
- # was 10
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 10, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 20, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 6)
- # was 19
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 959999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 6
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature_id"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature_id"), 1
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("next_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("next_feature_id")
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("from_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("from_feature_id")
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("to_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("to_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_hover_frame"),
- 0,
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_travel_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_frames"),
- 2,
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("travel_frames")
- )
-
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 8
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Hovering",
- )
-
- job = next(it)
- # was 10
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 10, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 20, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 7)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 959999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 7
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature_id"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature_id"), 1
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("next_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("next_feature_id")
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("from_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("from_feature_id")
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("to_feature"))
- self.assertIsNone(
- job.map_settings.expressionContext().variable("to_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_hover_frame"),
- 1,
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_travel_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_frames"),
- 2,
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("travel_frames")
- )
-
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 8
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Hovering",
- )
-
- with self.assertRaises(StopIteration):
- next(it)
-
- def test_planar_loop(self):
- """
- Test a planar job with looping
- """
-
- vl = QgsVectorLayer("Point?crs=EPSG:4326&field=name:string", "vl", "memory")
- self.assertTrue(vl.isValid())
-
- f = QgsFeature(vl.fields())
- f["name"] = "f1"
- f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1, 2)))
- self.assertTrue(vl.dataProvider().addFeature(f))
-
- f["name"] = "f2"
- f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10, 20)))
- self.assertTrue(vl.dataProvider().addFeature(f))
-
- map_settings = QgsMapSettings()
- map_settings.setExtent(QgsRectangle(1, 2, 3, 4))
- map_settings.setDestinationCrs(QgsCoordinateReferenceSystem("EPSG:4326"))
- map_settings.setOutputSize(QSize(400, 300))
- controller = AnimationController.create_moving_extent_controller(
- map_settings=map_settings,
- output_mode=None, # Will use map canvas dimensions
- mode=MapMode.PLANAR,
- feature_layer=vl,
- travel_duration=2,
- hover_duration=1,
- min_scale=2000000,
- max_scale=1000000,
- pan_easing=QEasingCurve(QEasingCurve.Type.Linear),
- zoom_easing=QEasingCurve(QEasingCurve.Type.Linear),
- frame_rate=2,
- loop=True,
- )
-
- it = controller.create_jobs()
-
- job = next(it)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 0)
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 1, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 2, 2)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 959999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("hover_feature_id"), 1
- )
- # make sure previous_feature is set to wrap around back to start
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature_id"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature_id"), 2
- )
-
- job = next(it)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 1, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 2, 2)
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 1)
-
- # now we start panning
- job = next(it)
- # Changed from 44
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 1, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 2, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 2)
-
- job = next(it)
- self.assertAlmostEqual(job.map_settings.scale(), 1599999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 4, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 8, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 3)
-
- job = next(it)
- self.assertAlmostEqual(job.map_settings.scale(), 1600000, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 7, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 14, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 4)
-
- job = next(it)
- # Changed from 44
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 10, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 20, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 5)
-
- # back to hovering
-
- job = next(it)
- # Changed from 44
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 10, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 20, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 6)
-
- job = next(it)
- # Was 44444
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 10, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 20, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 7)
-
- # make sure next_feature is set to wrap around back to start
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("previous_feature_id"), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("next_feature_id"), 1
- )
-
- # travel from last to first
- job = next(it)
- # was 15999994
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 10, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 20, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 8)
- # Was 10
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 959999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 8
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature_id"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature_id"), 1
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_hover_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_travel_frame"), 0
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("hover_frames"))
- self.assertEqual(
- job.map_settings.expressionContext().variable("travel_frames"), 4
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 12
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Travelling",
- )
-
- job = next(it)
- # was 16
- self.assertAlmostEqual(job.map_settings.scale(), 1599999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 7, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 14, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 9)
- # was 16
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 1599999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 9
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature_id"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature_id"), 1
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_hover_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_travel_frame"), 1
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("hover_frames"))
- self.assertEqual(
- job.map_settings.expressionContext().variable("travel_frames"), 4
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 12
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Travelling",
- )
-
- job = next(it)
- # was 16
- self.assertAlmostEqual(job.map_settings.scale(), 1599999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 4, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 8, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 10)
- # was 16
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 1599999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 10
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature_id"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature_id"), 1
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_hover_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_travel_frame"), 2
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("hover_frames"))
- self.assertEqual(
- job.map_settings.expressionContext().variable("travel_frames"), 4
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 12
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Travelling",
- )
-
- job = next(it)
- self.assertAlmostEqual(job.map_settings.scale(), 959999, delta=120000)
- self.assertAlmostEqual(job.map_settings.extent().center().x(), 1, 2)
- self.assertAlmostEqual(job.map_settings.extent().center().y(), 2, 2)
- self.assertEqual(job.map_settings.frameRate(), 2)
- self.assertEqual(job.map_settings.currentFrame(), 11)
- self.assertAlmostEqual(
- job.map_settings.expressionContext().variable("map_scale"),
- 959999,
- delta=120000,
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("frame_number"), 11
- )
- self.assertEqual(job.map_settings.expressionContext().variable("frame_rate"), 2)
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature")
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("hover_feature_id")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature").id(), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("from_feature_id"), 2
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature").id(), 1
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("to_feature_id"), 1
- )
- self.assertIsNone(
- job.map_settings.expressionContext().variable("current_hover_frame")
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_travel_frame"), 3
- )
- self.assertIsNone(job.map_settings.expressionContext().variable("hover_frames"))
- self.assertEqual(
- job.map_settings.expressionContext().variable("travel_frames"), 4
- )
- self.assertEqual(job.map_settings.expressionContext().feature().id(), 2)
- self.assertEqual(
- job.map_settings.expressionContext().variable("total_frame_count"), 12
- )
- self.assertEqual(
- job.map_settings.expressionContext().variable("current_animation_action"),
- "Travelling",
- )
-
- with self.assertRaises(StopIteration):
- next(it)
-
-
-if __name__ == "__main__":
- suite = unittest.makeSuite(AnimationControllerTest)
- runner = unittest.TextTestRunner(verbosity=2)
- runner.run(suite)
diff --git a/geest/test-old/test_init.py b/geest/test-old/test_init.py
deleted file mode 100644
index 358e0568..00000000
--- a/geest/test-old/test_init.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# coding=utf-8
-"""Tests QGIS plugin init.
-
-.. note:: This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-"""
-
-__author__ = "Nyall Dawson "
-__revision__ = "$Format:%H$"
-__date__ = "20/04/2018"
-__license__ = "GPL"
-__copyright__ = "Copyright 2018, LINZ"
-
-
-import os
-import unittest
-import logging
-import configparser
-
-
-LOGGER = logging.getLogger("QGIS")
-
-
-class TestInit(unittest.TestCase):
- """Test that the plugin init is usable for QGIS.
-
- Based heavily on the validator class by Alessandro
- Passoti available here:
-
- http://github.com/qgis/qgis-django/blob/master/qgis-app/
- plugins/validator.py
-
- """
-
- def test_read_init(self):
- """Test that the plugin __init__ will validate on plugins.qgis.org."""
-
- # You should update this list according to the latest in
- # https://github.com/qgis/qgis-django/blob/master/qgis-app/
- # plugins/validator.py
-
- required_metadata = [
- "name",
- "description",
- "version",
- "qgisMinimumVersion",
- "email",
- "author",
- ]
-
- file_path = os.path.abspath(
- os.path.join(os.path.dirname(__file__), os.pardir, "metadata.txt")
- )
- LOGGER.info(file_path)
- metadata = []
- parser = configparser.ConfigParser()
- parser.optionxform = str
- parser.read(file_path)
- message = 'Cannot find a section named "general" in %s' % file_path
- assert parser.has_section("general"), message
- metadata.extend(parser.items("general"))
-
- for expectation in required_metadata:
- message = 'Cannot find metadata "%s" in metadata source (%s).' % (
- expectation,
- file_path,
- )
-
- self.assertIn(expectation, dict(metadata), message)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/geest/test-old/test_movie_creator.py b/geest/test-old/test_movie_creator.py
deleted file mode 100644
index f9894dda..00000000
--- a/geest/test-old/test_movie_creator.py
+++ /dev/null
@@ -1,217 +0,0 @@
-# coding=utf-8
-"""Movie creator test.
-
-.. note:: This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
-"""
-
-__author__ = "(C) 2018 by Nyall Dawson"
-__date__ = "20/04/2018"
-__copyright__ = "Copyright 2018, North Road"
-# This will get replaced with a git SHA1 when you do a git archive
-__revision__ = "$Format:%H$"
-
-import unittest
-
-from animation_workbench.core import MovieCommandGenerator, MovieFormat
-from .utilities import get_qgis_app
-
-QGIS_APP = get_qgis_app()
-
-
-class MovieCreatorTest(unittest.TestCase):
- """Test MovieCommandGenerator works."""
-
- # pylint: disable=too-many-statements
-
- def test_mp4(self):
- """
- Test mp4 command generation
- """
- generator = MovieCommandGenerator(
- output_file="/home/me/videos/test.mp4",
- output_mode="1920:1080",
- intro_command=None,
- outro_command=None,
- music_command=None,
- output_format=MovieFormat.MP4,
- work_directory="/tmp/movies",
- frame_filename_prefix="frames",
- framerate=90,
- temp_dir="/tmp",
- )
-
- commands = generator.as_commands()
- self.assertEqual(
- commands,
- [
- (
- "/usr/bin/ffmpeg",
- [
- "-hide_banner",
- "-y",
- "-framerate",
- "90",
- "-i",
- "/tmp/movies/frames-%010d.png",
- "-vf",
- "pad=ceil(iw/2)*2:ceil(ih/2)*2:color=white",
- "-c:v",
- "libx264",
- "-pix_fmt",
- "yuv420p",
- "/tmp/main.mp4",
- ],
- ),
- (
- "/usr/bin/ffmpeg",
- [
- "-y",
- "-f",
- "concat",
- "-safe",
- "0",
- "-i",
- "/tmp/list.txt",
- "-c",
- "copy",
- "-vf",
- "pad=ceil(iw/2)*2:ceil(ih/2)*2:color=white,scale=1920:1080,setsar=1:1",
- "-c:v",
- "libx264",
- "-pix_fmt",
- "yuv420p",
- "/home/me/videos/test.mp4",
- ],
- ),
- ],
- )
-
- def test_mp4_with_music(self):
- """
- Test mp4 command generation
- """
- self.maxDiff = None
- generator = MovieCommandGenerator(
- output_file="/home/me/videos/test.mp4",
- output_mode="1920:1080",
- intro_command=None,
- outro_command=None,
- music_command=None,
- output_format=MovieFormat.MP4,
- work_directory="/tmp/movies",
- frame_filename_prefix="frames",
- framerate=90,
- temp_dir="/tmp",
- )
-
- commands = generator.as_commands()
- self.assertEqual(
- commands,
- [
- (
- "/usr/bin/ffmpeg",
- [
- "-hide_banner",
- "-y",
- "-framerate",
- "90",
- "-i",
- "/tmp/movies/frames-%010d.png",
- "-vf",
- "pad=ceil(iw/2)*2:ceil(ih/2)*2:color=white",
- "-c:v",
- "libx264",
- "-pix_fmt",
- "yuv420p",
- "/tmp/main.mp4",
- ],
- ),
- (
- "/usr/bin/ffmpeg",
- [
- "-y",
- "-f",
- "concat",
- "-safe",
- "0",
- "-i",
- "/tmp/list.txt",
- "-c",
- "copy",
- "-vf",
- "pad=ceil(iw/2)*2:ceil(ih/2)*2:color=white,"
- "scale=1920:1080,setsar=1:1",
- "-c:v",
- "libx264",
- "-pix_fmt",
- "yuv420p",
- "/home/me/videos/test.mp4",
- ],
- ),
- ],
- )
-
- def test_gif(self):
- """
- Test gif command generation
- """
- generator = MovieCommandGenerator(
- output_file="/home/me/videos/test.gif",
- output_mode="720p",
- intro_command=None,
- outro_command=None,
- music_command=None,
- output_format=MovieFormat.GIF,
- work_directory="/tmp/movies",
- frame_filename_prefix="frames",
- framerate=90,
- temp_dir="/tmp/",
- )
-
- commands = generator.as_commands()
- self.assertEqual(
- commands,
- [
- (
- "/usr/bin/convert",
- [
- "-delay",
- "1.1111111111111112",
- "-loop",
- "0",
- "/tmp/movies/frames-*.png",
- "/home/me/videos/test.gif",
- ],
- ),
- (
- "/usr/bin/convert",
- [
- "/home/me/videos/test.gif",
- "-coalesce",
- "-scale",
- "600x600",
- "-fuzz",
- "2%",
- "+dither",
- "-remap",
- "/home/me/videos/test.gif[20]",
- "+dither",
- "-colors",
- "14",
- "-layers",
- "Optimize",
- "/tmp/movies/animation_small.gif",
- ],
- ),
- ],
- )
-
-
-if __name__ == "__main__":
- suite = unittest.makeSuite(MovieCreatorTest)
- runner = unittest.TextTestRunner(verbosity=2)
- runner.run(suite)
diff --git a/geest/test-old/test_qgis_environment.py b/geest/test-old/test_qgis_environment.py
deleted file mode 100644
index c35308b0..00000000
--- a/geest/test-old/test_qgis_environment.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# coding=utf-8
-"""Tests for QGIS functionality.
-
-.. note:: This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
-"""
-__author__ = "tim@linfiniti.com"
-__date__ = "20/01/2011"
-__copyright__ = "(C) 2012, Australia Indonesia Facility for Disaster Reduction"
-
-import unittest
-from qgis.core import QgsProviderRegistry
-from .utilities import get_qgis_app
-
-
-QGIS_APP = get_qgis_app()
-
-
-class QGISTest(unittest.TestCase):
- """Test the QGIS Environment"""
-
- def test_qgis_environment(self):
- """QGIS environment has the expected providers"""
-
- r = QgsProviderRegistry.instance()
- self.assertIn("gdal", r.providerList())
- self.assertIn("ogr", r.providerList())
- self.assertIn("postgres", r.providerList())
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/geest/test-old/test_translations.py b/geest/test-old/test_translations.py
deleted file mode 100644
index 9d8f5d95..00000000
--- a/geest/test-old/test_translations.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# coding=utf-8
-"""Translations Test.
-
-.. note:: This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
-"""
-
-__author__ = "ismailsunni@yahoo.co.id"
-__date__ = "12/10/2011"
-__copyright__ = "(C) 2012, Australia Indonesia Facility for Disaster Reduction"
-
-import unittest
-import os
-from qgis.PyQt.QtCore import QCoreApplication, QTranslator
-from .utilities import get_qgis_app
-
-QGIS_APP = get_qgis_app()
-
-
-class SafeTranslationsTest(unittest.TestCase):
- """Test translations work."""
-
- def setUp(self):
- """Runs before each test."""
- if "LANG" in os.environ:
- del os.environ["LANG"]
-
- def tearDown(self):
- """Runs after each test."""
- if "LANG" in os.environ:
- del os.environ["LANG"]
-
- def test_qgis_translations(self):
- """Test that translations work."""
- parent_path = os.path.join(__file__, os.path.pardir, os.path.pardir)
- dir_path = os.path.abspath(parent_path)
- file_path = os.path.join(dir_path, "i18n", "af.qm")
- self.assertTrue(
- os.path.isfile(file_path),
- "%s is not a valid translation file or it does not exist" % file_path,
- )
- translator = QTranslator()
- translator.load(file_path)
- QCoreApplication.installTranslator(translator)
-
- expected_message = "Goeie more"
- real_message = QCoreApplication.translate("@default", "Good morning")
- self.assertEqual(real_message, expected_message)
-
-
-if __name__ == "__main__":
- suite = unittest.makeSuite(SafeTranslationsTest)
- runner = unittest.TextTestRunner(verbosity=2)
- runner.run(suite)
diff --git a/geest/test-old/utilities.py b/geest/test-old/utilities.py
deleted file mode 100644
index 0903bb25..00000000
--- a/geest/test-old/utilities.py
+++ /dev/null
@@ -1,106 +0,0 @@
-# coding=utf-8
-"""Common functionality used by regression tests."""
-
-import sys
-import logging
-import os
-import atexit
-
-from qgis.core import QgsApplication
-from qgis.utils import iface
-from qgis.gui import QgsMapCanvas
-from qgis.PyQt.QtCore import QSize
-from qgis.PyQt.QtWidgets import QWidget
-
-from .qgis_interface import QgisInterface
-
-LOGGER = logging.getLogger("QGIS")
-QGIS_APP = None # Static variable used to hold hand to running QGIS app
-CANVAS = None
-PARENT = None
-IFACE = None
-
-
-def get_qgis_app(cleanup=True):
- """Start one QGIS application to test against.
-
- :returns: Handle to QGIS app, canvas, iface and parent. If there are any
- errors the tuple members will be returned as None.
- :rtype: (QgsApplication, CANVAS, IFACE, PARENT)
-
- If QGIS is already running the handle to that app will be returned.
- """
-
- global QGIS_APP, PARENT, IFACE, CANVAS # pylint: disable=W0603
-
- if iface:
- QGIS_APP = QgsApplication
- CANVAS = iface.mapCanvas()
- PARENT = iface.mainWindow()
- IFACE = iface
- return QGIS_APP, CANVAS, IFACE, PARENT
-
- global QGISAPP # pylint: disable=global-variable-undefined
-
- try:
- QGISAPP # pylint: disable=used-before-assignment
- except NameError:
- myGuiFlag = True # All test will run qgis in gui mode
-
- # In python3 we need to convert to a bytes object (or should
- # QgsApplication accept a QString instead of const char* ?)
- try:
- argvb = list(map(os.fsencode, sys.argv))
- except AttributeError:
- argvb = sys.argv
-
- # Note: QGIS_PREFIX_PATH is evaluated in QgsApplication -
- # no need to mess with it here.
- QGISAPP = QgsApplication(argvb, myGuiFlag)
-
- QGISAPP.initQgis()
- s = QGISAPP.showSettings()
- LOGGER.debug(s)
-
- def debug_log_message(message, tag, level):
- """
- Prints a debug message to a log
- :param message: message to print
- :param tag: log tag
- :param level: log message level (severity)
- :return:
- """
- print("{}({}): {}".format(tag, level, message))
-
- QgsApplication.instance().messageLog().messageReceived.connect(
- debug_log_message
- )
-
- if cleanup:
-
- @atexit.register
- def exitQgis(): # pylint: disable=unused-variable
- """
- Gracefully closes the QgsApplication instance
- """
- try:
- QGISAPP.exitQgis() # pylint: disable=used-before-assignment
- QGISAPP = None # pylint: disable=redefined-outer-name
- except NameError:
- pass
-
- if PARENT is None:
- # noinspection PyPep8Naming
- PARENT = QWidget()
-
- if CANVAS is None:
- # noinspection PyPep8Naming
- CANVAS = QgsMapCanvas(PARENT)
- CANVAS.resize(QSize(400, 400))
-
- if IFACE is None:
- # QgisInterface is a stub implementation of the QGIS plugin interface
- # noinspection PyPep8Naming
- IFACE = QgisInterface(CANVAS)
-
- return QGISAPP, CANVAS, IFACE, PARENT
diff --git a/shell.nix b/shell.nix
index 6443e98e..1f8a46fa 100644
--- a/shell.nix
+++ b/shell.nix
@@ -21,6 +21,9 @@ in pkgs.mkShell rec {
python3Packages.pandas
python3Packages.odfpy
python3Packages.psutil
+ python3Packages.httpx
+ python3Packages.toml
+ python3Packages.typer
# This executes some shell code to initialize a venv in $venvDir before
# dropping into the shell
From 4a573817db6725f83a78e6dd7746b9c4e32780ba Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:33:49 +0200
Subject: [PATCH 33/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index f6eba218..75711fa7 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -193,7 +193,7 @@ The successful completion of the process is indicated by the green checkmark wid
| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
|----------|----------------------|---------------------------|---------------------------------|---------------------|-----------------------------|------------------------------|
-| **FCV** | battles and explosions | explosions and remote violence | violence against civilians | N/A | protests and riots | No overlap with any event |
+| **FCV** | battles and explosions | explosions and remote violence | violence against civilians | N/A | protests and riots | no overlap with any event |
**Process FCV factor**
From e5618c6ba68081f35617ae590f139e40cc8a2019 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:36:26 +0200
Subject: [PATCH 34/56] Update placecharacterization.md
---
docs/userguide/placecharacterization.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/userguide/placecharacterization.md b/docs/userguide/placecharacterization.md
index 75711fa7..607dff74 100644
--- a/docs/userguide/placecharacterization.md
+++ b/docs/userguide/placecharacterization.md
@@ -193,7 +193,7 @@ The successful completion of the process is indicated by the green checkmark wid
| Factor | Score 0 | Score 1 | Score 2 | Score 3 | Score 4 | Score 5 |
|----------|----------------------|---------------------------|---------------------------------|---------------------|-----------------------------|------------------------------|
-| **FCV** | battles and explosions | explosions and remote violence | violence against civilians | N/A | protests and riots | no overlap with any event |
+| **FCV** | battles and explosions | explosions and remote violence | violence against civilians | not applicable | protests and riots | no overlap with any event |
**Process FCV factor**
From 7e4366607dcd25fb3b14b049da59d5f6b3f14860 Mon Sep 17 00:00:00 2001
From: dragosgontariu <161034767+dragosgontariu@users.noreply.github.com>
Date: Thu, 6 Feb 2025 14:53:25 +0200
Subject: [PATCH 35/56] Update datacollection.md
---
docs/userguide/datacollection.md | 244 +++++++++++++++++++++++++++++++
1 file changed, 244 insertions(+)
diff --git a/docs/userguide/datacollection.md b/docs/userguide/datacollection.md
index e4e658bf..8f21db31 100644
--- a/docs/userguide/datacollection.md
+++ b/docs/userguide/datacollection.md
@@ -4,6 +4,250 @@ This page provides guidance on finding and collecting relevant data for the GEES
## Data Sources for Saint Lucia
+**NEW:**
+
+
+
+ Humdata
+
+ or
+ [out:xml][timeout:25];{{geocodeArea:**country name**}}->.area_0;(node["amenity"="kindergarten"](area.area_0);way["amenity"="kindergarten"](area.area_0);relation["amenity"="kindergarten"](area.area_0););(._;>;);out body; in
+
+ OSM
+
+
+
+
+
🏫Location of primary schools
+
+
+ Humdata
+
+ or
+ [out:xml][timeout:25];{{geocodeArea:**country name**}}->.area_0;(node["amenity"="school"](area.area_0);way["amenity"="school"](area.area_0);relation["amenity"="school"](area.area_0););(._;>;);out body; in
+
+ OSM
+
+
+
+
+
🛒Location of groceries
+
+ [out:xml][timeout:25];{{geocodeArea:**country name**}}->.area_0;(node["shop"="greengrocer"](area.area_0);way["shop"="greengrocer"](area.area_0);relation["shop"="greengrocer"](area.area_0););(._;>;);out body; in
+
+ OSM
+
+
+
+
+
💊Location of pharmacies
+
+ [out:xml][timeout:25];{{geocodeArea:**country name**}}->.area_0;(node["amenity"="pharmacy"](area.area_0);way["amenity"="pharmacy"](area.area_0);relation["amenity"="pharmacy"](area.area_0););(._;>;);out body;
+
+ OSM
+
+
+
+
+
🌳Location of green spaces
+
+ [out:xml][timeout:25];{{geocodeArea:**country name**}}->.area_0;(node["leisure"="park"](area.area_0);node["boundary"="national_park"](area.area_0);way["leisure"="park"](area.area_0);way["boundary"="national_park"](area.area_0);relation["leisure"="park"](area.area_0);relation["boundary"="national_park"](area.area_0););(._;>;);out body; in
+
+ OSM
+
+
+
+
+
🚌Access to Public Transport
+
Location of public transportation stops, including maritime
+
+ [out:xml][timeout:25];{{geocodeArea:**country name**}}->.area_0;(node["public_transport"="stop_position"](area.area_0);node["public_transport"="platform"](area.area_0);node["public_transport"="station"](area.area_0);node["public_transport"="stop_area"](area.area_0);node["highway"="bus_stop"](area.area_0);node["highway"="platform"](area.area_0);way["public_transport"="stop_position"](area.area_0);way["public_transport"="platform"](area.area_0);way["public_transport"="station"](area.area_0);way["public_transport"="stop_area"](area.area_0);way["highway"="bus_stop"](area.area_0);way["highway"="platform"](area.area_0);relation["public_transport"="stop_position"](area.area_0);relation["public_transport"="platform"](area.area_0);relation["public_transport"="station"](area.area_0);relation["public_transport"="stop_area"](area.area_0);relation["highway"="bus_stop"](area.area_0);relation["highway"="platform"](area.area_0);node["amenity"="ferry_terminal"](area.area_0);way["amenity"="ferry_terminal"](area.area_0);relation["amenity"="ferry_terminal"](area.area_0););(._;>;);out body; in
+
+ OSM
+
+
From 3c0d9cfe7c556f4e88a3e3baa790f93630079068 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Fri, 7 Feb 2025 00:16:38 +0000
Subject: [PATCH 36/56] WIP implementation for grid chunker in its own class
---
geest/core/tasks/grid_chunker.py | 148 +++++++++++++++++++++++++++++++
geest/gui/geest_settings.py | 4 +-
geest/gui/panels/tree_panel.py | 2 +-
test/test_grid_chunker.py | 47 ++++++++++
4 files changed, 198 insertions(+), 3 deletions(-)
create mode 100644 geest/core/tasks/grid_chunker.py
create mode 100644 test/test_grid_chunker.py
diff --git a/geest/core/tasks/grid_chunker.py b/geest/core/tasks/grid_chunker.py
new file mode 100644
index 00000000..38aa370a
--- /dev/null
+++ b/geest/core/tasks/grid_chunker.py
@@ -0,0 +1,148 @@
+from osgeo import ogr, osr
+from geest.utilities import log_message
+
+
+class GridChunker:
+ """
+ A class to divide a grid into chunks and process each chunk.
+
+ Attributes:
+ xmin (float): Minimum x-coordinate of the grid.
+ xmax (float): Maximum x-coordinate of the grid.
+ ymin (float): Minimum y-coordinate of the grid.
+ ymax (float): Maximum y-coordinate of the grid.
+ cell_size (float): Size of each cell in the grid.
+ chunk_size (int): Number of cells in each chunk.
+
+ Methods:
+ log_message(message):
+ Logs a message to the console.
+
+ chunks():
+ Yields chunks of the grid with their bounding box coordinates.
+
+ total_cells_in_chunk():
+ Returns the total number of cells in a chunk.
+
+ Example:
+ grid_chunker = GridChunker(0, 100, 0, 100, 10, 5)
+ for chunk in grid_chunker.chunks():
+ print(chunk)
+
+ total_cells = grid_chunker.total_cells_in_chunk()
+ print(f"Total cells in each chunk: {total_cells}")
+ """
+
+ def __init__(self, xmin, xmax, ymin, ymax, cell_size, chunk_size):
+ """
+ Initializes the GridChunker with the given grid boundaries, cell size, and chunk size.
+
+ Args:
+ xmin (float): Minimum x-coordinate of the grid.
+ xmax (float): Maximum x-coordinate of the grid.
+ ymin (float): Minimum y-coordinate of the grid.
+ ymax (float): Maximum y-coordinate of the grid.
+ cell_size (float): Size of each cell in the grid.
+ chunk_size (int): Number of cells in each chunk.
+ """
+ self.xmin = xmin
+ self.xmax = xmax
+ self.ymin = ymin
+ self.ymax = ymax
+ self.cell_size = cell_size
+ self.chunk_size = chunk_size
+
+ self.x_range_count = int((xmax - xmin) / cell_size)
+ self.y_range_count = int((ymax - ymin) / cell_size)
+
+ def write_chunks_to_gpkg(self, gpkg_path):
+ """
+ Writes the chunk polygon boundaries to a GeoPackage using the GDAL OGR API.
+
+ Args:
+ gpkg_path (str): The file path to the GeoPackage.
+ """
+
+ # Create the data source
+ driver = ogr.GetDriverByName("GPKG")
+ data_source = driver.CreateDataSource(gpkg_path)
+
+ # Create the spatial reference, WGS84
+ srs = osr.SpatialReference()
+ srs.ImportFromEPSG(4326)
+
+ # Create the layer
+ layer = data_source.CreateLayer("chunks", srs, ogr.wkbPolygon)
+
+ # Add fields
+ field_index = ogr.FieldDefn("index", ogr.OFTInteger)
+ layer.CreateField(field_index)
+
+ # Create the feature and set values
+ for chunk in self.chunks():
+ feature = ogr.Feature(layer.GetLayerDefn())
+ feature.SetField("index", chunk["index"])
+
+ # Create polygon from bounding box coordinates
+ ring = ogr.Geometry(ogr.wkbLinearRing)
+ ring.AddPoint(chunk["x_start"], chunk["y_start"])
+ ring.AddPoint(chunk["x_end"], chunk["y_start"])
+ ring.AddPoint(chunk["x_end"], chunk["y_end"])
+ ring.AddPoint(chunk["x_start"], chunk["y_end"])
+ ring.AddPoint(chunk["x_start"], chunk["y_start"])
+
+ polygon = ogr.Geometry(ogr.wkbPolygon)
+ polygon.AddGeometry(ring)
+
+ feature.SetGeometry(polygon)
+ layer.CreateFeature(feature)
+ feature = None
+
+ # Close the data source
+ data_source = None
+
+ def chunks(self):
+ """
+ Yields chunks of the grid with their bounding box coordinates.
+
+ Yields:
+ dict: A dictionary containing the index and bounding box coordinates of each chunk.
+ """
+ x_blocks = range(0, self.x_range_count, self.chunk_size)
+ y_blocks = range(0, self.y_range_count, self.chunk_size)
+ index = 0
+
+ for x_block_start in x_blocks:
+ log_message(f"Processing chunk {x_block_start} of {self.x_range_count}")
+ x_block_end = min(x_block_start + self.chunk_size, self.x_range_count)
+
+ x_start_coord = self.xmin + x_block_start * self.cell_size
+ x_end_coord = self.xmin + x_block_end * self.cell_size
+
+ for y_block_start in y_blocks:
+ log_message(f"Processing chunk {y_block_start} of {self.y_range_count}")
+ y_block_end = min(y_block_start + self.chunk_size, self.y_range_count)
+
+ y_start_coord = self.ymin + y_block_start * self.cell_size
+ y_end_coord = self.ymin + y_block_end * self.cell_size
+
+ log_message(
+ f"Created Chunk bbox: {x_start_coord}, {x_end_coord}, {y_start_coord}, {y_end_coord}"
+ )
+ yield {
+ "index": index,
+ "x_start": x_start_coord,
+ "x_end": x_end_coord,
+ "y_start": y_start_coord,
+ "y_end": y_end_coord,
+ }
+ index += 1
+
+ def total_cells_in_chunk(self):
+ """
+ Returns the total number of cells in a chunk.
+
+ Returns:
+ int: The total number of cells in a chunk.
+ """
+ return self.chunk_size * self.chunk_size
diff --git a/geest/gui/geest_settings.py b/geest/gui/geest_settings.py
index 42bb540c..59cef3c0 100644
--- a/geest/gui/geest_settings.py
+++ b/geest/gui/geest_settings.py
@@ -34,7 +34,7 @@ def __init__(self, parent=None):
# of CPU cores you have would be a good conservative approach
# You could probably run 100 or more on a decently specced machine
self.spin_thread_pool_size.setValue(
- int(setting(key="render_thread_pool_size", default=1))
+ int(setting(key="concurrent_tasks", default=1))
)
# This is intended for developers to attach to the plugin using a
@@ -77,7 +77,7 @@ def apply(self):
.. note:: This is called on OK click.
"""
set_setting(
- key="render_thread_pool_size",
+ key="concurrent_tasks",
value=self.spin_thread_pool_size.value(),
)
diff --git a/geest/gui/panels/tree_panel.py b/geest/gui/panels/tree_panel.py
index 26133a73..d6ffa10e 100644
--- a/geest/gui/panels/tree_panel.py
+++ b/geest/gui/panels/tree_panel.py
@@ -74,7 +74,7 @@ def __init__(self, parent=None, json_file=None):
# Initialize the QueueManager
self.working_directory = None
- pool_size = int(setting(key="render_thread_pool_size", default=1))
+ pool_size = int(setting(key="concurrent_tasks", default=1))
self.queue_manager = WorkflowQueueManager(pool_size=pool_size)
self.json_file = json_file
self.tree_view_visible = True
diff --git a/test/test_grid_chunker.py b/test/test_grid_chunker.py
new file mode 100644
index 00000000..88345e0e
--- /dev/null
+++ b/test/test_grid_chunker.py
@@ -0,0 +1,47 @@
+import unittest
+from geest.core.tasks.grid_chunker import GridChunker
+from osgeo import ogr, osr
+import os
+
+
+class TestGridChunker(unittest.TestCase):
+
+ def setUp(self):
+ self.grid_chunker = GridChunker(0, 100, 0, 100, 10, 5)
+
+ def test_chunks(self):
+ chunks = list(self.grid_chunker.chunks())
+ self.assertEqual(len(chunks), 4)
+ expected_chunks = [
+ {"index": 0, "x_start": 0, "x_end": 50, "y_start": 0, "y_end": 50},
+ {"index": 1, "x_start": 0, "x_end": 50, "y_start": 50, "y_end": 100},
+ {"index": 2, "x_start": 50, "x_end": 100, "y_start": 0, "y_end": 50},
+ {"index": 3, "x_start": 50, "x_end": 100, "y_start": 50, "y_end": 100},
+ ]
+ for chunk, expected_chunk in zip(chunks, expected_chunks):
+ self.assertEqual(chunk, expected_chunk)
+
+ def test_total_cells_in_chunk(self):
+ self.assertEqual(self.grid_chunker.total_cells_in_chunk(), 25)
+
+ def test_write_chunks_to_gpkg(self):
+ gpkg_path = "test_chunks.gpkg"
+ self.grid_chunker.write_chunks_to_gpkg(gpkg_path)
+ self.assertTrue(os.path.exists(gpkg_path))
+
+ driver = ogr.GetDriverByName("GPKG")
+ data_source = driver.Open(gpkg_path, 0)
+ self.assertIsNotNone(data_source)
+
+ layer = data_source.GetLayer()
+ self.assertEqual(layer.GetFeatureCount(), 4)
+
+ for feature in layer:
+ self.assertIn(feature.GetField("index"), [0, 1, 2, 3])
+
+ data_source = None
+ os.remove(gpkg_path)
+
+
+if __name__ == "__main__":
+ unittest.main()
From 0e0643ee074e2197ba42104ee73e6340cdf65417 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Fri, 7 Feb 2025 13:46:13 +0000
Subject: [PATCH 37/56] Update tests for chunker. Add geom checks
---
geest/core/tasks/grid_chunker.py | 76 +++++++++++++++++++++++++--
test/test_grid_chunker.py | 89 ++++++++++++++++++++++++++++++++
2 files changed, 160 insertions(+), 5 deletions(-)
diff --git a/geest/core/tasks/grid_chunker.py b/geest/core/tasks/grid_chunker.py
index 38aa370a..907a3d7e 100644
--- a/geest/core/tasks/grid_chunker.py
+++ b/geest/core/tasks/grid_chunker.py
@@ -4,7 +4,9 @@
class GridChunker:
"""
- A class to divide a grid into chunks and process each chunk.
+ A class to divide a bbox into chunks and process each chunk.
+
+
Attributes:
xmin (float): Minimum x-coordinate of the grid.
@@ -33,7 +35,15 @@ class GridChunker:
print(f"Total cells in each chunk: {total_cells}")
"""
- def __init__(self, xmin, xmax, ymin, ymax, cell_size, chunk_size):
+ def __init__(
+ self,
+ xmin: float,
+ xmax: float,
+ ymin: float,
+ ymax: float,
+ cell_size: float,
+ chunk_size: int,
+ ):
"""
Initializes the GridChunker with the given grid boundaries, cell size, and chunk size.
@@ -49,16 +59,57 @@ def __init__(self, xmin, xmax, ymin, ymax, cell_size, chunk_size):
self.xmax = xmax
self.ymin = ymin
self.ymax = ymax
- self.cell_size = cell_size
- self.chunk_size = chunk_size
+ self.cell_size = cell_size # size in map units of each cell (typically meters)
+ self.chunk_size = (
+ chunk_size # number of cells in each chunk in both x and y directions
+ )
+ # e.g. chunk size of 5 would mean 5x5 cells in each chunk
self.x_range_count = int((xmax - xmin) / cell_size)
self.y_range_count = int((ymax - ymin) / cell_size)
+ self.geometry = None
+
+ def set_geometry(self, wkb_geometry):
+ """
+ Sets the geometry for the grid chunker.
+
+ Args:
+ wkb_geometry (bytes): The geometry in WKB format.
+ """
+ if wkb_geometry is None:
+ self.geometry = None
+ return
+
+ self.geometry = ogr.CreateGeometryFromWkb(wkb_geometry)
+
+ # If the geometry is a 3d geometry, convert it to 2d
+ if self.geometry.GetCoordinateDimension() == 3:
+ self.geometry.FlattenTo2D()
+
+ # Check the geom is a single part and if not, raise an error
+ if self.geometry.GetGeometryCount() > 1:
+ raise ValueError("The geometry must be a single part.")
+
+ # Check the geom is a polygon and if not, raise an error
+ if self.geometry.GetGeometryType() != ogr.wkbPolygon:
+ # Get the geomtery type name from the geometry type
+ geom_type_name = ogr.GeometryTypeToName(self.geometry.GetGeometryType())
+ raise ValueError(
+ f"The geometry must be a polygon. Received a geometry of type {geom_type_name}"
+ )
+
+ # check the geom is in the same projection as the grid by seeing if they intersect
+ if not self.geometry.Intersects(self.geometry):
+ raise ValueError("The geometry must be in the same projection as the grid.")
def write_chunks_to_gpkg(self, gpkg_path):
"""
Writes the chunk polygon boundaries to a GeoPackage using the GDAL OGR API.
+ If self.geometry is not none, chunks that do not intersect with the geom
+ will be excluded. Additionally, chunks will be labelled as "inside" or "edge"
+ so that the user can easily filter out chunks that are completely inside the geometry.
+
Args:
gpkg_path (str): The file path to the GeoPackage.
"""
@@ -77,6 +128,9 @@ def write_chunks_to_gpkg(self, gpkg_path):
# Add fields
field_index = ogr.FieldDefn("index", ogr.OFTInteger)
layer.CreateField(field_index)
+ # Add a field to label the chunks as "inside" or "edge"
+ field_type = ogr.FieldDefn("type", ogr.OFTString)
+ layer.CreateField(field_type)
# Create the feature and set values
for chunk in self.chunks():
@@ -95,7 +149,19 @@ def write_chunks_to_gpkg(self, gpkg_path):
polygon.AddGeometry(ring)
feature.SetGeometry(polygon)
- layer.CreateFeature(feature)
+
+ # if the geometry is not none and the polygon intersects with it, add it to the layer
+ if self.geometry is not None and self.geometry.Intersects(polygon):
+ layer.CreateFeature(feature)
+ if self.geometry.Contains(polygon):
+ feature.SetField("type", "inside")
+ layer.SetFeature(feature)
+ else:
+ feature.SetField("type", "edge")
+ layer.SetFeature(feature)
+ else:
+ layer.CreateFeature(feature)
+ layer.SetFeature(feature)
feature = None
# Close the data source
diff --git a/test/test_grid_chunker.py b/test/test_grid_chunker.py
index 88345e0e..6fa98b33 100644
--- a/test/test_grid_chunker.py
+++ b/test/test_grid_chunker.py
@@ -9,6 +9,10 @@ class TestGridChunker(unittest.TestCase):
def setUp(self):
self.grid_chunker = GridChunker(0, 100, 0, 100, 10, 5)
+ def tearDown(self):
+ self.grid_chunker = None
+ return super().tearDown()
+
def test_chunks(self):
chunks = list(self.grid_chunker.chunks())
self.assertEqual(len(chunks), 4)
@@ -42,6 +46,91 @@ def test_write_chunks_to_gpkg(self):
data_source = None
os.remove(gpkg_path)
+ def test_set_geometry_valid(self):
+ # Create a simple square polygon in WKB format
+ ring = ogr.Geometry(ogr.wkbLinearRing)
+ ring.AddPoint(0, 0)
+ ring.AddPoint(100, 0)
+ ring.AddPoint(100, 100)
+ ring.AddPoint(0, 100)
+ ring.AddPoint(0, 0)
+ polygon = ogr.Geometry(ogr.wkbPolygon)
+ polygon.AddGeometry(ring)
+ wkb_geometry = polygon.ExportToWkb()
+
+ self.grid_chunker.set_geometry(wkb_geometry)
+ self.assertIsNotNone(self.grid_chunker.geometry)
+ self.assertTrue(self.grid_chunker.geometry.Intersects(polygon))
+
+ def test_set_geometry_invalid_multipolygon(self):
+ # Create a multipolygon in WKB format
+ ring1 = ogr.Geometry(ogr.wkbLinearRing)
+ ring1.AddPoint(0, 0)
+ ring1.AddPoint(50, 0)
+ ring1.AddPoint(50, 50)
+ ring1.AddPoint(0, 50)
+ ring1.AddPoint(0, 0)
+ polygon1 = ogr.Geometry(ogr.wkbPolygon)
+ polygon1.AddGeometry(ring1)
+
+ ring2 = ogr.Geometry(ogr.wkbLinearRing)
+ ring2.AddPoint(60, 60)
+ ring2.AddPoint(100, 60)
+ ring2.AddPoint(100, 100)
+ ring2.AddPoint(60, 100)
+ ring2.AddPoint(60, 60)
+ polygon2 = ogr.Geometry(ogr.wkbPolygon)
+ polygon2.AddGeometry(ring2)
+
+ multipolygon = ogr.Geometry(ogr.wkbMultiPolygon)
+ multipolygon.AddGeometry(polygon1)
+ multipolygon.AddGeometry(polygon2)
+ wkb_geometry = multipolygon.ExportToWkb()
+
+ with self.assertRaises(ValueError):
+ self.grid_chunker.set_geometry(wkb_geometry)
+
+ def test_set_geometry_invalid_non_polygon(self):
+ # Create a point in WKB format
+ point = ogr.Geometry(ogr.wkbPoint)
+ point.AddPoint(0, 0)
+ wkb_geometry = point.ExportToWkb()
+
+ with self.assertRaises(ValueError):
+ self.grid_chunker.set_geometry(wkb_geometry)
+
+ def test_write_chunks_to_gpkg_with_geometry(self):
+ # Create a simple square polygon in WKB format
+ ring = ogr.Geometry(ogr.wkbLinearRing)
+ ring.AddPoint(0, 0)
+ ring.AddPoint(100, 0)
+ ring.AddPoint(100, 100)
+ ring.AddPoint(0, 100)
+ ring.AddPoint(0, 0)
+ polygon = ogr.Geometry(ogr.wkbPolygon)
+ polygon.AddGeometry(ring)
+ wkb_geometry = polygon.ExportToWkb()
+
+ self.grid_chunker.set_geometry(wkb_geometry)
+
+ gpkg_path = "test_chunks_with_geometry.gpkg"
+ self.grid_chunker.write_chunks_to_gpkg(gpkg_path)
+ self.assertTrue(os.path.exists(gpkg_path))
+
+ driver = ogr.GetDriverByName("GPKG")
+ data_source = driver.Open(gpkg_path, 0)
+ self.assertIsNotNone(data_source)
+
+ layer = data_source.GetLayer()
+ self.assertEqual(layer.GetFeatureCount(), 4)
+
+ for feature in layer:
+ self.assertIn(feature.GetField("index"), [0, 1, 2, 3])
+ self.assertIn(feature.GetField("type"), ["inside", "edge"])
+
+ data_source = None
+ os.remove(gpkg_path)
+
if __name__ == "__main__":
unittest.main()
From 017a0a9cd35cbe3824f5cd21636e5d2f8344c959 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Fri, 7 Feb 2025 23:48:31 +0000
Subject: [PATCH 38/56] WIP Study Area prep performance improvements...
---
geest/core/tasks/grid_chunker.py | 74 ++++++++++++++++++++++++--------
geest/core/tasks/study_area.py | 49 ++++++++++++++-------
2 files changed, 89 insertions(+), 34 deletions(-)
diff --git a/geest/core/tasks/grid_chunker.py b/geest/core/tasks/grid_chunker.py
index 907a3d7e..838f401f 100644
--- a/geest/core/tasks/grid_chunker.py
+++ b/geest/core/tasks/grid_chunker.py
@@ -1,3 +1,4 @@
+import os
from osgeo import ogr, osr
from geest.utilities import log_message
@@ -15,6 +16,7 @@ class GridChunker:
ymax (float): Maximum y-coordinate of the grid.
cell_size (float): Size of each cell in the grid.
chunk_size (int): Number of cells in each chunk.
+ epsg (int): The EPSG code of the grid.
Methods:
log_message(message):
@@ -43,6 +45,7 @@ def __init__(
ymax: float,
cell_size: float,
chunk_size: int,
+ epsg: int,
):
"""
Initializes the GridChunker with the given grid boundaries, cell size, and chunk size.
@@ -54,6 +57,7 @@ def __init__(
ymax (float): Maximum y-coordinate of the grid.
cell_size (float): Size of each cell in the grid.
chunk_size (int): Number of cells in each chunk.
+ epsg (int): The EPSG code of the grid.
"""
self.xmin = xmin
self.xmax = xmax
@@ -67,7 +71,9 @@ def __init__(
self.x_range_count = int((xmax - xmin) / cell_size)
self.y_range_count = int((ymax - ymin) / cell_size)
+ self.epsg = epsg
self.geometry = None
+ self.layer_name = "chunks"
def set_geometry(self, wkb_geometry):
"""
@@ -102,36 +108,54 @@ def set_geometry(self, wkb_geometry):
if not self.geometry.Intersects(self.geometry):
raise ValueError("The geometry must be in the same projection as the grid.")
- def write_chunks_to_gpkg(self, gpkg_path):
+ def create_layer_if_not_exists(self, gpkg_path):
"""
- Writes the chunk polygon boundaries to a GeoPackage using the GDAL OGR API.
-
- If self.geometry is not none, chunks that do not intersect with the geom
- will be excluded. Additionally, chunks will be labelled as "inside" or "edge"
- so that the user can easily filter out chunks that are completely inside the geometry.
-
- Args:
- gpkg_path (str): The file path to the GeoPackage.
+ Create a GPKG layer if it does not exist.
"""
- # Create the data source
- driver = ogr.GetDriverByName("GPKG")
- data_source = driver.CreateDataSource(gpkg_path)
+ if not os.path.exists(gpkg_path):
+ # Create new GPKG
+ driver = ogr.GetDriverByName("GPKG")
+ driver.CreateDataSource(self.gpkg_path)
+ data_source = ogr.Open(gpkg_path, 1)
+ layer = data_source.GetLayerByName(self.layer_name)
+ if layer is not None:
+ data_source = None
+ return # Already exists
# Create the spatial reference, WGS84
srs = osr.SpatialReference()
- srs.ImportFromEPSG(4326)
-
- # Create the layer
- layer = data_source.CreateLayer("chunks", srs, ogr.wkbPolygon)
-
+ srs.ImportFromEPSG(self.epsg)
+ # Create it
+ layer = data_source.CreateLayer(self.layer_name, srs, geom_type=ogr.wkbPolygon)
# Add fields
field_index = ogr.FieldDefn("index", ogr.OFTInteger)
layer.CreateField(field_index)
# Add a field to label the chunks as "inside" or "edge"
field_type = ogr.FieldDefn("type", ogr.OFTString)
layer.CreateField(field_type)
+ layer.SyncToDisk()
+
+ data_source = None
+
+ def write_chunks_to_gpkg(self, gpkg_path):
+ """
+ Writes the chunk polygon boundaries to a GeoPackage using the GDAL OGR API.
+
+ If self.geometry is not none, chunks that do not intersect with the geom
+ will be excluded. Additionally, chunks will be labelled as "inside" or "edge"
+ so that the user can easily filter out chunks that are completely inside the geometry.
+ Args:
+ gpkg_path (str): The file path to the GeoPackage.
+ """
+ self.create_layer_if_not_exists(gpkg_path=gpkg_path)
+ data_source = ogr.Open(gpkg_path, 1)
+ layer = data_source.GetLayerByName(self.layer_name)
+ if not layer:
+ raise RuntimeError(
+ f"Could not open target layer {self.layer_name} in {gpkg_path}"
+ )
# Create the feature and set values
for chunk in self.chunks():
feature = ogr.Feature(layer.GetLayerDefn())
@@ -160,8 +184,7 @@ def write_chunks_to_gpkg(self, gpkg_path):
feature.SetField("type", "edge")
layer.SetFeature(feature)
else:
- layer.CreateFeature(feature)
- layer.SetFeature(feature)
+ pass
feature = None
# Close the data source
@@ -212,3 +235,16 @@ def total_cells_in_chunk(self):
int: The total number of cells in a chunk.
"""
return self.chunk_size * self.chunk_size
+
+ def total_chunks(self):
+ """
+ Returns the total number of chunks.
+
+ Returns:
+ int: The total number of chunks.
+ """
+ count = int(self.x_range_count / self.chunk_size) * int(
+ self.y_range_count / self.chunk_size
+ )
+ log_message(f"Total chunks: {count}")
+ return count
diff --git a/geest/core/tasks/study_area.py b/geest/core/tasks/study_area.py
index 1bd93168..232184a4 100644
--- a/geest/core/tasks/study_area.py
+++ b/geest/core/tasks/study_area.py
@@ -17,6 +17,7 @@
)
from geest.utilities import log_message
from .grid_from_bbox import GridFromBbox
+from .grid_chunker import GridChunker
from geest.core import setting
@@ -627,6 +628,20 @@ def create_and_save_grid(self, normalized_name, geom, bbox):
xmin, xmax, ymin, ymax = bbox
cell_size = self.cell_size_m
+ # size is squared so 5 will make a 5x5 cell chunk
+ chunk_size = int(setting(key="chunk_size", default=50))
+
+ chunker = GridChunker(
+ xmin,
+ xmax,
+ ymin,
+ ymax,
+ cell_size,
+ chunk_size=chunk_size,
+ epsg=self.epsg_code,
+ )
+ chunker.set_geometry(geom.ExportToWkb())
+ chunker.write_chunks_to_gpkg(self.gpkg_path)
log_message(
f"Creating grid for extents: xmin {xmin}, xmax {xmax}, ymin {ymin}, ymax {ymax}"
@@ -658,24 +673,25 @@ def create_and_save_grid(self, normalized_name, geom, bbox):
feedback = QgsFeedback()
# 1. Chunk the bounding box
- # size is squared so 5 will make a 5x5 cell chunk
- chunk_size = int(setting(key="chunk_size", default=50))
- bbox_chunks = list(
- self.chunk_bbox(xmin, xmax, ymin, ymax, cell_size, chunk_size)
- )
+
# print out all the chunk bboxes
- log_message(f"Chunk count: {len(bbox_chunks)}")
+ chunk_count = chunker.total_chunks()
+ log_message(f"Chunk count: {chunk_count}")
log_message(f"Chunk size: {chunk_size}")
- chunk_count = len(bbox_chunks)
- for idx, chunk in enumerate(bbox_chunks):
- log_message(f"Chunk {idx}: {chunk}")
self.feedback.setProgress(0)
- for idx, chunk in enumerate(bbox_chunks):
+ for chunk in chunker.chunks():
start_time = (
time.time()
) # used for both create chunk start and total chunk start
- task = GridFromBbox(idx, chunk, geom, cell_size, feedback)
+ index = chunk["index"]
+ task = GridFromBbox(
+ index,
+ (chunk["x_start"], chunk["x_end"], chunk["y_start"], chunk["y_end"]),
+ geom,
+ cell_size,
+ feedback,
+ )
self.track_time("Creating chunks", start_time)
# Not running in thread for now, see note below
task.run()
@@ -683,15 +699,18 @@ def create_and_save_grid(self, normalized_name, geom, bbox):
self.write_chunk(layer, task, normalized_name)
# We use the progress object to notify of progress in the subtask
# And the QgsTask progressChanged signal to track the main task
- current_progress = int(idx + 1 / (chunk_count * chunk_size))
- log_message(f"XXXXXX Chunks Progress: {current_progress}% XXXXXX")
- self.feedback.setProgress(current_progress)
+ try:
+ current_progress = int(index + 1 / (chunk_count * chunk_size))
+ log_message(f"XXXXXX Chunks Progress: {current_progress}% XXXXXX")
+ self.feedback.setProgress(current_progress)
+ except ZeroDivisionError:
+ pass
# This is blocking, but we're in a thread
# Crashes QGIS, needs to be refactored to use QgsTask subtasks
# task.taskCompleted.connect(write_grids)
# worker_tasks.append(task)
- # log_message(f"Task {idx} created for chunk {chunk}")
+ # log_message(f"Task {index} created for chunk {chunk}")
# log_message(f"{len(worker_tasks)} tasks queued.")
# task_manager.addTask(task)
self.track_time("Complete chunk", start_time)
From 3c8a7bb0c112095882ddc1ff84bd043504db9336 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sat, 8 Feb 2025 00:38:41 +0000
Subject: [PATCH 39/56] Use transactions for grid creations - massive speedup
---
geest/core/tasks/study_area.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/geest/core/tasks/study_area.py b/geest/core/tasks/study_area.py
index 232184a4..2696017a 100644
--- a/geest/core/tasks/study_area.py
+++ b/geest/core/tasks/study_area.py
@@ -734,12 +734,13 @@ def write_chunk(self, layer, task, normalized_name):
# If write_lock is true, wait for the lock to be released
while self.write_lock:
log_message("Waiting for write lock...")
- time.sleep(0.1)
+ time.sleep(0.001)
log_message("Write lock released.")
log_message(f"Writing {len(task.features_out)} features to layer.")
self.track_time("Preparing chunks", task.run_time)
self.write_lock = True
feat_defn = layer.GetLayerDefn()
+ layer.StartTransaction()
try:
for geometry in task.features_out:
feature = ogr.Feature(feat_defn)
@@ -751,6 +752,7 @@ def write_chunk(self, layer, task, normalized_name):
self.current_geom_actual_cell_count += 1
if self.current_geom_actual_cell_count % 10000 == 0:
+
try:
log_message(
f" Cell count: {self.current_geom_actual_cell_count}"
@@ -769,10 +771,13 @@ def write_chunk(self, layer, task, normalized_name):
except ZeroDivisionError:
pass
# commit changes
- layer.SyncToDisk()
+ layer.CommitTransaction()
+ layer.StartTransaction()
+ layer.CommitTransaction() # Final commit
self.track_time("Writing chunks", start_time)
self.write_lock = False
except Exception as e:
+ layer.RollbackTransaction() # Rollback on error
log_message(f"write_grids: {str(e)}")
log_message(f"write_grids: {traceback.format_exc()}")
self.write_lock = False
From 72b98552e58be0e57df72bac2678cb42b3549979 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sat, 8 Feb 2025 12:40:24 +0000
Subject: [PATCH 40/56] Count errors during processing
---
geest/core/tasks/study_area.py | 23 ++++++++++++++++++++++-
1 file changed, 22 insertions(+), 1 deletion(-)
diff --git a/geest/core/tasks/study_area.py b/geest/core/tasks/study_area.py
index 2696017a..e61eca8e 100644
--- a/geest/core/tasks/study_area.py
+++ b/geest/core/tasks/study_area.py
@@ -80,6 +80,7 @@ def __init__(
self.valid_feature_count = 0
self.current_geom_actual_cell_count = 0
self.current_geom_cell_count_estimate = 0
+ self.error_count = 0
self.total_cells = 0
self.write_lock = False
# Make sure output directory exists
@@ -278,6 +279,9 @@ def run(self):
log_message(
f"Processing complete. Valid: {self.valid_feature_count}, Fixed: {fixed_feature_count}, Invalid: {invalid_feature_count}"
)
+ log_message(
+ f"Areas that could not be processed due to errors: {self.error_count}"
+ )
log_message(f"Total cells generated: {self.total_cells}")
# 4) Create a VRT of all generated raster masks
@@ -413,6 +417,20 @@ def process_singlepart_geometry(self, geom, normalized_name, area_name):
normalized_name, "timestamp_start", now_str
)
+ # Check we have a single part geom
+ geom_type = ogr.GT_Flatten(geom.GetGeometryType())
+ if geom_type != ogr.wkbPolygon:
+ log_message(
+ f"Skipping non-polygon geometry type {geom_type} for {normalized_name}."
+ )
+ return
+ # check it has only one part
+ if geom.GetGeometryCount() > 1:
+ log_message(
+ f"Skipping multi-part geometry for {normalized_name}.",
+ level="WARNING",
+ )
+ return
# Compute aligned bounding box in target CRS
# (We already have a coordinate transformation if the source has a known SRS)
geometry_bbox = geom.GetEnvelope() # (xmin, xmax, ymin, ymax)
@@ -479,7 +497,10 @@ def process_multipart_geometry(self, geom, normalized_name, area_name):
for i in range(count):
part_geom = geom.GetGeometryRef(i)
part_name = f"{normalized_name}_part{i}"
- self.process_singlepart_geometry(part_geom, part_name, area_name)
+ try:
+ self.process_singlepart_geometry(part_geom, part_name, area_name)
+ except:
+ self.error_count += 1
##########################################################################
# BBox handling
From d1deb8ab2e1489a28b3e0ca82632223f3a60d1f1 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sat, 8 Feb 2025 13:01:52 +0000
Subject: [PATCH 41/56] Initialise gpkg path on grid chunker start
---
geest/core/tasks/grid_chunker.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/geest/core/tasks/grid_chunker.py b/geest/core/tasks/grid_chunker.py
index 838f401f..6ff8caef 100644
--- a/geest/core/tasks/grid_chunker.py
+++ b/geest/core/tasks/grid_chunker.py
@@ -74,6 +74,7 @@ def __init__(
self.epsg = epsg
self.geometry = None
self.layer_name = "chunks"
+ self.gpkg_path = None # Initialize gpkg_path
def set_geometry(self, wkb_geometry):
"""
From 9388b231d7f8d42f1010314ff35ec0f72e02e8e2 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sat, 8 Feb 2025 23:30:39 +0000
Subject: [PATCH 42/56] Fix progress reporting and more grid creation
optimisation
---
geest/core/tasks/grid_chunker.py | 79 ++++++++++++++++++------------
geest/core/tasks/study_area.py | 84 ++++++++++++++------------------
2 files changed, 85 insertions(+), 78 deletions(-)
diff --git a/geest/core/tasks/grid_chunker.py b/geest/core/tasks/grid_chunker.py
index 6ff8caef..bc52d4a7 100644
--- a/geest/core/tasks/grid_chunker.py
+++ b/geest/core/tasks/grid_chunker.py
@@ -7,13 +7,12 @@ class GridChunker:
"""
A class to divide a bbox into chunks and process each chunk.
-
-
Attributes:
xmin (float): Minimum x-coordinate of the grid.
xmax (float): Maximum x-coordinate of the grid.
ymin (float): Minimum y-coordinate of the grid.
ymax (float): Maximum y-coordinate of the grid.
+ geom (ogr.Geometry): The geometry to check for intersections. (e.g. an island or land area)
cell_size (float): Size of each cell in the grid.
chunk_size (int): Number of cells in each chunk.
epsg (int): The EPSG code of the grid.
@@ -28,6 +27,18 @@ class GridChunker:
total_cells_in_chunk():
Returns the total number of cells in a chunk.
+ total_chunks():
+ Returns the total number of chunks.
+
+ set_geometry(wkb_geometry):
+ Sets the geometry for the grid chunker.
+
+ create_layer_if_not_exists(gpkg_path):
+ Create a GPKG layer if it does not exist.
+
+ write_chunks_to_gpkg(gpkg_path):
+ Writes the chunk polygon boundaries to a GeoPackage.
+
Example:
grid_chunker = GridChunker(0, 100, 0, 100, 10, 5)
for chunk in grid_chunker.chunks():
@@ -46,6 +57,7 @@ def __init__(
cell_size: float,
chunk_size: int,
epsg: int,
+ geometry: bytes = None,
):
"""
Initializes the GridChunker with the given grid boundaries, cell size, and chunk size.
@@ -58,6 +70,7 @@ def __init__(
cell_size (float): Size of each cell in the grid.
chunk_size (int): Number of cells in each chunk.
epsg (int): The EPSG code of the grid.
+ geometry (bytes): The geometry in WKB format.
"""
self.xmin = xmin
self.xmax = xmax
@@ -72,7 +85,7 @@ def __init__(
self.x_range_count = int((xmax - xmin) / cell_size)
self.y_range_count = int((ymax - ymin) / cell_size)
self.epsg = epsg
- self.geometry = None
+ self.set_geometry(geometry)
self.layer_name = "chunks"
self.gpkg_path = None # Initialize gpkg_path
@@ -158,36 +171,17 @@ def write_chunks_to_gpkg(self, gpkg_path):
f"Could not open target layer {self.layer_name} in {gpkg_path}"
)
# Create the feature and set values
+ layer.StartTransaction()
+
for chunk in self.chunks():
feature = ogr.Feature(layer.GetLayerDefn())
feature.SetField("index", chunk["index"])
-
- # Create polygon from bounding box coordinates
- ring = ogr.Geometry(ogr.wkbLinearRing)
- ring.AddPoint(chunk["x_start"], chunk["y_start"])
- ring.AddPoint(chunk["x_end"], chunk["y_start"])
- ring.AddPoint(chunk["x_end"], chunk["y_end"])
- ring.AddPoint(chunk["x_start"], chunk["y_end"])
- ring.AddPoint(chunk["x_start"], chunk["y_start"])
-
- polygon = ogr.Geometry(ogr.wkbPolygon)
- polygon.AddGeometry(ring)
-
+ polygon = chunk["geometry"]
feature.SetGeometry(polygon)
-
- # if the geometry is not none and the polygon intersects with it, add it to the layer
- if self.geometry is not None and self.geometry.Intersects(polygon):
- layer.CreateFeature(feature)
- if self.geometry.Contains(polygon):
- feature.SetField("type", "inside")
- layer.SetFeature(feature)
- else:
- feature.SetField("type", "edge")
- layer.SetFeature(feature)
- else:
- pass
+ feature.SetField("type", chunk["type"])
+ layer.CreateFeature(feature)
feature = None
-
+ layer.CommitTransaction()
# Close the data source
data_source = None
@@ -203,21 +197,42 @@ def chunks(self):
index = 0
for x_block_start in x_blocks:
- log_message(f"Processing chunk {x_block_start} of {self.x_range_count}")
+ log_message(f"Processing chunk (x) {x_block_start} of {self.x_range_count}")
x_block_end = min(x_block_start + self.chunk_size, self.x_range_count)
x_start_coord = self.xmin + x_block_start * self.cell_size
x_end_coord = self.xmin + x_block_end * self.cell_size
for y_block_start in y_blocks:
- log_message(f"Processing chunk {y_block_start} of {self.y_range_count}")
+ log_message(
+ f"Processing chunk (y) {y_block_start} of {self.y_range_count}"
+ )
y_block_end = min(y_block_start + self.chunk_size, self.y_range_count)
y_start_coord = self.ymin + y_block_start * self.cell_size
y_end_coord = self.ymin + y_block_end * self.cell_size
+ # Create polygon from bounding box coordinates
+ ring = ogr.Geometry(ogr.wkbLinearRing)
+ ring.AddPoint(x_start_coord, y_start_coord)
+ ring.AddPoint(x_end_coord, y_start_coord)
+ ring.AddPoint(x_end_coord, y_end_coord)
+ ring.AddPoint(x_start_coord, y_end_coord)
+ ring.AddPoint(x_start_coord, y_start_coord)
+
+ polygon = ogr.Geometry(ogr.wkbPolygon)
+ polygon.AddGeometry(ring)
+ chunk_position = None
+ # if the geometry is not none and the polygon intersects with it, add it to the layer
+ if self.geometry is not None and self.geometry.Intersects(polygon):
+ if self.geometry.Contains(polygon):
+ chunk_position = "inside"
+ else:
+ chunk_position = "edge"
+ else:
+ chunk_position = "undefined"
log_message(
- f"Created Chunk bbox: {x_start_coord}, {x_end_coord}, {y_start_coord}, {y_end_coord}"
+ f"Created Chunk bbox: {x_start_coord}, {x_end_coord}, {y_start_coord}, {y_end_coord}, {chunk_position}"
)
yield {
"index": index,
@@ -225,6 +240,8 @@ def chunks(self):
"x_end": x_end_coord,
"y_start": y_start_coord,
"y_end": y_end_coord,
+ "geometry": polygon,
+ "type": chunk_position,
}
index += 1
diff --git a/geest/core/tasks/study_area.py b/geest/core/tasks/study_area.py
index e61eca8e..c4278c51 100644
--- a/geest/core/tasks/study_area.py
+++ b/geest/core/tasks/study_area.py
@@ -660,25 +660,14 @@ def create_and_save_grid(self, normalized_name, geom, bbox):
cell_size,
chunk_size=chunk_size,
epsg=self.epsg_code,
+ geometry=geom.ExportToWkb(),
)
- chunker.set_geometry(geom.ExportToWkb())
chunker.write_chunks_to_gpkg(self.gpkg_path)
log_message(
f"Creating grid for extents: xmin {xmin}, xmax {xmax}, ymin {ymin}, ymax {ymax}"
)
- # Approx count for logging
- x_range_count = int((xmax - xmin) / cell_size)
- y_range_count = int((ymax - ymin) / cell_size)
- self.current_geom_cell_count_estimate = x_range_count * y_range_count
- if self.current_geom_cell_count_estimate == 0:
- log_message("No cells to generate.")
- return
- log_message(
- f"Estimated total cells to generate: {self.current_geom_cell_count_estimate}"
- )
-
# OGR geometry intersection can be slow for large grids.
# If this area is huge, consider a more robust approach or indexing.
# For demonstration, we do a naive approach.
@@ -701,28 +690,43 @@ def create_and_save_grid(self, normalized_name, geom, bbox):
log_message(f"Chunk size: {chunk_size}")
self.feedback.setProgress(0)
+ counter = (
+ 1 # We cant use the chunk index as it includes chunks outside the geometry
+ )
for chunk in chunker.chunks():
start_time = (
time.time()
) # used for both create chunk start and total chunk start
index = chunk["index"]
- task = GridFromBbox(
- index,
- (chunk["x_start"], chunk["x_end"], chunk["y_start"], chunk["y_end"]),
- geom,
- cell_size,
- feedback,
- )
- self.track_time("Creating chunks", start_time)
- # Not running in thread for now, see note below
- task.run()
+ relationship = chunk["type"] # inside, edge or undefined
+ if relationship != "undefined":
+ task = GridFromBbox(
+ index,
+ (
+ chunk["x_start"],
+ chunk["x_end"],
+ chunk["y_start"],
+ chunk["y_end"],
+ ),
+ geom,
+ cell_size,
+ feedback,
+ )
+ self.track_time("Creating chunks", start_time)
+ # Not running in thread for now, see note below
+ task.run()
- self.write_chunk(layer, task, normalized_name)
- # We use the progress object to notify of progress in the subtask
- # And the QgsTask progressChanged signal to track the main task
+ self.write_chunk(layer, task, normalized_name)
+ # We use the progress object to notify of progress in the subtask
+ # And the QgsTask progressChanged signal to track the main task
+ else:
+ log_message(f"Chunk {index} is outside the geometry.")
+ continue
try:
- current_progress = int(index + 1 / (chunk_count * chunk_size))
- log_message(f"XXXXXX Chunks Progress: {current_progress}% XXXXXX")
+ current_progress = int((counter / chunk_count) * 100)
+ log_message(
+ f"XXXXXX Chunks Progress: {counter} / {chunk_count} : {current_progress}% XXXXXX"
+ )
self.feedback.setProgress(current_progress)
except ZeroDivisionError:
pass
@@ -735,6 +739,7 @@ def create_and_save_grid(self, normalized_name, geom, bbox):
# log_message(f"{len(worker_tasks)} tasks queued.")
# task_manager.addTask(task)
self.track_time("Complete chunk", start_time)
+ counter += 1
ds = None
# ----------------------------
# Print out metrics summary
@@ -770,27 +775,12 @@ def write_chunk(self, layer, task, normalized_name):
feature.SetGeometry(geometry)
layer.CreateFeature(feature)
feature = None
-
self.current_geom_actual_cell_count += 1
- if self.current_geom_actual_cell_count % 10000 == 0:
-
- try:
- log_message(
- f" Cell count: {self.current_geom_actual_cell_count}"
- )
- log_message(
- f" Total cells: {self.current_geom_cell_count_estimate}"
- )
- percent = (
- self.current_geom_actual_cell_count
- / float(self.current_geom_cell_count_estimate)
- ) * 100.0
- log_message(f" Percent complete: {percent:.2f}%")
- log_message(
- f"Grid creation for part {normalized_name}: {self.current_geom_actual_cell_count}/{self.current_geom_cell_count_estimate} ({percent:.2f}%)"
- )
- except ZeroDivisionError:
- pass
+ if self.current_geom_actual_cell_count % 20000 == 0:
+ log_message(
+ f" Cell count: {self.current_geom_actual_cell_count}"
+ )
+ log_message(f" Grid creation for part {normalized_name}")
# commit changes
layer.CommitTransaction()
layer.StartTransaction()
From 42adb1d6cf73f2f46985292eff73f65435b1fadd Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sun, 9 Feb 2025 10:16:56 +0000
Subject: [PATCH 43/56] Adds study area summary report to geest
---
config.json | 2 +-
geest/core/reports/study_area_report.py | 139 +++++++++++++++++++++++
geest/gui/panels/create_project_panel.py | 25 ++++
geest/gui/panels/tree_panel.py | 36 ++++++
test/test_study_area_report.py | 80 +++++++++++++
5 files changed, 281 insertions(+), 1 deletion(-)
create mode 100644 geest/core/reports/study_area_report.py
create mode 100644 test/test_study_area_report.py
diff --git a/config.json b/config.json
index 064fea8f..1abb3fb5 100644
--- a/config.json
+++ b/config.json
@@ -16,7 +16,7 @@
"author": "Kartoza for and with The World Bank",
"email": "info@kartoza.com, gost@worldbank.org",
"description": "Gender Enabling Environments Spatial Tool",
- "version": "0.5.5",
+ "version": "0.5.6",
"changelog": "",
"server": false
}
diff --git a/geest/core/reports/study_area_report.py b/geest/core/reports/study_area_report.py
new file mode 100644
index 00000000..69f13ff0
--- /dev/null
+++ b/geest/core/reports/study_area_report.py
@@ -0,0 +1,139 @@
+from qgis.core import (
+ QgsProject,
+ QgsVectorLayer,
+ QgsLayout,
+ QgsLayoutItemLabel,
+ QgsLayoutPoint,
+ QgsUnitTypes,
+ QgsLayoutExporter,
+ QgsStatisticalSummary,
+)
+from qgis.PyQt.QtGui import QFont
+
+
+class StudyAreaReport:
+ """
+ A class to generate a PDF report from a GeoPackage table of study area creation status data.
+
+ The report computes summary statistics (based on the field "geom_total_duration_secs")
+ and creates a QGIS layout (report) that is then exported to PDF.
+ """
+
+ def __init__(self, layer_input, report_name="Study Area Creation Report"):
+ """
+ Initialize the report.
+
+ Parameters:
+ layer_input (str or QgsVectorLayer): Either a file path to the GeoPackage (from which the
+ layer "study_area_creation_status" will be loaded) or an existing QgsVectorLayer.
+ report_name (str): The title to use for the report.
+
+ Raises:
+ ValueError: If the layer cannot be loaded from the given file path.
+ TypeError: If layer_input is neither a string nor a QgsVectorLayer.
+ """
+ if isinstance(layer_input, str):
+ uri = f"{layer_input}|layername=study_area_creation_status"
+ self.layer = QgsVectorLayer(uri, "study_area_creation_status", "ogr")
+ if not self.layer.isValid():
+ raise ValueError("Failed to load layer from the given file path.")
+ elif isinstance(layer_input, QgsVectorLayer):
+ self.layer = layer_input
+ else:
+ raise TypeError(
+ "layer_input must be a file path (str) or a QgsVectorLayer instance."
+ )
+
+ self.report_name = report_name
+ self.layout = None # Will hold the QgsLayout for the report
+
+ def compute_statistics(self, field_name="geom_total_duration_secs"):
+ """
+ Compute statistical summary for a given field in the layer.
+
+ Parameters:
+ field_name (str): The attribute field on which to compute statistics.
+
+ Returns:
+ dict: A dictionary containing 'count', 'min', 'max', 'mean', 'sum', and 'std_dev'.
+ """
+ values = []
+ for feat in self.layer.getFeatures():
+ val = feat[field_name]
+ if val is not None:
+ values.append(val)
+ if not values:
+ raise ValueError(f"No valid data found for field '{field_name}'.")
+
+ count_val = len(values)
+ sum_val = sum(values)
+ min_val = min(values)
+ max_val = max(values)
+ mean_val = sum_val / count_val
+ # Compute population standard deviation
+ var = sum((x - mean_val) ** 2 for x in values) / count_val
+ std_dev = var**0.5
+
+ return {
+ "count": count_val,
+ "min": min_val,
+ "max": max_val,
+ "mean": mean_val,
+ "sum": sum_val,
+ "std_dev": std_dev,
+ }
+
+ def create_layout(self):
+ """
+ Create a QGIS layout (report) that includes a title and a label with summary statistics.
+
+ The layout is stored in the attribute self.layout.
+ """
+ project = QgsProject.instance()
+ self.layout = QgsLayout(project)
+ self.layout.initializeDefaults()
+ # self.layout.setTitle(self.report_name)
+
+ # Add a title label
+ title = QgsLayoutItemLabel(self.layout)
+ title.setText(self.report_name)
+ title.setFont(QFont("Arial", 20))
+ title.adjustSizeToText()
+ title.attemptMove(QgsLayoutPoint(20, 20, QgsUnitTypes.LayoutMillimeters))
+ self.layout.addLayoutItem(title)
+
+ # Compute statistics and add a summary label
+ stats = self.compute_statistics()
+ summary_text = (
+ f"Total parts: {stats['count']}\n"
+ f"Minimum processing time: {stats['min']:.3f} sec\n"
+ f"Maximum processing time: {stats['max']:.3f} sec\n"
+ f"Average processing time: {stats['mean']:.3f} sec\n"
+ f"Total processing time: {stats['sum']:.3f} sec\n"
+ f"Standard Deviation: {stats['std_dev']:.3f} sec"
+ )
+ summary_label = QgsLayoutItemLabel(self.layout)
+ summary_label.setText(summary_text)
+ summary_label.setFont(QFont("Arial", 12))
+ summary_label.adjustSizeToText()
+ summary_label.attemptMove(
+ QgsLayoutPoint(20, 40, QgsUnitTypes.LayoutMillimeters)
+ )
+ self.layout.addLayoutItem(summary_label)
+
+ def export_pdf(self, output_path):
+ """
+ Export the current layout as a PDF file.
+
+ Parameters:
+ output_path (str): The full file path (including filename) for the output PDF.
+
+ Returns:
+ bool: True if the export was successful, False otherwise.
+ """
+ if self.layout is None:
+ self.create_layout()
+ export_settings = QgsLayoutExporter.PdfExportSettings()
+ exporter = QgsLayoutExporter(self.layout)
+ result = exporter.exportToPdf(output_path, export_settings)
+ return result == QgsLayoutExporter.Success
diff --git a/geest/gui/panels/create_project_panel.py b/geest/gui/panels/create_project_panel.py
index cc972e16..154a2367 100644
--- a/geest/gui/panels/create_project_panel.py
+++ b/geest/gui/panels/create_project_panel.py
@@ -25,6 +25,8 @@
from geest.core import WorkflowQueueManager
from geest.utilities import log_message
from geest.gui.widgets import CustomBannerLabel
+from geest.core.reports.study_area_report import StudyAreaReport
+import platform
FORM_CLASS = get_ui_class("create_project_panel_base.ui")
@@ -255,7 +257,30 @@ def on_task_completed(self):
)
self.progress_bar.setVisible(False)
self.child_progress_bar.setVisible(False)
+ gpkg_path = os.path.join(self.working_dir, "study_area", "study_area.gpkg")
+ report = StudyAreaReport(
+ layer_input=gpkg_path, report_name="Study Area Summary"
+ )
+ report.create_layout()
+ report.export_pdf(os.path.join(self.working_dir, "study_area_report.pdf"))
+ # open the pdf using the system PDF viewer
+ # Windows
+ if os.name == "nt": # Windows
+ os.system(
+ f'start "{os.path.join(self.working_dir, "study_area_report.pdf")}"'
+ )
+ else: # macOS and Linux
+ system = platform.system().lower()
+ if system == "darwin": # macOS
+ os.system(
+ f'open "{os.path.join(self.working_dir, "study_area_report.pdf")}"'
+ )
+ else: # Linux
+ os.system(
+ f'xdg-open "{os.path.join(self.working_dir, "study_area_report.pdf")}"'
+ )
self.enable_widgets()
+
self.switch_to_next_tab.emit()
def update_recent_projects(self, directory):
diff --git a/geest/gui/panels/tree_panel.py b/geest/gui/panels/tree_panel.py
index d6ffa10e..a0c9ba91 100644
--- a/geest/gui/panels/tree_panel.py
+++ b/geest/gui/panels/tree_panel.py
@@ -4,6 +4,8 @@
import traceback
from logging import getLogger
from typing import Union, Dict, List
+import platform
+
from qgis.PyQt.QtWidgets import (
QAction,
QApplication,
@@ -61,6 +63,7 @@
OpportunitiesByWeeScoreProcessingTask,
OpportunitiesByWeeScorePopulationProcessingTask,
)
+from geest.core.reports.study_area_report import StudyAreaReport
from geest.utilities import log_message
@@ -553,6 +556,12 @@ def update_action_text():
add_study_area_layers_action.triggered.connect(self.add_study_area_to_map)
menu.addAction(add_study_area_layers_action)
+ add_study_area_report_action = QAction("Show Study Area Report", self)
+ add_study_area_report_action.triggered.connect(
+ self.generate_study_area_report
+ )
+ menu.addAction(add_study_area_report_action)
+
open_log_file_action = QAction("Open Log File", self)
open_log_file_action.triggered.connect(self.open_log_file)
menu.addAction(open_log_file_action)
@@ -640,6 +649,33 @@ def update_action_text():
# Show the menu at the cursor's position
menu.exec_(self.treeView.viewport().mapToGlobal(position))
+ def generate_study_area_report(self):
+ """Add a report showing population information for the study area."""
+ gpkg_path = os.path.join(
+ self.working_directory, "study_area", "study_area.gpkg"
+ )
+ report = StudyAreaReport(
+ layer_input=gpkg_path, report_name="Study Area Summary"
+ )
+ report.create_layout()
+ report.export_pdf(os.path.join(self.working_directory, "study_area_report.pdf"))
+ # open the pdf using the system PDF viewer
+ # Windows
+ if os.name == "nt": # Windows
+ os.system(
+ f'start "{os.path.join(self.working_directory, "study_area_report.pdf")}"'
+ )
+ else: # macOS and Linux
+ system = platform.system().lower()
+ if system == "darwin": # macOS
+ os.system(
+ f'open "{os.path.join(self.working_directory, "study_area_report.pdf")}"'
+ )
+ else: # Linux
+ os.system(
+ f'xdg-open "{os.path.join(self.working_directory, "study_area_report.pdf")}"'
+ )
+
def add_masked_scores_to_map(self, item):
"""Add the masked scores to the map."""
self.add_to_map(
diff --git a/test/test_study_area_report.py b/test/test_study_area_report.py
new file mode 100644
index 00000000..12abbef4
--- /dev/null
+++ b/test/test_study_area_report.py
@@ -0,0 +1,80 @@
+import unittest
+from qgis.core import QgsVectorLayer, QgsField, QgsFeature
+from qgis.PyQt.QtCore import QVariant
+from geest.core.reports.study_area_report import StudyAreaReport
+
+# ================================
+# Test Suite for StudyAreaReport
+# ================================
+
+
+class TestStudyAreaReport(unittest.TestCase):
+ """
+ Test suite for the StudyAreaReport class.
+
+ This suite creates a memory layer with a 'geom_total_duration_secs' field and sample data,
+ then tests the computation of statistics, creation of layout, and PDF export functionality.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ """
+ Set up a memory layer with sample data for testing.
+ """
+ # Create a memory vector layer
+ cls.layer = QgsVectorLayer("Point?crs=EPSG:4326", "test_layer", "memory")
+ provider = cls.layer.dataProvider()
+ provider.addAttributes([QgsField("geom_total_duration_secs", QVariant.Double)])
+ cls.layer.updateFields()
+
+ # Add sample features with known processing times
+ features = []
+ sample_values = [0.5, 1.0, 2.0, 3.0, 4.0]
+ for val in sample_values:
+ feat = QgsFeature(cls.layer.fields())
+ feat.setAttribute("geom_total_duration_secs", val)
+ # Geometry is not used in the statistics so we leave it empty (None)
+ features.append(feat)
+ provider.addFeatures(features)
+ cls.layer.updateExtents()
+
+ # Instantiate the report using the memory layer
+ cls.report = StudyAreaReport(cls.layer, report_name="Test Report")
+
+ def test_compute_statistics(self):
+ """
+ Test that the compute_statistics method returns correct summary values.
+ """
+ stats = self.report.compute_statistics()
+ expected_sum = sum([0.5, 1.0, 2.0, 3.0, 4.0])
+ expected_mean = expected_sum / 5
+ self.assertEqual(stats["count"], 5)
+ self.assertAlmostEqual(stats["min"], 0.5)
+ self.assertAlmostEqual(stats["max"], 4.0)
+ self.assertAlmostEqual(stats["mean"], expected_mean)
+ self.assertAlmostEqual(stats["sum"], expected_sum)
+
+ def test_create_layout(self):
+ """
+ Test that the layout is created and contains at least two items (the title and summary).
+ """
+ self.report.create_layout()
+ self.assertIsNotNone(self.report.layout)
+ items = self.report.layout.items()
+ self.assertGreaterEqual(len(items), 2)
+
+ def test_export_pdf(self):
+ """
+ Test that the PDF export function creates a file successfully.
+ """
+ output_pdf = "test_report.pdf"
+ success = self.report.export_pdf(output_pdf)
+ self.assertTrue(success)
+ self.assertTrue(os.path.exists(output_pdf))
+ # Clean up the test PDF file.
+ os.remove(output_pdf)
+
+
+if __name__ == "__main__":
+ # Run the test suite without exiting the interpreter.
+ unittest.main(exit=False)
From 3c1b98490d8649c05e877dbb32bf9bce5403fa69 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sun, 9 Feb 2025 12:15:27 +0000
Subject: [PATCH 44/56] Iterate over study area layers for report
---
geest/core/reports/study_area_report.py | 110 +++++++++++++++++++----
geest/gui/panels/create_project_panel.py | 4 +-
geest/gui/panels/tree_panel.py | 4 +-
3 files changed, 94 insertions(+), 24 deletions(-)
diff --git a/geest/core/reports/study_area_report.py b/geest/core/reports/study_area_report.py
index 69f13ff0..6657850e 100644
--- a/geest/core/reports/study_area_report.py
+++ b/geest/core/reports/study_area_report.py
@@ -1,3 +1,5 @@
+from collections import defaultdict
+
from qgis.core import (
QgsProject,
QgsVectorLayer,
@@ -6,9 +8,12 @@
QgsLayoutPoint,
QgsUnitTypes,
QgsLayoutExporter,
- QgsStatisticalSummary,
+ QgsLayoutItemMap,
+ QgsLayoutSize,
+ QgsProviderRegistry,
)
from qgis.PyQt.QtGui import QFont
+from geest.utilities import log_message
class StudyAreaReport:
@@ -19,35 +24,92 @@ class StudyAreaReport:
and creates a QGIS layout (report) that is then exported to PDF.
"""
- def __init__(self, layer_input, report_name="Study Area Creation Report"):
+ def __init__(self, gpkg_path: str, report_name="Study Area Creation Report"):
"""
Initialize the report.
Parameters:
- layer_input (str or QgsVectorLayer): Either a file path to the GeoPackage (from which the
- layer "study_area_creation_status" will be loaded) or an existing QgsVectorLayer.
+ layer_input (str): A file path to the GeoPackage (from which the
+ layer "study_area_creation_status" and other layers will be loaded).
report_name (str): The title to use for the report.
Raises:
ValueError: If the layer cannot be loaded from the given file path.
TypeError: If layer_input is neither a string nor a QgsVectorLayer.
"""
- if isinstance(layer_input, str):
- uri = f"{layer_input}|layername=study_area_creation_status"
- self.layer = QgsVectorLayer(uri, "study_area_creation_status", "ogr")
- if not self.layer.isValid():
- raise ValueError("Failed to load layer from the given file path.")
- elif isinstance(layer_input, QgsVectorLayer):
- self.layer = layer_input
- else:
- raise TypeError(
- "layer_input must be a file path (str) or a QgsVectorLayer instance."
- )
+
+ uri = f"{gpkg_path}|layername=study_area_creation_status"
+ self.gpkg_path = gpkg_path
+ layer = QgsVectorLayer(uri, "study_area_creation_status", "ogr")
+ if not layer.isValid():
+ raise ValueError("Failed to load layer from the given file path.")
self.report_name = report_name
self.layout = None # Will hold the QgsLayout for the report
+ self.layers = self.load_layers_from_gpkg()
+
+ def __del__(self):
+ """
+ Destructor to clean up layers from the QGIS project.
+ """
+ for layer_name, layer in self.layers.items():
+ if layer:
+ del layer
+ log_message(f"Layer '{layer_name}' deleted.")
- def compute_statistics(self, field_name="geom_total_duration_secs"):
+ def load_layers_from_gpkg(self):
+ """
+ Load all vector layers from the specified GeoPackage.
+
+ Returns:
+ dict: A dictionary mapping layer names to QgsVectorLayer objects.
+ """
+ layers = {}
+ # Create a temporary layer to access the data provider
+ temp_layer = QgsVectorLayer(self.gpkg_path, "temp", "ogr")
+ if not temp_layer.isValid():
+ log_message(f"Failed to load GeoPackage: {self.gpkg_path}")
+ return layers
+
+ # Retrieve subLayers information
+ sub_layers = temp_layer.dataProvider().subLayers()
+ for sub_layer in sub_layers:
+ # sub_layer is a string in the format "layer_id!!::!!layer_name"
+ log_message(f"Loading layer: {sub_layer}")
+ parts = sub_layer.split("!!::!!")
+ layer_id = parts[0]
+ layer_name = parts[1]
+ uri = f"{self.gpkg_path}|layername={layer_name}"
+ layer = QgsVectorLayer(uri, layer_name, "ogr")
+ if layer.isValid():
+ layers[layer_name] = layer
+ else:
+ log_message(f"Failed to load layer: {layer_name}")
+ return layers
+
+ def compute_statistics(self, layer):
+ """
+ Compute summary statistics for a given vector layer.
+
+ Parameters:
+ layer (QgsVectorLayer): The vector layer to analyze.
+
+ Returns:
+ dict: A dictionary containing summary statistics.
+ """
+ area_counts = defaultdict(int)
+ total_count = 0
+
+ for feature in layer.getFeatures():
+ area_name = feature[self.area_field_name]
+ area_counts[area_name] += 1
+ total_count += 1
+
+ return {"area_counts": dict(area_counts), "total_count": total_count}
+
+ def compute_study_area_creation_statistics(
+ self, field_name="geom_total_duration_secs"
+ ):
"""
Compute statistical summary for a given field in the layer.
@@ -58,7 +120,9 @@ def compute_statistics(self, field_name="geom_total_duration_secs"):
dict: A dictionary containing 'count', 'min', 'max', 'mean', 'sum', and 'std_dev'.
"""
values = []
- for feat in self.layer.getFeatures():
+ uri = f"{self.gpkg_path}|layername=study_area_creation_status"
+ layer = QgsVectorLayer(uri, "study_area_creation_status", "ogr")
+ for feat in layer.getFeatures():
val = feat[field_name]
if val is not None:
values.append(val)
@@ -103,7 +167,7 @@ def create_layout(self):
self.layout.addLayoutItem(title)
# Compute statistics and add a summary label
- stats = self.compute_statistics()
+ stats = self.compute_study_area_creation_statistics()
summary_text = (
f"Total parts: {stats['count']}\n"
f"Minimum processing time: {stats['min']:.3f} sec\n"
@@ -121,6 +185,16 @@ def create_layout(self):
)
self.layout.addLayoutItem(summary_label)
+ # Add a map item capturing the current map canvas
+ map_item = QgsLayoutItemMap(self.layout)
+ # Position the map item (e.g., at 20 mm, 70 mm) and give it a size (e.g., 150x100 mm)
+ map_item.attemptMove(QgsLayoutPoint(20, 70, QgsUnitTypes.LayoutMillimeters))
+ map_item.attemptResize(QgsLayoutSize(150, 100, QgsUnitTypes.LayoutMillimeters))
+ # Set the map extent to the current map canvas extent
+ # map_item.setExtent(canvas.extent())
+ map_item.refresh()
+ self.layout.addLayoutItem(map_item)
+
def export_pdf(self, output_path):
"""
Export the current layout as a PDF file.
diff --git a/geest/gui/panels/create_project_panel.py b/geest/gui/panels/create_project_panel.py
index 154a2367..12e6a13a 100644
--- a/geest/gui/panels/create_project_panel.py
+++ b/geest/gui/panels/create_project_panel.py
@@ -258,9 +258,7 @@ def on_task_completed(self):
self.progress_bar.setVisible(False)
self.child_progress_bar.setVisible(False)
gpkg_path = os.path.join(self.working_dir, "study_area", "study_area.gpkg")
- report = StudyAreaReport(
- layer_input=gpkg_path, report_name="Study Area Summary"
- )
+ report = StudyAreaReport(gpkg_path=gpkg_path, report_name="Study Area Summary")
report.create_layout()
report.export_pdf(os.path.join(self.working_dir, "study_area_report.pdf"))
# open the pdf using the system PDF viewer
diff --git a/geest/gui/panels/tree_panel.py b/geest/gui/panels/tree_panel.py
index a0c9ba91..7734e3c3 100644
--- a/geest/gui/panels/tree_panel.py
+++ b/geest/gui/panels/tree_panel.py
@@ -654,9 +654,7 @@ def generate_study_area_report(self):
gpkg_path = os.path.join(
self.working_directory, "study_area", "study_area.gpkg"
)
- report = StudyAreaReport(
- layer_input=gpkg_path, report_name="Study Area Summary"
- )
+ report = StudyAreaReport(gpkg_path=gpkg_path, report_name="Study Area Summary")
report.create_layout()
report.export_pdf(os.path.join(self.working_directory, "study_area_report.pdf"))
# open the pdf using the system PDF viewer
From 16ff958fa1f0699862d2ea5f0129543911760240 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sun, 9 Feb 2025 12:16:51 +0000
Subject: [PATCH 45/56] Iterate over study area layers for report
---
geest/core/reports/study_area_report.py | 53 ++++++++++++++++++++-----
1 file changed, 42 insertions(+), 11 deletions(-)
diff --git a/geest/core/reports/study_area_report.py b/geest/core/reports/study_area_report.py
index 6657850e..e6161714 100644
--- a/geest/core/reports/study_area_report.py
+++ b/geest/core/reports/study_area_report.py
@@ -10,7 +10,7 @@
QgsLayoutExporter,
QgsLayoutItemMap,
QgsLayoutSize,
- QgsProviderRegistry,
+ QgsLayoutItemPage,
)
from qgis.PyQt.QtGui import QFont
from geest.utilities import log_message
@@ -101,7 +101,7 @@ def compute_statistics(self, layer):
total_count = 0
for feature in layer.getFeatures():
- area_name = feature[self.area_field_name]
+ area_name = feature["area_name"]
area_counts[area_name] += 1
total_count += 1
@@ -185,15 +185,46 @@ def create_layout(self):
)
self.layout.addLayoutItem(summary_label)
- # Add a map item capturing the current map canvas
- map_item = QgsLayoutItemMap(self.layout)
- # Position the map item (e.g., at 20 mm, 70 mm) and give it a size (e.g., 150x100 mm)
- map_item.attemptMove(QgsLayoutPoint(20, 70, QgsUnitTypes.LayoutMillimeters))
- map_item.attemptResize(QgsLayoutSize(150, 100, QgsUnitTypes.LayoutMillimeters))
- # Set the map extent to the current map canvas extent
- # map_item.setExtent(canvas.extent())
- map_item.refresh()
- self.layout.addLayoutItem(map_item)
+ # Compute and add summary statistics for each layer on separate pages
+ for page_number, (layer_name, layer) in enumerate(self.layers.items()):
+
+ # Add a new page for each layer
+ page = QgsLayoutItemPage(self.layout)
+ self.layout.pageCollection().addPage(page)
+
+ # Compute statistics for the current layer
+ try:
+ stats = self.compute_statistics(layer)
+ summary_text = f"Layer: {layer_name}\n"
+ for area_name, count in stats["area_counts"].items():
+ summary_text += f"{area_name}: {count} features\n"
+ summary_text += f"Total count: {stats['total_count']} features"
+ except Exception as e:
+ log_message(f"Error computing statistics for layer '{layer_name}': {e}")
+ continue
+ # Add summary label to the current page
+ summary_label = QgsLayoutItemLabel(self.layout)
+ summary_label.setText(summary_text)
+ summary_label.setFont(QFont("Arial", 12))
+ summary_label.adjustSizeToText()
+ # Position the label on the current page
+ summary_label.attemptMove(
+ QgsLayoutPoint(20, 40, QgsUnitTypes.LayoutMillimeters), page=page_number
+ )
+ self.layout.addLayoutItem(summary_label)
+
+ # Add a map item for the current layer
+ map_item = QgsLayoutItemMap(self.layout)
+ map_item.setLayers([layer])
+ map_item.attemptMove(
+ QgsLayoutPoint(20, 70, QgsUnitTypes.LayoutMillimeters), page=page_number
+ )
+ map_item.attemptResize(
+ QgsLayoutSize(150, 100, QgsUnitTypes.LayoutMillimeters)
+ )
+ map_item.setExtent(layer.extent())
+ map_item.refresh()
+ self.layout.addLayoutItem(map_item)
def export_pdf(self, output_path):
"""
From f5c4de5a063b08729adc199294530ef51f8e87a9 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sun, 9 Feb 2025 18:06:29 +0000
Subject: [PATCH 46/56] Study area report shows outputs on separate pages
---
geest/core/reports/study_area_report.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/geest/core/reports/study_area_report.py b/geest/core/reports/study_area_report.py
index e6161714..9bfb5f74 100644
--- a/geest/core/reports/study_area_report.py
+++ b/geest/core/reports/study_area_report.py
@@ -11,8 +11,9 @@
QgsLayoutItemMap,
QgsLayoutSize,
QgsLayoutItemPage,
+ QgsLayoutMeasurement,
)
-from qgis.PyQt.QtGui import QFont
+from qgis.PyQt.QtGui import QFont, QColor
from geest.utilities import log_message
@@ -187,11 +188,10 @@ def create_layout(self):
# Compute and add summary statistics for each layer on separate pages
for page_number, (layer_name, layer) in enumerate(self.layers.items()):
-
# Add a new page for each layer
page = QgsLayoutItemPage(self.layout)
+ page.setPageSize("A4", QgsLayoutItemPage.Portrait)
self.layout.pageCollection().addPage(page)
-
# Compute statistics for the current layer
try:
stats = self.compute_statistics(layer)
@@ -225,6 +225,10 @@ def create_layout(self):
map_item.setExtent(layer.extent())
map_item.refresh()
self.layout.addLayoutItem(map_item)
+ # Add a black frame around the map item
+ map_item.setFrameEnabled(True)
+ map_item.setFrameStrokeColor(QColor(0, 0, 0))
+ map_item.setFrameStrokeWidth(QgsLayoutMeasurement(0.5))
def export_pdf(self, output_path):
"""
From 69acd004beeee5719bb9e7f96f2c25c257b169f0 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sun, 9 Feb 2025 22:20:43 +0000
Subject: [PATCH 47/56] WIP templating for study area report
---
geest/core/reports/study_area_report.py | 110 +++-
.../reports/templated_study_area_report.py | 132 ++++
.../qpt/study_area_report_template.qpt | 613 ++++++++++++++++++
3 files changed, 846 insertions(+), 9 deletions(-)
create mode 100644 geest/core/reports/templated_study_area_report.py
create mode 100644 geest/resources/qpt/study_area_report_template.qpt
diff --git a/geest/core/reports/study_area_report.py b/geest/core/reports/study_area_report.py
index 9bfb5f74..b5c53162 100644
--- a/geest/core/reports/study_area_report.py
+++ b/geest/core/reports/study_area_report.py
@@ -12,9 +12,16 @@
QgsLayoutSize,
QgsLayoutItemPage,
QgsLayoutMeasurement,
+ QgsPrintLayout,
+ QgsLayoutItemLabel,
+ QgsLayoutItemMap,
+ QgsReadWriteContext,
+ QgsLayoutExporter,
+ QgsVectorLayer,
)
+from qgis.PyQt.QtXml import QDomDocument
from qgis.PyQt.QtGui import QFont, QColor
-from geest.utilities import log_message
+from geest.utilities import log_message, resources_path
class StudyAreaReport:
@@ -48,6 +55,9 @@ def __init__(self, gpkg_path: str, report_name="Study Area Creation Report"):
self.report_name = report_name
self.layout = None # Will hold the QgsLayout for the report
self.layers = self.load_layers_from_gpkg()
+ self.template_path = resources_path(
+ "resources", "qpt", f"study_area_report_template.qpt"
+ )
def __del__(self):
"""
@@ -157,14 +167,42 @@ def create_layout(self):
project = QgsProject.instance()
self.layout = QgsLayout(project)
self.layout.initializeDefaults()
- # self.layout.setTitle(self.report_name)
+ self.layout.initializeDefaults()
+
+ # Load the QPT template
+ try:
+ with open(self.template_path, "r") as template_file:
+ template_content = template_file.read()
+ except IOError:
+ raise FileNotFoundError(
+ f"Template file '{self.template_path}' not found or cannot be read."
+ )
+
+ document = QDomDocument()
+ if not document.setContent(template_content):
+ raise ValueError(
+ f"Failed to parse the template content from '{self.template_path}'."
+ )
+
+ context = QgsReadWriteContext()
+ if not self.layout.loadFromTemplate(document, context):
+ raise ValueError(
+ f"Failed to load the template into the layout from '{self.template_path}'."
+ )
+
+ # self.layout.setTitle(self.report_name)
+ page = QgsLayoutItemPage(self.layout)
+ page.setPageSize("A4", QgsLayoutItemPage.Portrait)
+ self.layout.pageCollection().addPage(page)
# Add a title label
title = QgsLayoutItemLabel(self.layout)
title.setText(self.report_name)
title.setFont(QFont("Arial", 20))
title.adjustSizeToText()
- title.attemptMove(QgsLayoutPoint(20, 20, QgsUnitTypes.LayoutMillimeters))
+ title.attemptMove(
+ QgsLayoutPoint(20, 20, QgsUnitTypes.LayoutMillimeters), page=1
+ )
self.layout.addLayoutItem(title)
# Compute statistics and add a summary label
@@ -182,11 +220,12 @@ def create_layout(self):
summary_label.setFont(QFont("Arial", 12))
summary_label.adjustSizeToText()
summary_label.attemptMove(
- QgsLayoutPoint(20, 40, QgsUnitTypes.LayoutMillimeters)
+ QgsLayoutPoint(20, 40, QgsUnitTypes.LayoutMillimeters), page=1
)
self.layout.addLayoutItem(summary_label)
# Compute and add summary statistics for each layer on separate pages
+ current_page = 1
for page_number, (layer_name, layer) in enumerate(self.layers.items()):
# Add a new page for each layer
page = QgsLayoutItemPage(self.layout)
@@ -196,8 +235,9 @@ def create_layout(self):
try:
stats = self.compute_statistics(layer)
summary_text = f"Layer: {layer_name}\n"
- for area_name, count in stats["area_counts"].items():
- summary_text += f"{area_name}: {count} features\n"
+ # feature_count = 0
+ # for area_name, count in stats["area_counts"].items():
+ # feature_count += count
summary_text += f"Total count: {stats['total_count']} features"
except Exception as e:
log_message(f"Error computing statistics for layer '{layer_name}': {e}")
@@ -209,7 +249,8 @@ def create_layout(self):
summary_label.adjustSizeToText()
# Position the label on the current page
summary_label.attemptMove(
- QgsLayoutPoint(20, 40, QgsUnitTypes.LayoutMillimeters), page=page_number
+ QgsLayoutPoint(100, 40, QgsUnitTypes.LayoutMillimeters),
+ page=current_page,
)
self.layout.addLayoutItem(summary_label)
@@ -217,10 +258,12 @@ def create_layout(self):
map_item = QgsLayoutItemMap(self.layout)
map_item.setLayers([layer])
map_item.attemptMove(
- QgsLayoutPoint(20, 70, QgsUnitTypes.LayoutMillimeters), page=page_number
+ QgsLayoutPoint(20, 110, QgsUnitTypes.LayoutMillimeters),
+ page=current_page,
)
map_item.attemptResize(
- QgsLayoutSize(150, 100, QgsUnitTypes.LayoutMillimeters)
+ # 170mm width x 100mm height
+ QgsLayoutSize(170, 100, QgsUnitTypes.LayoutMillimeters)
)
map_item.setExtent(layer.extent())
map_item.refresh()
@@ -229,6 +272,55 @@ def create_layout(self):
map_item.setFrameEnabled(True)
map_item.setFrameStrokeColor(QColor(0, 0, 0))
map_item.setFrameStrokeWidth(QgsLayoutMeasurement(0.5))
+ # Add the page footer
+ self.add_header_and_footer(page_number=current_page)
+ current_page += 1
+
+ def add_header_and_footer(self, page_number):
+ """_summary_
+
+ Args:
+ page_number (_type_): _description_
+ """
+ footer_text = """
+
This plugin is built with support from the Canada Clean Energy and
+ Forest Climate Facility (CCEFCF), the Geospatial Operational
+ Support Team (GOST, DECSC) for the project Geospatial Assessment of
+ Women Employment and Business Opportunities in the Renewable Energy Sector.
+ This project is open source; you can download the code at
+ https://github.com/worldbank/GEEST.
+"""
+ credits_text = """Developed by Kartoza for and
+ with The World Bank."""
+ # Add summary label to the current page
+ footer_label = QgsLayoutItemLabel(self.layout)
+ footer_label.setText(footer_text)
+ footer_label.setFont(QFont("Arial", 8))
+ footer_label.setFixedSize(
+ QgsLayoutSize(180, 40, QgsUnitTypes.LayoutMillimeters)
+ )
+ # Use html mode
+ footer_label.setMode(QgsLayoutItemLabel.ModeHtml)
+ # Position the label on the current page
+ footer_label.attemptMove(
+ QgsLayoutPoint(20, 270, QgsUnitTypes.LayoutMillimeters), page=page_number
+ )
+ self.layout.addLayoutItem(footer_label)
+
+ # Add summary label to the current page
+ credits_label = QgsLayoutItemLabel(self.layout)
+ credits_label.setText(credits_text)
+ credits_label.setFont(QFont("Arial", 8))
+ credits_label.setFixedSize(
+ QgsLayoutSize(180, 40, QgsUnitTypes.LayoutMillimeters)
+ )
+ # Use html mode
+ credits_label.setMode(QgsLayoutItemLabel.ModeHtml)
+ # Position the label on the current page
+ credits_label.attemptMove(
+ QgsLayoutPoint(20, 280, QgsUnitTypes.LayoutMillimeters), page=page_number
+ )
+ self.layout.addLayoutItem(credits_label)
def export_pdf(self, output_path):
"""
diff --git a/geest/core/reports/templated_study_area_report.py b/geest/core/reports/templated_study_area_report.py
new file mode 100644
index 00000000..2f168ce6
--- /dev/null
+++ b/geest/core/reports/templated_study_area_report.py
@@ -0,0 +1,132 @@
+from qgis.core import (
+ QgsProject,
+ QgsPrintLayout,
+ QgsLayoutItemLabel,
+ QgsLayoutItemMap,
+ QgsReadWriteContext,
+ QgsLayoutExporter,
+ QgsVectorLayer,
+)
+from qgis.PyQt.QtXml import QDomDocument
+from qgis.PyQt.QtGui import QFont, QColor
+
+
+class TemplatedStudyAreaReport:
+ """
+ A class to manage QGIS Print Layouts by loading templates, modifying label text,
+ setting map layers, and exporting the final layout to a PDF.
+
+ Attributes:
+ project (QgsProject): The QGIS project instance.
+ layout (QgsPrintLayout): The print layout instance.
+ """
+
+ def __init__(self, template_path):
+ """
+ Initializes the QGISLayoutManager with a specified template.
+
+ Args:
+ template_path (str): The file path to the QPT template.
+
+ Raises:
+ FileNotFoundError: If the template file does not exist or cannot be read.
+ ValueError: If the template content is invalid or cannot be loaded.
+ """
+ self.layout = QgsPrintLayout(self.project)
+ self.layout.initializeDefaults()
+
+ # Load the QPT template
+ try:
+ with open(template_path, "r") as template_file:
+ template_content = template_file.read()
+ except IOError:
+ raise FileNotFoundError(
+ f"Template file '{template_path}' not found or cannot be read."
+ )
+
+ document = QDomDocument()
+ if not document.setContent(template_content):
+ raise ValueError(
+ f"Failed to parse the template content from '{template_path}'."
+ )
+
+ context = QgsReadWriteContext()
+ if not self.layout.loadFromTemplate(document, context):
+ raise ValueError(
+ f"Failed to load the template into the layout from '{template_path}'."
+ )
+
+ def set_label_text(self, label_id, text, font_name="Arial", font_size=12):
+ """
+ Sets the text and font properties of a label item in the layout.
+
+ Args:
+ label_id (str): The ID of the label item in the template.
+ text (str): The text to set for the label.
+ font_name (str, optional): The font family for the label text. Defaults to "Arial".
+ font_size (int, optional): The font size for the label text. Defaults to 12.
+
+ Raises:
+ KeyError: If the label item with the specified ID is not found in the layout.
+ TypeError: If the item with the specified ID is not a QgsLayoutItemLabel.
+ """
+ label = self.layout.itemById(label_id)
+ if label is None:
+ raise KeyError(f"Label item with ID '{label_id}' not found in the layout.")
+ if not isinstance(label, QgsLayoutItemLabel):
+ raise TypeError(f"Item with ID '{label_id}' is not a QgsLayoutItemLabel.")
+
+ label.setText(text)
+ label.setFont(QFont(font_name, font_size))
+ label.adjustSizeToText()
+
+ def set_map_layers(self, map_item_id, layer_paths):
+ """
+ Sets the layers for a map item in the layout based on provided file paths.
+
+ Args:
+ map_item_id (str): The ID of the map item in the template.
+ layer_paths (list of str): A list of file paths to the vector layers to be added.
+
+ Raises:
+ KeyError: If the map item with the specified ID is not found in the layout.
+ TypeError: If the item with the specified ID is not a QgsLayoutItemMap.
+ ValueError: If any of the provided layer paths are invalid or the layers cannot be loaded.
+ """
+ map_item = self.layout.itemById(map_item_id)
+ if map_item is None:
+ raise KeyError(f"Map item with ID '{map_item_id}' not found in the layout.")
+ if not isinstance(map_item, QgsLayoutItemMap):
+ raise TypeError(f"Item with ID '{map_item_id}' is not a QgsLayoutItemMap.")
+
+ layers = []
+ for path in layer_paths:
+ layer = QgsVectorLayer(path, path.split("/")[-1], "ogr")
+ if not layer.isValid():
+ raise ValueError(f"Failed to load layer from path '{path}'.")
+ self.project.addMapLayer(layer)
+ layers.append(layer)
+
+ map_item.setLayers(layers)
+ if layers:
+ map_item.setExtent(layers[0].extent())
+ map_item.setFrameEnabled(True)
+ map_item.setFrameStrokeColor(QColor(0, 0, 0))
+ map_item.setFrameStrokeWidth(0.5)
+ map_item.refresh()
+
+ def export_to_pdf(self, output_path):
+ """
+ Exports the current layout to a PDF file.
+
+ Args:
+ output_path (str): The file path where the PDF will be saved.
+
+ Returns:
+ bool: True if the export is successful, False otherwise.
+ """
+ exporter = QgsLayoutExporter(self.layout)
+ result = exporter.exportToPdf(
+ output_path, QgsLayoutExporter.PdfExportSettings()
+ )
+ return result == QgsLayoutExporter.Success
diff --git a/geest/resources/qpt/study_area_report_template.qpt b/geest/resources/qpt/study_area_report_template.qpt
new file mode 100644
index 00000000..84fd0167
--- /dev/null
+++ b/geest/resources/qpt/study_area_report_template.qpt
@@ -0,0 +1,613 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 449f65cf93f3225c2bc1dab08fdea4c31edc28da Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sun, 9 Feb 2025 23:54:31 +0000
Subject: [PATCH 48/56] Fix aspect ratio of maps in study area report
---
geest/core/reports/study_area_report.py | 95 +++++++++++++++++++------
1 file changed, 75 insertions(+), 20 deletions(-)
diff --git a/geest/core/reports/study_area_report.py b/geest/core/reports/study_area_report.py
index b5c53162..f24d8fc0 100644
--- a/geest/core/reports/study_area_report.py
+++ b/geest/core/reports/study_area_report.py
@@ -21,6 +21,7 @@
)
from qgis.PyQt.QtXml import QDomDocument
from qgis.PyQt.QtGui import QFont, QColor
+from qgis.PyQt.QtCore import Qt
from geest.utilities import log_message, resources_path
@@ -58,6 +59,40 @@ def __init__(self, gpkg_path: str, report_name="Study Area Creation Report"):
self.template_path = resources_path(
"resources", "qpt", f"study_area_report_template.qpt"
)
+ self.page_descriptions = {}
+ self.page_descriptions[
+ "study_area_bbox"
+ ] = """
+ The study area bounding box (bbox) is the outer extent of the entire study area.
+ The bounding box width and height is guaranteed to be a factor of the
+ analysis dimension. All other data products are then aligned to this bbox.
+ """
+ self.page_descriptions[
+ "study_area_bboxes"
+ ] = """
+ The study area bboxes are a set of smaller bounding boxes that surround each
+ polygon in the study area. They are grid aligned such that the origin and
+ furthest corners are guaranteed to be a factor of the analysis dimension
+ apart.
+ """
+ self.page_descriptions[
+ "study_area_polygons"
+ ] = """
+ The study area polygons are the single part form of all polygons in the
+ study area. Any invalid geometries will have been discarded.
+ """
+ self.page_descriptions[
+ "study_area_grid"
+ ] = """
+ The study area grid is a set of polygon squares that each have the
+ x and y dimension of the analysis cell size. They are guaranteed to
+ be aligned to the study area bbox and bboxes layers. The grid is used
+ to create a version of the study_area_polygons that have been expanded
+ out so that the edges align exactly to the grid.
+
+ The grid is also used to perform certain types of spatial analysis such as
+ the Active Transport layer analyses.
+ """
def __del__(self):
"""
@@ -191,20 +226,6 @@ def create_layout(self):
f"Failed to load the template into the layout from '{self.template_path}'."
)
- # self.layout.setTitle(self.report_name)
- page = QgsLayoutItemPage(self.layout)
- page.setPageSize("A4", QgsLayoutItemPage.Portrait)
- self.layout.pageCollection().addPage(page)
- # Add a title label
- title = QgsLayoutItemLabel(self.layout)
- title.setText(self.report_name)
- title.setFont(QFont("Arial", 20))
- title.adjustSizeToText()
- title.attemptMove(
- QgsLayoutPoint(20, 20, QgsUnitTypes.LayoutMillimeters), page=1
- )
- self.layout.addLayoutItem(title)
-
# Compute statistics and add a summary label
stats = self.compute_study_area_creation_statistics()
summary_text = (
@@ -220,7 +241,7 @@ def create_layout(self):
summary_label.setFont(QFont("Arial", 12))
summary_label.adjustSizeToText()
summary_label.attemptMove(
- QgsLayoutPoint(20, 40, QgsUnitTypes.LayoutMillimeters), page=1
+ QgsLayoutPoint(80, 200, QgsUnitTypes.LayoutMillimeters), page=0
)
self.layout.addLayoutItem(summary_label)
@@ -231,6 +252,18 @@ def create_layout(self):
page = QgsLayoutItemPage(self.layout)
page.setPageSize("A4", QgsLayoutItemPage.Portrait)
self.layout.pageCollection().addPage(page)
+ # Add a title label
+ title = QgsLayoutItemLabel(self.layout)
+ # Put title in title case
+ title_text = layer_name.replace("_", " ").title()
+ title.setText(title_text)
+ title.setFont(QFont("Arial", 20))
+ title.setFixedSize(QgsLayoutSize(200, 40, QgsUnitTypes.LayoutMillimeters))
+ title.attemptMove(
+ QgsLayoutPoint(20, 20, QgsUnitTypes.LayoutMillimeters),
+ page=current_page,
+ )
+ self.layout.addLayoutItem(title)
# Compute statistics for the current layer
try:
stats = self.compute_statistics(layer)
@@ -242,6 +275,26 @@ def create_layout(self):
except Exception as e:
log_message(f"Error computing statistics for layer '{layer_name}': {e}")
continue
+
+ description_text = self.page_descriptions.get(layer_name, "")
+ # Add description label to the current page
+ description_label = QgsLayoutItemLabel(self.layout)
+ description_label.setText(description_text)
+ description_label.setFont(QFont("Arial", 12))
+ description_label.adjustSizeToText()
+ description_label.setMode(QgsLayoutItemLabel.ModeHtml)
+
+ # Position the label on the current page
+ description_label.attemptMove(
+ QgsLayoutPoint(20, 40, QgsUnitTypes.LayoutMillimeters),
+ page=current_page,
+ )
+ description_label.setFixedSize(
+ QgsLayoutSize(80, 40, QgsUnitTypes.LayoutMillimeters)
+ )
+ description_label.setHAlign(Qt.AlignJustify)
+ self.layout.addLayoutItem(description_label)
+
# Add summary label to the current page
summary_label = QgsLayoutItemLabel(self.layout)
summary_label.setText(summary_text)
@@ -249,7 +302,7 @@ def create_layout(self):
summary_label.adjustSizeToText()
# Position the label on the current page
summary_label.attemptMove(
- QgsLayoutPoint(100, 40, QgsUnitTypes.LayoutMillimeters),
+ QgsLayoutPoint(120, 60, QgsUnitTypes.LayoutMillimeters),
page=current_page,
)
self.layout.addLayoutItem(summary_label)
@@ -297,7 +350,7 @@ def add_header_and_footer(self, page_number):
footer_label.setText(footer_text)
footer_label.setFont(QFont("Arial", 8))
footer_label.setFixedSize(
- QgsLayoutSize(180, 40, QgsUnitTypes.LayoutMillimeters)
+ QgsLayoutSize(160, 40, QgsUnitTypes.LayoutMillimeters)
)
# Use html mode
footer_label.setMode(QgsLayoutItemLabel.ModeHtml)
@@ -305,21 +358,23 @@ def add_header_and_footer(self, page_number):
footer_label.attemptMove(
QgsLayoutPoint(20, 270, QgsUnitTypes.LayoutMillimeters), page=page_number
)
+ footer_label.setHAlign(Qt.AlignJustify)
self.layout.addLayoutItem(footer_label)
- # Add summary label to the current page
+ # Add credits label to the current page
credits_label = QgsLayoutItemLabel(self.layout)
credits_label.setText(credits_text)
credits_label.setFont(QFont("Arial", 8))
credits_label.setFixedSize(
- QgsLayoutSize(180, 40, QgsUnitTypes.LayoutMillimeters)
+ QgsLayoutSize(160, 40, QgsUnitTypes.LayoutMillimeters)
)
# Use html mode
credits_label.setMode(QgsLayoutItemLabel.ModeHtml)
# Position the label on the current page
credits_label.attemptMove(
- QgsLayoutPoint(20, 280, QgsUnitTypes.LayoutMillimeters), page=page_number
+ QgsLayoutPoint(20, 288, QgsUnitTypes.LayoutMillimeters), page=page_number
)
+ credits_label.setHAlign(Qt.AlignCenter)
self.layout.addLayoutItem(credits_label)
def export_pdf(self, output_path):
From 2813f50e43a914dfcc36f7be552975c9b121f9bd Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sun, 9 Feb 2025 23:56:22 +0000
Subject: [PATCH 49/56] Fix aspect ratio of maps in study area report
---
geest/core/reports/study_area_report.py | 40 +++++++++++++++++++++++--
1 file changed, 37 insertions(+), 3 deletions(-)
diff --git a/geest/core/reports/study_area_report.py b/geest/core/reports/study_area_report.py
index f24d8fc0..90fd7912 100644
--- a/geest/core/reports/study_area_report.py
+++ b/geest/core/reports/study_area_report.py
@@ -12,7 +12,7 @@
QgsLayoutSize,
QgsLayoutItemPage,
QgsLayoutMeasurement,
- QgsPrintLayout,
+ QgsRectangle,
QgsLayoutItemLabel,
QgsLayoutItemMap,
QgsReadWriteContext,
@@ -314,11 +314,45 @@ def create_layout(self):
QgsLayoutPoint(20, 110, QgsUnitTypes.LayoutMillimeters),
page=current_page,
)
+ map_width_mm = 170
+ map_height_mm = 100
map_item.attemptResize(
# 170mm width x 100mm height
- QgsLayoutSize(170, 100, QgsUnitTypes.LayoutMillimeters)
+ QgsLayoutSize(
+ map_width_mm, map_height_mm, QgsUnitTypes.LayoutMillimeters
+ )
)
- map_item.setExtent(layer.extent())
+ # if the extent does not have the same aspect ratio as
+ # the map item, the extent will be expanded to fit the map item
+ # Calculate the aspect ratio of the map item
+ map_aspect_ratio = map_width_mm / map_height_mm
+
+ # Get the current extent of the layer
+ layer_extent = layer.extent()
+
+ # Calculate the aspect ratio of the layer's extent
+ layer_aspect_ratio = layer_extent.width() / layer_extent.height()
+
+ # Initialize variables for the new extent
+ new_extent = QgsRectangle(layer_extent)
+
+ # Adjust the extent to match the map item's aspect ratio
+ if layer_aspect_ratio > map_aspect_ratio:
+ # Layer is wider than the map item; adjust height
+ new_height = layer_extent.width() / map_aspect_ratio
+ height_diff = new_height - layer_extent.height()
+ new_extent.setYMinimum(layer_extent.yMinimum() - height_diff / 2)
+ new_extent.setYMaximum(layer_extent.yMaximum() + height_diff / 2)
+ else:
+ # Layer is taller than the map item; adjust width
+ new_width = layer_extent.height() * map_aspect_ratio
+ width_diff = new_width - layer_extent.width()
+ new_extent.setXMinimum(layer_extent.xMinimum() - width_diff / 2)
+ new_extent.setXMaximum(layer_extent.xMaximum() + width_diff / 2)
+
+ # Set the new extent to the map item
+ map_item.setExtent(new_extent)
+
map_item.refresh()
self.layout.addLayoutItem(map_item)
# Add a black frame around the map item
From 6e9fc2ba93cb2b4c8fd37cd4cbf65650c3f0224d Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Mon, 10 Feb 2025 08:51:06 +0000
Subject: [PATCH 50/56] Added remove pycache script
---
remove_pycache.sh | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
create mode 100755 remove_pycache.sh
diff --git a/remove_pycache.sh b/remove_pycache.sh
new file mode 100755
index 00000000..815a51ac
--- /dev/null
+++ b/remove_pycache.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+# This script recursively removes all __pycache__ directories in the 'geest' folder.
+
+TARGET_DIR="geest"
+
+# Check if the target directory exists
+if [ ! -d "$TARGET_DIR" ]; then
+ echo "Error: Directory '$TARGET_DIR' does not exist."
+ exit 1
+fi
+
+echo "Searching for __pycache__ directories in '$TARGET_DIR'..."
+
+# Find and remove all __pycache__ directories
+find "$TARGET_DIR" -type d -name "__pycache__" -exec rm -rf {} +
+
+echo "All __pycache__ directories have been removed from '$TARGET_DIR'."
+
From 6b72c185388767cc049712b009ede8fa47dab81e Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Mon, 10 Feb 2025 09:02:44 +0000
Subject: [PATCH 51/56] Added some notes about ORS key
---
config.json | 2 +-
geest/ui/ors_panel_base.ui | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/config.json b/config.json
index 1abb3fb5..abb336fd 100644
--- a/config.json
+++ b/config.json
@@ -16,7 +16,7 @@
"author": "Kartoza for and with The World Bank",
"email": "info@kartoza.com, gost@worldbank.org",
"description": "Gender Enabling Environments Spatial Tool",
- "version": "0.5.6",
+ "version": "0.5.7",
"changelog": "",
"server": false
}
diff --git a/geest/ui/ors_panel_base.ui b/geest/ui/ors_panel_base.ui
index 9a5fa360..5dc633f1 100644
--- a/geest/ui/ors_panel_base.ui
+++ b/geest/ui/ors_panel_base.ui
@@ -142,7 +142,9 @@
- This plugin makes use of the Open Route Service (ORS) platform for elements of the spatial analysis workflows. In order to use ORS, you need to obtain an API key. There is no charge to get your key. Click on [this link](https://openrouteservice.org/dev/#/signup) for the API Key sign up page. Once you have your API key, paste it into the box below.
+ This plugin makes use of the Open Route Service (ORS) platform for elements of the spatial analysis workflows. In order to use ORS, you need to obtain an API key. There is no charge to get your key. Click on [this link](https://openrouteservice.org/dev/#/signup) for the API Key sign up page. Once you have your API key, paste it into the box below.
+
+Please note that you can write to ORS for a collaborator key if you experience issues with ORS timeouts during the analysis (there is no charge for this).Qt::MarkdownText
From 8e2ec75318058d01059ffcaaaa7d1317b447e6fc Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Mon, 10 Feb 2025 23:47:43 +0000
Subject: [PATCH 52/56] More study area report descriptions and add grid to map
in report
---
config.json | 2 +-
geest/core/reports/study_area_report.py | 61 +++++++-
.../reports/templated_study_area_report.py | 132 ------------------
3 files changed, 59 insertions(+), 136 deletions(-)
delete mode 100644 geest/core/reports/templated_study_area_report.py
diff --git a/config.json b/config.json
index abb336fd..47af8436 100644
--- a/config.json
+++ b/config.json
@@ -16,7 +16,7 @@
"author": "Kartoza for and with The World Bank",
"email": "info@kartoza.com, gost@worldbank.org",
"description": "Gender Enabling Environments Spatial Tool",
- "version": "0.5.7",
+ "version": "0.5.9",
"changelog": "",
"server": false
}
diff --git a/geest/core/reports/study_area_report.py b/geest/core/reports/study_area_report.py
index 90fd7912..e64cc191 100644
--- a/geest/core/reports/study_area_report.py
+++ b/geest/core/reports/study_area_report.py
@@ -18,6 +18,9 @@
QgsReadWriteContext,
QgsLayoutExporter,
QgsVectorLayer,
+ QgsLayoutItemMapGrid,
+ QgsUnitTypes,
+ QgsCoordinateReferenceSystem,
)
from qgis.PyQt.QtXml import QDomDocument
from qgis.PyQt.QtGui import QFont, QColor
@@ -93,6 +96,26 @@ def __init__(self, gpkg_path: str, report_name="Study Area Creation Report"):
The grid is also used to perform certain types of spatial analysis such as
the Active Transport layer analyses.
"""
+ self.page_descriptions[
+ "chunks"
+ ] = """
+ The chunks are the result of splitting the study area grid into smaller
+ chunks that are used to process the study area more efficiently. Each chunk
+ is labelled as to whether it is inside, on the edge of, or outside the
+ geometry of a study area polygon. Grid cells in chunks that are 'inside' can be processed
+ more efficiently as we can skip the intersection test with the study area polygons.
+ """
+ self.page_descriptions[
+ "study_area_clip_polygons"
+ ] = """
+ The study area clip polygons are the original polygon areas but expanded so that the edges
+ of the polygon exactly coincide with the edges of the grid. This will ensure that all analysis
+ results are coherant with the grid."""
+ self.page_descriptions[
+ "study_area_creation_status"
+ ] = """
+ The study area creation status is a record of the time taken to process each part of the study area.
+ """
def __del__(self):
"""
@@ -266,15 +289,14 @@ def create_layout(self):
self.layout.addLayoutItem(title)
# Compute statistics for the current layer
try:
- stats = self.compute_statistics(layer)
summary_text = f"Layer: {layer_name}\n"
+ stats = self.compute_statistics(layer)
# feature_count = 0
# for area_name, count in stats["area_counts"].items():
# feature_count += count
summary_text += f"Total count: {stats['total_count']} features"
except Exception as e:
log_message(f"Error computing statistics for layer '{layer_name}': {e}")
- continue
description_text = self.page_descriptions.get(layer_name, "")
# Add description label to the current page
@@ -326,6 +348,38 @@ def create_layout(self):
# the map item, the extent will be expanded to fit the map item
# Calculate the aspect ratio of the map item
map_aspect_ratio = map_width_mm / map_height_mm
+ # ---------------------------
+ # Set up a grid over the map
+ # ---------------------------
+ # Create a new map grid for the map item
+ grid = QgsLayoutItemMapGrid("Grid 1", map_item)
+ grid.setEnabled(True)
+ grid.setCrs(QgsCoordinateReferenceSystem("EPSG:4326"))
+
+ # Specify that the grid is a graticule (i.e. based on geographic coordinates)
+ # grid.setGridType(QgsLayoutItemMapGrid.Graticule)
+
+ # Define a grid interval of 1 degree.
+ grid.setIntervalX(1)
+ grid.setIntervalY(1)
+
+ grid.setAnnotationDirection(
+ QgsLayoutItemMapGrid.Vertical, QgsLayoutItemMapGrid.Bottom
+ )
+ grid.setAnnotationDirection(
+ QgsLayoutItemMapGrid.Vertical, QgsLayoutItemMapGrid.Top
+ )
+
+ # (Optional) Enable and configure annotations for the grid lines
+ grid.setAnnotationEnabled(True)
+ # Example format: degrees and minutes (you can customize this format as needed)
+ # grid.setAnnotationFormat("dd° mm'")
+
+ # Add the grid to the map item. The map_item.grids() returns a list;
+ # append our configured grid to it.
+ map_item.grids().addGrid(grid)
+
+ # If needed, refresh or update your layout to see the grid applied.
# Get the current extent of the layer
layer_extent = layer.extent()
@@ -413,7 +467,7 @@ def add_header_and_footer(self, page_number):
def export_pdf(self, output_path):
"""
- Export the current layout as a PDF file.
+ Export the current layout as a PDF file in raster mode.
Parameters:
output_path (str): The full file path (including filename) for the output PDF.
@@ -424,6 +478,7 @@ def export_pdf(self, output_path):
if self.layout is None:
self.create_layout()
export_settings = QgsLayoutExporter.PdfExportSettings()
+ export_settings.rasterizeWholeImage = True
exporter = QgsLayoutExporter(self.layout)
result = exporter.exportToPdf(output_path, export_settings)
return result == QgsLayoutExporter.Success
diff --git a/geest/core/reports/templated_study_area_report.py b/geest/core/reports/templated_study_area_report.py
deleted file mode 100644
index 2f168ce6..00000000
--- a/geest/core/reports/templated_study_area_report.py
+++ /dev/null
@@ -1,132 +0,0 @@
-from qgis.core import (
- QgsProject,
- QgsPrintLayout,
- QgsLayoutItemLabel,
- QgsLayoutItemMap,
- QgsReadWriteContext,
- QgsLayoutExporter,
- QgsVectorLayer,
-)
-from qgis.PyQt.QtXml import QDomDocument
-from qgis.PyQt.QtGui import QFont, QColor
-
-
-class TemplatedStudyAreaReport:
- """
- A class to manage QGIS Print Layouts by loading templates, modifying label text,
- setting map layers, and exporting the final layout to a PDF.
-
- Attributes:
- project (QgsProject): The QGIS project instance.
- layout (QgsPrintLayout): The print layout instance.
- """
-
- def __init__(self, template_path):
- """
- Initializes the QGISLayoutManager with a specified template.
-
- Args:
- template_path (str): The file path to the QPT template.
-
- Raises:
- FileNotFoundError: If the template file does not exist or cannot be read.
- ValueError: If the template content is invalid or cannot be loaded.
- """
- self.layout = QgsPrintLayout(self.project)
- self.layout.initializeDefaults()
-
- # Load the QPT template
- try:
- with open(template_path, "r") as template_file:
- template_content = template_file.read()
- except IOError:
- raise FileNotFoundError(
- f"Template file '{template_path}' not found or cannot be read."
- )
-
- document = QDomDocument()
- if not document.setContent(template_content):
- raise ValueError(
- f"Failed to parse the template content from '{template_path}'."
- )
-
- context = QgsReadWriteContext()
- if not self.layout.loadFromTemplate(document, context):
- raise ValueError(
- f"Failed to load the template into the layout from '{template_path}'."
- )
-
- def set_label_text(self, label_id, text, font_name="Arial", font_size=12):
- """
- Sets the text and font properties of a label item in the layout.
-
- Args:
- label_id (str): The ID of the label item in the template.
- text (str): The text to set for the label.
- font_name (str, optional): The font family for the label text. Defaults to "Arial".
- font_size (int, optional): The font size for the label text. Defaults to 12.
-
- Raises:
- KeyError: If the label item with the specified ID is not found in the layout.
- TypeError: If the item with the specified ID is not a QgsLayoutItemLabel.
- """
- label = self.layout.itemById(label_id)
- if label is None:
- raise KeyError(f"Label item with ID '{label_id}' not found in the layout.")
- if not isinstance(label, QgsLayoutItemLabel):
- raise TypeError(f"Item with ID '{label_id}' is not a QgsLayoutItemLabel.")
-
- label.setText(text)
- label.setFont(QFont(font_name, font_size))
- label.adjustSizeToText()
-
- def set_map_layers(self, map_item_id, layer_paths):
- """
- Sets the layers for a map item in the layout based on provided file paths.
-
- Args:
- map_item_id (str): The ID of the map item in the template.
- layer_paths (list of str): A list of file paths to the vector layers to be added.
-
- Raises:
- KeyError: If the map item with the specified ID is not found in the layout.
- TypeError: If the item with the specified ID is not a QgsLayoutItemMap.
- ValueError: If any of the provided layer paths are invalid or the layers cannot be loaded.
- """
- map_item = self.layout.itemById(map_item_id)
- if map_item is None:
- raise KeyError(f"Map item with ID '{map_item_id}' not found in the layout.")
- if not isinstance(map_item, QgsLayoutItemMap):
- raise TypeError(f"Item with ID '{map_item_id}' is not a QgsLayoutItemMap.")
-
- layers = []
- for path in layer_paths:
- layer = QgsVectorLayer(path, path.split("/")[-1], "ogr")
- if not layer.isValid():
- raise ValueError(f"Failed to load layer from path '{path}'.")
- self.project.addMapLayer(layer)
- layers.append(layer)
-
- map_item.setLayers(layers)
- if layers:
- map_item.setExtent(layers[0].extent())
- map_item.setFrameEnabled(True)
- map_item.setFrameStrokeColor(QColor(0, 0, 0))
- map_item.setFrameStrokeWidth(0.5)
- map_item.refresh()
-
- def export_to_pdf(self, output_path):
- """
- Exports the current layout to a PDF file.
-
- Args:
- output_path (str): The file path where the PDF will be saved.
-
- Returns:
- bool: True if the export is successful, False otherwise.
- """
- exporter = QgsLayoutExporter(self.layout)
- result = exporter.exportToPdf(
- output_path, QgsLayoutExporter.PdfExportSettings()
- )
- return result == QgsLayoutExporter.Success
From 815b50995e7dbc0b383fa6e786704a663bf95092 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Fri, 14 Feb 2025 22:05:19 +0000
Subject: [PATCH 53/56] Fix merge conflicts
---
geest/core/workflow_queue.py | 2 +-
geest/core/workflow_queue_manager.py | 2 +-
geest/gui/panels/create_project_panel.py | 2 +-
geest/gui/panels/credits_panel.py | 2 +-
geest/gui/panels/help_panel.py | 4 ++--
geest/gui/panels/intro_panel.py | 2 +-
geest/gui/panels/open_project_panel.py | 6 +++---
geest/gui/panels/ors_panel.py | 2 +-
geest/gui/panels/setup_panel.py | 4 ++--
task.py | 2 +-
test/test_json_tree_item.py | 4 ++--
11 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/geest/core/workflow_queue.py b/geest/core/workflow_queue.py
index e68c5be5..201f9f29 100644
--- a/geest/core/workflow_queue.py
+++ b/geest/core/workflow_queue.py
@@ -1,6 +1,6 @@
from functools import partial
from qgis.core import QgsApplication
-from PyQt5.QtCore import QObject, pyqtSignal
+from PyQt.QtCore import QObject, pyqtSignal
from typing import List, Optional
from .workflow_job import WorkflowJob
diff --git a/geest/core/workflow_queue_manager.py b/geest/core/workflow_queue_manager.py
index eb166f0f..a6cd6cdb 100644
--- a/geest/core/workflow_queue_manager.py
+++ b/geest/core/workflow_queue_manager.py
@@ -1,4 +1,4 @@
-from PyQt5.QtCore import QObject, pyqtSignal
+from PyQt.QtCore import QObject, pyqtSignal
from qgis.core import Qgis, QgsTask, QgsProcessingContext, QgsProject
from .workflow_queue import WorkflowQueue
from .workflow_job import WorkflowJob
diff --git a/geest/gui/panels/create_project_panel.py b/geest/gui/panels/create_project_panel.py
index 12e6a13a..7b07c0b6 100644
--- a/geest/gui/panels/create_project_panel.py
+++ b/geest/gui/panels/create_project_panel.py
@@ -2,7 +2,7 @@
import json
import shutil
import traceback
-from PyQt5.QtWidgets import (
+from PyQt.QtWidgets import (
QWidget,
QFileDialog,
QMessageBox,
diff --git a/geest/gui/panels/credits_panel.py b/geest/gui/panels/credits_panel.py
index d0a04278..8378a716 100644
--- a/geest/gui/panels/credits_panel.py
+++ b/geest/gui/panels/credits_panel.py
@@ -1,4 +1,4 @@
-from PyQt5.QtWidgets import (
+from PyQt.QtWidgets import (
QWidget,
)
from qgis.core import Qgis
diff --git a/geest/gui/panels/help_panel.py b/geest/gui/panels/help_panel.py
index ac0d9828..68c13323 100644
--- a/geest/gui/panels/help_panel.py
+++ b/geest/gui/panels/help_panel.py
@@ -7,10 +7,10 @@
QSizePolicy,
)
from qgis.PyQt.QtCore import QUrl, Qt, pyqtSignal
-from PyQt5.QtGui import QDesktopServices
+from PyQt.QtGui import QDesktopServices
try:
- from PyQt5.QtWebEngineWidgets import QWebEngineView
+ from PyQt.QtWebEngineWidgets import QWebEngineView
web_engine_available = True
except ImportError:
diff --git a/geest/gui/panels/intro_panel.py b/geest/gui/panels/intro_panel.py
index 842d265e..a81a7db5 100644
--- a/geest/gui/panels/intro_panel.py
+++ b/geest/gui/panels/intro_panel.py
@@ -1,4 +1,4 @@
-from PyQt5.QtWidgets import (
+from PyQt.QtWidgets import (
QWidget,
)
from qgis.core import Qgis
diff --git a/geest/gui/panels/open_project_panel.py b/geest/gui/panels/open_project_panel.py
index bf5ddc5e..6e140035 100644
--- a/geest/gui/panels/open_project_panel.py
+++ b/geest/gui/panels/open_project_panel.py
@@ -1,7 +1,7 @@
import os
-from PyQt5.QtWidgets import QWidget, QFileDialog, QMessageBox, QComboBox
-from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QFontMetrics
+from PyQt.QtWidgets import QWidget, QFileDialog, QMessageBox, QComboBox
+from PyQt.QtCore import Qt
+from PyQt.QtGui import QFontMetrics
from qgis.core import (
Qgis,
)
diff --git a/geest/gui/panels/ors_panel.py b/geest/gui/panels/ors_panel.py
index 96c0eec3..94c6f440 100644
--- a/geest/gui/panels/ors_panel.py
+++ b/geest/gui/panels/ors_panel.py
@@ -1,4 +1,4 @@
-from PyQt5.QtWidgets import (
+from PyQt.QtWidgets import (
QWidget,
)
from qgis.PyQt.QtCore import QUrl, pyqtSignal
diff --git a/geest/gui/panels/setup_panel.py b/geest/gui/panels/setup_panel.py
index 42f6b5bd..4798c364 100644
--- a/geest/gui/panels/setup_panel.py
+++ b/geest/gui/panels/setup_panel.py
@@ -1,8 +1,8 @@
-from PyQt5.QtWidgets import (
+from PyQt.QtWidgets import (
QWidget,
)
from qgis.PyQt.QtCore import pyqtSignal
-from PyQt5.QtGui import QFont
+from PyQt.QtGui import QFont
from geest.utilities import (
get_ui_class,
resources_path,
diff --git a/task.py b/task.py
index 0ed10641..1f4a8990 100644
--- a/task.py
+++ b/task.py
@@ -10,7 +10,7 @@
"""
from qgis.core import QgsTask, QgsMessageLog, Qgis
-from PyQt5.QtCore import pyqtSignal
+from PyQt.QtCore import pyqtSignal
import os
diff --git a/test/test_json_tree_item.py b/test/test_json_tree_item.py
index 9ddea257..e8a6643c 100644
--- a/test/test_json_tree_item.py
+++ b/test/test_json_tree_item.py
@@ -1,6 +1,6 @@
import unittest
-from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QColor
+from PyQt.QtCore import Qt
+from PyQt.QtGui import QColor
from uuid import UUID
from geest.core.json_tree_item import JsonTreeItem
From d741a78474081db3fdf881491ac83ab8bfc4f221 Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Fri, 14 Feb 2025 23:40:15 +0000
Subject: [PATCH 54/56] Show version in start logs
---
geest/__init__.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/geest/__init__.py b/geest/__init__.py
index 34096155..eda3d3b0 100644
--- a/geest/__init__.py
+++ b/geest/__init__.py
@@ -36,7 +36,7 @@
# Import your plugin components here
from .core import setting # , JSONValidator
-from .utilities import resources_path, log_message
+from .utilities import resources_path, log_message, version
from .gui import GeestOptionsFactory, GeestDock
import datetime
import logging
@@ -61,6 +61,8 @@
date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message(f"»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»", force=True)
log_message(f"Geest2 started at {date}", force=True)
+version = version()
+log_message(f"Geest Version: {version}")
log_message(f"Logging output to: {log_file_path}", force=True)
log_message(f"log_path_env: {log_path_env}", force=True)
log_message(f"»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»", force=True)
From 754d01fa8193a8f1af6df6beba95fd92d6e2c51e Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sat, 15 Feb 2025 01:25:14 +0000
Subject: [PATCH 55/56] Fix transparency in windows
---
admin.py | 20 +++++++++++++++-----
geest/core/workflow_queue.py | 2 +-
geest/core/workflow_queue_manager.py | 2 +-
geest/gui/panels/create_project_panel.py | 2 +-
geest/gui/panels/credits_panel.py | 2 +-
geest/gui/panels/help_panel.py | 4 ++--
geest/gui/panels/intro_panel.py | 2 +-
geest/gui/panels/open_project_panel.py | 6 +++---
geest/gui/panels/ors_panel.py | 2 +-
geest/gui/panels/setup_panel.py | 4 ++--
geest/ui/create_project_panel_base.ui | 10 +++++-----
geest/ui/credits_panel_base.ui | 11 ++++++++++-
geest/ui/intro_panel_base.ui | 10 ++++++++--
geest/ui/ors_panel_base.ui | 5 ++++-
task.py | 2 +-
test/test_json_tree_item.py | 4 ++--
16 files changed, 58 insertions(+), 30 deletions(-)
diff --git a/admin.py b/admin.py
index dc551607..beb50494 100644
--- a/admin.py
+++ b/admin.py
@@ -79,12 +79,22 @@ def install(context: typer.Context, build_src: bool = True):
build(context, clean=True) if build_src else LOCAL_ROOT_DIR / "build" / SRC_NAME
)
- root_directory = (
- Path.home() / f".local/share/QGIS/QGIS3/profiles/"
- f"{context.obj['qgis_profile']}"
- )
+ # For windows root dir in in AppData
+ if os.name == "nt":
+ print("User profile:")
+ print(os.environ["USERPROFILE"])
+ plugin_path = os.path.join(
+
+ "AppData", "Roaming", "QGIS", "QGIS3", "profiles", "default",
+ )
+ root_directory = os.environ["USERPROFILE"] + "\\" + plugin_path
+ else:
+ root_directory = (
+ Path.home() / f".local/share/QGIS/QGIS3/profiles/"
+ f"{context.obj['qgis_profile']}"
+ )
- base_target_directory = root_directory / "python/plugins" / SRC_NAME
+ base_target_directory = os.path.join(root_directory, "python/plugins", SRC_NAME)
_log(f"Copying built plugin to {base_target_directory}...", context=context)
shutil.copytree(built_directory, base_target_directory)
_log(
diff --git a/geest/core/workflow_queue.py b/geest/core/workflow_queue.py
index 201f9f29..e68c5be5 100644
--- a/geest/core/workflow_queue.py
+++ b/geest/core/workflow_queue.py
@@ -1,6 +1,6 @@
from functools import partial
from qgis.core import QgsApplication
-from PyQt.QtCore import QObject, pyqtSignal
+from PyQt5.QtCore import QObject, pyqtSignal
from typing import List, Optional
from .workflow_job import WorkflowJob
diff --git a/geest/core/workflow_queue_manager.py b/geest/core/workflow_queue_manager.py
index a6cd6cdb..eb166f0f 100644
--- a/geest/core/workflow_queue_manager.py
+++ b/geest/core/workflow_queue_manager.py
@@ -1,4 +1,4 @@
-from PyQt.QtCore import QObject, pyqtSignal
+from PyQt5.QtCore import QObject, pyqtSignal
from qgis.core import Qgis, QgsTask, QgsProcessingContext, QgsProject
from .workflow_queue import WorkflowQueue
from .workflow_job import WorkflowJob
diff --git a/geest/gui/panels/create_project_panel.py b/geest/gui/panels/create_project_panel.py
index 7b07c0b6..12e6a13a 100644
--- a/geest/gui/panels/create_project_panel.py
+++ b/geest/gui/panels/create_project_panel.py
@@ -2,7 +2,7 @@
import json
import shutil
import traceback
-from PyQt.QtWidgets import (
+from PyQt5.QtWidgets import (
QWidget,
QFileDialog,
QMessageBox,
diff --git a/geest/gui/panels/credits_panel.py b/geest/gui/panels/credits_panel.py
index 8378a716..d0a04278 100644
--- a/geest/gui/panels/credits_panel.py
+++ b/geest/gui/panels/credits_panel.py
@@ -1,4 +1,4 @@
-from PyQt.QtWidgets import (
+from PyQt5.QtWidgets import (
QWidget,
)
from qgis.core import Qgis
diff --git a/geest/gui/panels/help_panel.py b/geest/gui/panels/help_panel.py
index 68c13323..ac0d9828 100644
--- a/geest/gui/panels/help_panel.py
+++ b/geest/gui/panels/help_panel.py
@@ -7,10 +7,10 @@
QSizePolicy,
)
from qgis.PyQt.QtCore import QUrl, Qt, pyqtSignal
-from PyQt.QtGui import QDesktopServices
+from PyQt5.QtGui import QDesktopServices
try:
- from PyQt.QtWebEngineWidgets import QWebEngineView
+ from PyQt5.QtWebEngineWidgets import QWebEngineView
web_engine_available = True
except ImportError:
diff --git a/geest/gui/panels/intro_panel.py b/geest/gui/panels/intro_panel.py
index a81a7db5..842d265e 100644
--- a/geest/gui/panels/intro_panel.py
+++ b/geest/gui/panels/intro_panel.py
@@ -1,4 +1,4 @@
-from PyQt.QtWidgets import (
+from PyQt5.QtWidgets import (
QWidget,
)
from qgis.core import Qgis
diff --git a/geest/gui/panels/open_project_panel.py b/geest/gui/panels/open_project_panel.py
index 6e140035..bf5ddc5e 100644
--- a/geest/gui/panels/open_project_panel.py
+++ b/geest/gui/panels/open_project_panel.py
@@ -1,7 +1,7 @@
import os
-from PyQt.QtWidgets import QWidget, QFileDialog, QMessageBox, QComboBox
-from PyQt.QtCore import Qt
-from PyQt.QtGui import QFontMetrics
+from PyQt5.QtWidgets import QWidget, QFileDialog, QMessageBox, QComboBox
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QFontMetrics
from qgis.core import (
Qgis,
)
diff --git a/geest/gui/panels/ors_panel.py b/geest/gui/panels/ors_panel.py
index 94c6f440..96c0eec3 100644
--- a/geest/gui/panels/ors_panel.py
+++ b/geest/gui/panels/ors_panel.py
@@ -1,4 +1,4 @@
-from PyQt.QtWidgets import (
+from PyQt5.QtWidgets import (
QWidget,
)
from qgis.PyQt.QtCore import QUrl, pyqtSignal
diff --git a/geest/gui/panels/setup_panel.py b/geest/gui/panels/setup_panel.py
index 4798c364..42f6b5bd 100644
--- a/geest/gui/panels/setup_panel.py
+++ b/geest/gui/panels/setup_panel.py
@@ -1,8 +1,8 @@
-from PyQt.QtWidgets import (
+from PyQt5.QtWidgets import (
QWidget,
)
from qgis.PyQt.QtCore import pyqtSignal
-from PyQt.QtGui import QFont
+from PyQt5.QtGui import QFont
from geest.utilities import (
get_ui_class,
resources_path,
diff --git a/geest/ui/create_project_panel_base.ui b/geest/ui/create_project_panel_base.ui
index bcf64b1e..d43cc902 100644
--- a/geest/ui/create_project_panel_base.ui
+++ b/geest/ui/create_project_panel_base.ui
@@ -7,7 +7,7 @@
00604
- 938
+ 1009
@@ -344,14 +344,14 @@
- QgsMapLayerComboBox
+ QgsFieldComboBoxQComboBox
- qgis.gui
+ qgsfieldcombobox.h
- QgsFieldComboBox
+ QgsMapLayerComboBoxQComboBox
- qgis.gui
+ qgsmaplayercombobox.h
diff --git a/geest/ui/credits_panel_base.ui b/geest/ui/credits_panel_base.ui
index a44ed7fe..54be50a4 100644
--- a/geest/ui/credits_panel_base.ui
+++ b/geest/ui/credits_panel_base.ui
@@ -154,6 +154,9 @@
+
+ background-color: rgba(0, 0, 0, 0);
+ QFrame::NoFrame
@@ -166,9 +169,12 @@
00542
- 629
+ 643
+
+ background-color: rgba(0, 0, 0, 0);
+ 0
@@ -190,6 +196,9 @@
0
+
+ background-color: rgba(0, 0, 0, 0);
+ This plugin is built with support from the **Canada Clean Energy and Forest Climate Facility (CCEFCFy)**, the **Geospatial Operational Support Team (GOST, DECSC)** for the project Geospatial Assessment of Women Employment and Business Opportunities in the Renewable Energy Sector.
diff --git a/geest/ui/intro_panel_base.ui b/geest/ui/intro_panel_base.ui
index 4549db4b..ef8b20f5 100644
--- a/geest/ui/intro_panel_base.ui
+++ b/geest/ui/intro_panel_base.ui
@@ -78,7 +78,7 @@
- <html><head/><body><p align="center"><span style=" font-size:16pt; font-weight:600;">Welcome to GEEST</span></p></body></html>
+ <html><head/><body><p align="center"><span style=" font-size:16pt; font-weight:600;">Welcome to GEEST22</span></p></body></html>Qt::RichText
@@ -93,6 +93,9 @@
+
+ background-color: rgba(0, 0, 0, 0);
+ QFrame::NoFrame
@@ -105,7 +108,7 @@
00556
- 655
+ 664
@@ -123,6 +126,9 @@
+
+ background-color: rgba(0, 0, 0, 0);
+ The Gender Enabling Environments Spatial Tool (GEEST), developed by the **World Bank**, evaluates locations based on how supportive they are of women’s employment and business opportunities.
diff --git a/geest/ui/ors_panel_base.ui b/geest/ui/ors_panel_base.ui
index 5dc633f1..42a1380a 100644
--- a/geest/ui/ors_panel_base.ui
+++ b/geest/ui/ors_panel_base.ui
@@ -117,6 +117,9 @@
+
+ background-color: rgba(0, 0, 0, 0);
+ QFrame::NoFrame
@@ -129,7 +132,7 @@
00612
- 655
+ 664
diff --git a/task.py b/task.py
index 1f4a8990..0ed10641 100644
--- a/task.py
+++ b/task.py
@@ -10,7 +10,7 @@
"""
from qgis.core import QgsTask, QgsMessageLog, Qgis
-from PyQt.QtCore import pyqtSignal
+from PyQt5.QtCore import pyqtSignal
import os
diff --git a/test/test_json_tree_item.py b/test/test_json_tree_item.py
index e8a6643c..9ddea257 100644
--- a/test/test_json_tree_item.py
+++ b/test/test_json_tree_item.py
@@ -1,6 +1,6 @@
import unittest
-from PyQt.QtCore import Qt
-from PyQt.QtGui import QColor
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QColor
from uuid import UUID
from geest.core.json_tree_item import JsonTreeItem
From 01e9068c04335fb7b89f8ded86cc1f79f86e906e Mon Sep 17 00:00:00 2001
From: Tim Sutton
Date: Sat, 15 Feb 2025 11:17:37 +0000
Subject: [PATCH 56/56] Windows pdf open fix
---
geest/gui/panels/create_project_panel.py | 4 +---
geest/gui/panels/tree_panel.py | 4 +---
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/geest/gui/panels/create_project_panel.py b/geest/gui/panels/create_project_panel.py
index 12e6a13a..dc1787dc 100644
--- a/geest/gui/panels/create_project_panel.py
+++ b/geest/gui/panels/create_project_panel.py
@@ -264,9 +264,7 @@ def on_task_completed(self):
# open the pdf using the system PDF viewer
# Windows
if os.name == "nt": # Windows
- os.system(
- f'start "{os.path.join(self.working_dir, "study_area_report.pdf")}"'
- )
+ os.startfile(os.path.join(self.working_directory, "study_area_report.pdf"))
else: # macOS and Linux
system = platform.system().lower()
if system == "darwin": # macOS
diff --git a/geest/gui/panels/tree_panel.py b/geest/gui/panels/tree_panel.py
index 7734e3c3..e6d05d08 100644
--- a/geest/gui/panels/tree_panel.py
+++ b/geest/gui/panels/tree_panel.py
@@ -660,9 +660,7 @@ def generate_study_area_report(self):
# open the pdf using the system PDF viewer
# Windows
if os.name == "nt": # Windows
- os.system(
- f'start "{os.path.join(self.working_directory, "study_area_report.pdf")}"'
- )
+ os.startfile(os.path.join(self.working_directory, "study_area_report.pdf"))
else: # macOS and Linux
system = platform.system().lower()
if system == "darwin": # macOS