Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

actions update #29

Merged
merged 3 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,45 @@ name: Python package
on:
push:
branches: [ "main" ]

pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
pip install --upgrade pip
pip install pip-tools
pip-sync requirements-dev.txt

- name: Black code style check
run: |
# stop the build if there are files that need to reformatted
black --check .

- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Test with pytest
run: |
pytest
142 changes: 88 additions & 54 deletions brdr/aligner.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,16 @@
from brdr.geometry_utils import safe_intersection
from brdr.geometry_utils import safe_symmetric_difference
from brdr.geometry_utils import safe_union
from brdr.utils import diffs_from_dict_series, get_breakpoints_zerostreak, \
filter_resulting_series_by_key, get_collection, geojson_tuple_from_series, write_geojson, \
merge_geometries_by_theme_id, geojson_from_dict
from brdr.utils import (
diffs_from_dict_series,
get_breakpoints_zerostreak,
filter_resulting_series_by_key,
get_collection,
geojson_tuple_from_series,
write_geojson,
merge_geometries_by_theme_id,
geojson_from_dict,
)

logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(message)s", datefmt="%d-%b-%y %H:%M:%S"
Expand All @@ -69,7 +76,7 @@ def __init__(
relevant_distance=1,
threshold_overlap_percentage=50,
od_strategy=OpenbaarDomeinStrategy.SNAP_SINGLE_SIDE,
crs=DEFAULT_CRS
crs=DEFAULT_CRS,
):
"""
Initializes the Aligner object
Expand Down Expand Up @@ -104,16 +111,22 @@ def __init__(
# reference
self.reference_input = None # to save the initially loaded geojson

self.name_reference_id = "ref_identifier" # name of the identifier-field of the reference data (id has to be unique,f.e CAPAKEY for GRB-parcels)
self.name_reference_id = "ref_identifier" # name of the identifier-field of the reference data (id has to be unique,f.e CAPAKEY for GRB-parcels)
self.dict_reference = {} # dictionary to store all reference geometries
self.reference_union = None # to save a unioned geometry of all reference polygons; needed for calculation in most OD-strategies
self.reference_union = None # to save a unioned geometry of all reference polygons; needed for calculation in most OD-strategies

# output-dictionaries (when processing dict_thematic)
self.dict_result = None # dictionary to save resulting geometries
self.dict_result_diff = None # dictionary to save global resulting differences
self.dict_result_diff_plus = None # dictionary to save positive resulting differences
self.dict_result_diff_min = None # dictionary to save negative resulting differences
self.dict_relevant_intersection = None # dictionary to save relevant_intersections
self.dict_result_diff_plus = (
None # dictionary to save positive resulting differences
)
self.dict_result_diff_min = (
None # dictionary to save negative resulting differences
)
self.dict_relevant_intersection = (
None # dictionary to save relevant_intersections
)
self.dict_relevant_difference = None # dictionary to save relevant_differences

# Coordinate reference system
Expand Down Expand Up @@ -445,7 +458,9 @@ def process_dict_thematic(
self.dict_result_diff = merge_geometries_by_theme_id(dict_result_diff)
self.dict_result_diff_plus = merge_geometries_by_theme_id(dict_result_diff_plus)
self.dict_result_diff_min = merge_geometries_by_theme_id(dict_result_diff_min)
self.dict_relevant_intersection = merge_geometries_by_theme_id(dict_relevant_intersection)
self.dict_relevant_intersection = merge_geometries_by_theme_id(
dict_relevant_intersection
)
self.dict_relevant_difference = merge_geometries_by_theme_id(dict_relevant_diff)
self.feedback_info("thematic dictionary processed")
return (
Expand All @@ -458,23 +473,23 @@ def process_dict_thematic(
)

def predictor(
self,
relevant_distances=np.arange(0, 300, 10, dtype=int)/100,
od_strategy=OpenbaarDomeinStrategy.SNAP_SINGLE_SIDE,
treshold_overlap_percentage=50,
self,
relevant_distances=np.arange(0, 300, 10, dtype=int) / 100,
od_strategy=OpenbaarDomeinStrategy.SNAP_SINGLE_SIDE,
treshold_overlap_percentage=50,
):
"""
Predicts the 'most interesting' relevant distances for changes in thematic elements based on a distance series.

This function analyzes a set of thematic geometries (`self.dict_thematic`) to identify potentially
This function analyzes a set of thematic geometries (`self.dict_thematic`) to identify potentially
interesting distances where changes occur. It performs the following steps:

1. **Process Distance Series:**
- Calculates a series of results for different distances specified by `relevant_distances`.
- This calculation might involve functions like `self.process_series` (implementation details likely depend on your specific code).

2. **Calculate Difference Metrics:**
- Analyzes the results from the distance series to compute difference metrics
- Analyzes the results from the distance series to compute difference metrics
between thematic elements at each distance (using `diffs_from_dict_series`).

3. **Identify Breakpoints and Zero-Streaks:**
Expand Down Expand Up @@ -506,17 +521,25 @@ def predictor(
"""
dict_predicted = {}
for key in self.dict_thematic.keys():
dict_predicted[key]={}
dict_series = self.process_series(relevant_distances=relevant_distances,od_strategy=od_strategy,treshold_overlap_percentage=treshold_overlap_percentage)
dict_predicted[key] = {}
dict_series = self.process_series(
relevant_distances=relevant_distances,
od_strategy=od_strategy,
treshold_overlap_percentage=treshold_overlap_percentage,
)
diffs = diffs_from_dict_series(dict_series, self.dict_thematic)
for key in diffs:
if len(diffs[key]) == len(relevant_distances):
lst_diffs = list(diffs[key].values())
breakpoints, zero_streaks = get_breakpoints_zerostreak(relevant_distances, lst_diffs)
breakpoints, zero_streaks = get_breakpoints_zerostreak(
relevant_distances, lst_diffs
)
logging.debug(str(key))
for zs in zero_streaks:
dict_predicted[key][zs[0]] = dict_series[zs[0]]
dict_predicted[key] = filter_resulting_series_by_key(dict_predicted[key],key)
dict_predicted[key] = filter_resulting_series_by_key(
dict_predicted[key], key
)
return dict_predicted, diffs

def process_series(
Expand Down Expand Up @@ -549,7 +572,7 @@ def process_series(
self.feedback_debug("Process series" + str(relevant_distances))
self.od_strategy = od_strategy
self.threshold_overlap_percentage = treshold_overlap_percentage
#self._prepare_thematic_data() #not necessary? Assumed that dict_thematic is already loaded
# self._prepare_thematic_data() #not necessary? Assumed that dict_thematic is already loaded
dict_series = {}
for s in relevant_distances:
self.feedback_info(
Expand Down Expand Up @@ -649,7 +672,7 @@ def get_last_version_date(self, geometry, grb_type=GRBType.ADP):
"https://geo.api.vlaanderen.be/GRB/ogc/features/collections/"
+ grb_type
+ "/items?"
"limit=" + str(limit) + "&crs=" + crs + "&bbox-crs=" + crs +"&bbox=" + bbox
"limit=" + str(limit) + "&crs=" + crs + "&bbox-crs=" + crs + "&bbox=" + bbox
)
update_dates = []
collection = get_collection(actual_url, limit)
Expand All @@ -664,48 +687,60 @@ def get_results_as_dict(self):
"""
get a dict-tuple of the results
"""
return (self.dict_result, self.dict_result_diff, self.dict_result_diff_plus, self.dict_result_diff_min,
self.dict_relevant_intersection, self.dict_relevant_difference)
return (
self.dict_result,
self.dict_result_diff,
self.dict_result_diff_plus,
self.dict_result_diff_min,
self.dict_relevant_intersection,
self.dict_relevant_difference,
)

def get_results_as_geojson(self):
"""
get a geojson-tuple of the results
"""
return geojson_tuple_from_series({self.relevant_distance: self.get_results_as_dict()}, self.CRS,
self.name_thematic_id)
return geojson_tuple_from_series(
{self.relevant_distance: self.get_results_as_dict()},
self.CRS,
self.name_thematic_id,
)

def get_reference_as_geojson(self):
"""
get a geojson of the reference polygons
"""
return geojson_from_dict(self.dict_reference, self.CRS, self.name_reference_id,geom_attributes=False)
return geojson_from_dict(
self.dict_reference, self.CRS, self.name_reference_id, geom_attributes=False
)

def export_results(self, path, multi_to_single=True):
"""
Exports analysis results as GeoJSON files.

This function exports 6 GeoJSON files containing the analysis results to the specified `path`.

Args:
path (str): The path to the directory where the GeoJSON files will be saved.
multi_to_single (bool, optional): If True (default), converts MultiPolygon geometries to single Polygons
in the exported GeoJSON files. This can be useful for visualization purposes.

Details of exported files:
- result.geojson: Contains the original thematic data from `self.dict_result`.
- result_diff.geojson: Contains the difference between the original and predicted data from `self.dict_result_diff`.
- result_diff_plus.geojson: Contains results for areas that are added (increased area).
- result_diff_min.geojson: Contains results for areas that are removed (decreased area).
- result_relevant_intersection.geojson: Contains the areas with relevant intersection that has to be included in the result.
- result_relevant_difference.geojson: Contains the areas with relevant difference that has to be excluded from the result.
"""
Exports analysis results as GeoJSON files.

This function exports 6 GeoJSON files containing the analysis results to the specified `path`.

Args:
path (str): The path to the directory where the GeoJSON files will be saved.
multi_to_single (bool, optional): If True (default), converts MultiPolygon geometries to single Polygons
in the exported GeoJSON files. This can be useful for visualization purposes.

Details of exported files:
- result.geojson: Contains the original thematic data from `self.dict_result`.
- result_diff.geojson: Contains the difference between the original and predicted data from `self.dict_result_diff`.
- result_diff_plus.geojson: Contains results for areas that are added (increased area).
- result_diff_min.geojson: Contains results for areas that are removed (decreased area).
- result_relevant_intersection.geojson: Contains the areas with relevant intersection that has to be included in the result.
- result_relevant_difference.geojson: Contains the areas with relevant difference that has to be excluded from the result.
"""
fcs = self.get_results_as_geojson()
resultnames = [
"result.geojson",
"result_diff.geojson",
"result_diff_plus.geojson",
"result_diff_min.geojson",
"result_relevant_intersection.geojson",
"result_relevant_difference.geojson"
"result_relevant_difference.geojson",
]
for count, fc in enumerate(fcs):
write_geojson(os.path.join(path, resultnames[count]), fcs[count])
Expand Down Expand Up @@ -1007,14 +1042,14 @@ def _calculate_geom_by_intersection_and_reference(
self.buffer_distance(),
),
)
#TODO BEGIN: experimental fix - check if it is ok in all cases?
#when calculating for OD, we create a 'virtual parcel'. When calculating this virtual parcel, it is buffered to take outer boundaries into account.
#This results in a side-effect that there are extra non-logical parts included in the result. The function below tries to exclude these non-logica parts.
# TODO BEGIN: experimental fix - check if it is ok in all cases?
# when calculating for OD, we create a 'virtual parcel'. When calculating this virtual parcel, it is buffered to take outer boundaries into account.
# This results in a side-effect that there are extra non-logical parts included in the result. The function below tries to exclude these non-logica parts.
# see eo_id 206363 with relevant distance=0.2m and SNAP_ALL_SIDE
if is_openbaar_domein:
#geom = buffer_neg_pos(geom, self.buffer_distance())
geom = self.get_relevant_polygons_from_geom (geom)
#TODO END
# geom = buffer_neg_pos(geom, self.buffer_distance())
geom = self.get_relevant_polygons_from_geom(geom)
# TODO END
elif (
not geom_relevant_intersection.is_empty
and geom_relevant_difference.is_empty
Expand Down Expand Up @@ -1045,7 +1080,6 @@ def _calculate_geom_by_intersection_and_reference(
geom = geom_relevant_intersection # (=empty geometry)
return geom, geom_relevant_intersection, geom_relevant_difference


def get_relevant_polygons_from_geom(self, geom):
"""
Get only the relevant parts (polygon) from a geometry.
Expand All @@ -1058,12 +1092,12 @@ def get_relevant_polygons_from_geom(self, geom):
geom = make_valid(unary_union(geom))
# Create a GeometryCollection from the input geometry.
geometry_collection = GeometryCollection(geom)
array=[]
array = []
for g in geometry_collection.geoms:
# Ensure each sub-geometry is valid.
g = make_valid(g)
if str(g.geom_type) in ["Polygon", "MultiPolygon"]:
relevant_geom = buffer_neg(g,self.buffer_distance())
relevant_geom = buffer_neg(g, self.buffer_distance())
if relevant_geom != None and not relevant_geom.is_empty:
array.append(g)
return make_valid(unary_union(array))
Expand Down
8 changes: 4 additions & 4 deletions brdr/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
# Limit used when extracting features by URL, using the feature API (fe from GRB)
DOWNLOAD_LIMIT = 10000

#default CRS:
DEFAULT_CRS= "EPSG:31370"
# default CRS:
DEFAULT_CRS = "EPSG:31370"

#MULTI_SINGLE_ID_SEPARATOR #separator to split multipolygon_ids to single polygons
MULTI_SINGLE_ID_SEPARATOR = '*$*'
# MULTI_SINGLE_ID_SEPARATOR #separator to split multipolygon_ids to single polygons
MULTI_SINGLE_ID_SEPARATOR = "*$*"
21 changes: 16 additions & 5 deletions brdr/geometry_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ def safe_union(geom_a: BaseGeometry, geom_b: BaseGeometry) -> BaseGeometry:
geom = union(geom_a, geom_b)
except GEOSException:
try:
logging.warning("union_error for geoms:" + geom_a.wkt + " and " + geom_b.wkt)
logging.warning(
"union_error for geoms:" + geom_a.wkt + " and " + geom_b.wkt
)
geom = union(buffer(geom_a, 0.0000001), buffer(geom_b, 0.0000001))
except Exception:
logging.error("error: empty geometry returned")
Expand Down Expand Up @@ -206,7 +208,9 @@ def safe_intersection(geom_a: BaseGeometry, geom_b: BaseGeometry) -> BaseGeometr
geom = intersection(geom_a, geom_b)
except GEOSException:
try:
logging.warning("intersection_error for geoms:" + geom_a.wkt + " and " + geom_b.wkt)
logging.warning(
"intersection_error for geoms:" + geom_a.wkt + " and " + geom_b.wkt
)
geom = intersection(buffer(geom_a, 0.0000001), buffer(geom_b, 0.0000001))
except Exception:
logging.error("error: empty geometry returned")
Expand Down Expand Up @@ -244,7 +248,9 @@ def safe_difference(geom_a, geom_b):
geom = difference(geom_a, geom_b)
except GEOSException:
try:
logging.warning("difference_error for geoms:" + geom_a.wkt + " and " + geom_b.wkt)
logging.warning(
"difference_error for geoms:" + geom_a.wkt + " and " + geom_b.wkt
)
geom = difference(buffer(geom_a, 0.0000001), buffer(geom_b, 0.0000001))
except Exception:
logging.error("error: empty geometry returned")
Expand Down Expand Up @@ -282,7 +288,12 @@ def safe_symmetric_difference(geom_a, geom_b):
geom = symmetric_difference(geom_a, geom_b)
except GEOSException:
try:
logging.warning("symmetric_difference_error for geoms:" + geom_a.wkt + " and " + geom_b.wkt)
logging.warning(
"symmetric_difference_error for geoms:"
+ geom_a.wkt
+ " and "
+ geom_b.wkt
)
geom = symmetric_difference(
buffer(geom_a, 0.0000001), buffer(geom_b, 0.0000001)
)
Expand Down Expand Up @@ -316,7 +327,7 @@ def grid_bounds(geom: BaseGeometry, delta: float):
nx = 2
if ny < 2:
ny = 2
gx, gy = np.linspace(min_x, max_x, nx+1), np.linspace(min_y, max_y, ny+1)
gx, gy = np.linspace(min_x, max_x, nx + 1), np.linspace(min_y, max_y, ny + 1)
grid = []
for i in range(len(gx) - 1):
for j in range(len(gy) - 1):
Expand Down
Loading