Skip to content

Commit 314e877

Browse files
varmar05wonder-sk
authored andcommitted
Fix geometry handling, alter report columns
1 parent be84ccd commit 314e877

File tree

3 files changed

+77
-58
lines changed

3 files changed

+77
-58
lines changed

mergin/client_pull.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,8 @@ def download_diffs_async(mc, project_directory, file_path, versions):
707707

708708
for version in versions:
709709
version_data = file_history["history"][version]
710+
if "diff" not in version_data:
711+
continue # skip if there is no diff in history
710712
diff_data = copy.deepcopy(version_data)
711713
diff_data['version'] = version
712714
diff_data['diff'] = version_data['diff']

mergin/report.py

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@
33
import os
44
import tempfile
55
from collections import defaultdict
6+
from datetime import datetime
67
from itertools import groupby
78

89
from mergin import ClientError
910
from mergin.merginproject import MerginProject, pygeodiff
1011
from mergin.utils import int_version
1112

13+
try:
14+
from qgis.core import QgsGeometry, QgsDistanceArea, QgsCoordinateReferenceSystem, QgsCoordinateTransformContext, QgsWkbTypes
15+
has_qgis = True
16+
except ImportError:
17+
has_qgis = False
18+
1219

1320
# inspired by C++ implementation https://github.com/lutraconsulting/geodiff/blob/master/geodiff/src/drivers/sqliteutils.cpp
1421
# in geodiff lib (MIT licence)
@@ -39,76 +46,78 @@ def parse_gpkgb_header_size(gpkg_wkb):
3946
return no_envelope_header_size + envelope_size
4047

4148

49+
def qgs_geom_from_wkb(geom):
50+
if not has_qgis:
51+
raise NotImplementedError
52+
g = QgsGeometry()
53+
wkb_header_length = parse_gpkgb_header_size(geom)
54+
wkb_geom = geom[wkb_header_length:]
55+
g.fromWkb(wkb_geom)
56+
return g
57+
58+
4259
class ChangesetReportEntry:
4360
""" Derivative of geodiff ChangesetEntry suitable for further processing/reporting """
4461
def __init__(self, changeset_entry, geom_idx, geom):
4562
self.table = changeset_entry.table.name
4663
self.geom_type = geom["type"]
47-
self.crs = geom["srs_id"]
64+
self.crs = "EPSG:" + geom["srs_id"]
65+
self.length = None
66+
self.area = None
4867

4968
if changeset_entry.operation == changeset_entry.OP_DELETE:
5069
self.operation = "delete"
51-
self.old_geom = changeset_entry.old_values[geom_idx]
52-
self.new_geom = None
5370
elif changeset_entry.operation == changeset_entry.OP_UPDATE:
5471
self.operation = "update"
55-
self.old_geom = changeset_entry.old_values[geom_idx]
56-
self.new_geom = changeset_entry.new_values[geom_idx]
5772
elif changeset_entry.operation == changeset_entry.OP_INSERT:
5873
self.operation = "insert"
59-
self.old_geom = None
60-
self.new_geom = changeset_entry.new_values[geom_idx]
61-
62-
self.count = None
63-
self.length = None
64-
self.area = None
65-
66-
if self.geom_type == "LINESTRING":
67-
# we calculate change in length, for attributes changes only we set to 0
68-
self.metric = "length"
69-
if self.operation == "delete":
70-
self.length = self.measure(self.old_geom)
71-
elif self.operation == "update":
72-
self.length = self.measure(self.new_geom) - self.measure(self.old_geom)
73-
elif self.operation == "insert":
74-
self.length = self.measure(self.new_geom)
75-
elif self.geom_type == "POLYGON":
76-
# we calculate change in area, for attributes changes only we set to 0
77-
self.metric = "area"
78-
if self.operation == "delete":
79-
self.area = self.measure(self.old_geom)
80-
elif self.operation == "update":
81-
self.area = self.measure(self.new_geom) - self.measure(self.old_geom)
82-
elif self.operation == "insert":
83-
self.area = self.measure(self.new_geom)
8474
else:
85-
# regardless of geometry change count as 1
86-
self.metric = "count"
87-
self.count = 1
75+
self.operation = "unknown"
8876

89-
def measure(self, geom):
90-
""" Return length or area of geometry based on type """
91-
# calculate geom length/area only if QGIS API is available
92-
try:
93-
from qgis.core import QgsGeometry, QgsDistanceArea, QgsCoordinateReferenceSystem, QgsCoordinateTransformContext
94-
except ImportError:
95-
return -1
77+
# only calculate geom properties when qgis api is available
78+
if not has_qgis:
79+
return
9680

9781
d = QgsDistanceArea()
9882
d.setEllipsoid('WGS84')
9983
crs = QgsCoordinateReferenceSystem()
10084
crs.createFromString(self.crs)
10185
d.setSourceCrs(crs, QgsCoordinateTransformContext())
102-
g = QgsGeometry()
103-
wkb_header_length = parse_gpkgb_header_size(geom)
104-
wkb_geom = geom[wkb_header_length:]
105-
g.fromWkb(wkb_geom)
106-
if self.metric == "length":
107-
return d.measureLength(g)
108-
elif self.metric == "area":
109-
return d.measureArea(g)
86+
87+
if hasattr(changeset_entry, "old_values"):
88+
old_wkb = changeset_entry.old_values[geom_idx]
89+
else:
90+
old_wkb = None
91+
if hasattr(changeset_entry, "new_values"):
92+
new_wkb = changeset_entry.new_values[geom_idx]
11093
else:
111-
return 1
94+
new_wkb = None
95+
96+
# no geometry at all
97+
if old_wkb is None and new_wkb is None:
98+
return
99+
100+
updated_qgs_geom = None
101+
if self.operation == "delete":
102+
qgs_geom = qgs_geom_from_wkb(old_wkb)
103+
elif self.operation == "update":
104+
qgs_geom = qgs_geom_from_wkb(old_wkb)
105+
# get new geom if it was updated, there can be updates also without change of geom
106+
updated_qgs_geom = qgs_geom_from_wkb(new_wkb) if new_wkb else qgs_geom
107+
elif self.operation == "insert":
108+
qgs_geom = qgs_geom_from_wkb(new_wkb)
109+
110+
dim = QgsWkbTypes.wkbDimensions(qgs_geom.wkbType())
111+
if dim == 1:
112+
self.length = d.measureLength(qgs_geom)
113+
if updated_qgs_geom:
114+
self.length = d.measureLength(updated_qgs_geom) - self.length
115+
elif dim == 2:
116+
self.length = d.measurePerimeter(qgs_geom)
117+
self.area = d.measureArea(qgs_geom)
118+
if updated_qgs_geom:
119+
self.length = d.measurePerimeter(updated_qgs_geom) - self.length
120+
self.area = d.measureArea(updated_qgs_geom) - self.area
112121

113122

114123
class ChangesetReport:
@@ -139,15 +148,17 @@ def report(self):
139148
tables[obj.table].append(obj)
140149

141150
for table, entries in tables.items():
142-
items = groupby(entries, lambda i: (i.operation, i.metric))
151+
items = groupby(entries, lambda i: (i.operation, i.geom_type))
143152
for k, v in items:
144-
quantity_type = "_".join(k)
145153
values = list(v)
146-
quantity = sum([getattr(entry, k[1]) for entry in values])
154+
area = sum([entry.area for entry in values if entry.area]) if has_qgis else None
155+
length = sum([entry.length for entry in values if entry.length]) if has_qgis else None
147156
records.append({
148157
"table": table,
149-
"quantity_type": quantity_type,
150-
"quantity": quantity
158+
"operation": k[0],
159+
"length": length,
160+
"area": area,
161+
"count": len(values)
151162
})
152163
return records
153164

@@ -166,7 +177,7 @@ def create_report(mc, directory, project, since, to, out_dir=tempfile.gettempdir
166177
mp = MerginProject(directory)
167178
mp.log.info(f"--- Creating changesets report for {project} from {since} to {to} versions ----")
168179
versions_map = {v["name"]: v for v in mc.project_versions(project, since, to)}
169-
headers = ["file", "table", "author", "timestamp", "version", "quantity_type", "quantity"]
180+
headers = ["file", "table", "author", "date", "time", "version", "operation", "length", "area", "count"]
170181
records = []
171182
info = mc.project_info(project, since=since)
172183
num_since = int_version(since)
@@ -203,6 +214,9 @@ def create_report(mc, directory, project, since, to, out_dir=tempfile.gettempdir
203214

204215
# add records for every version (diff) and all tables within geopackage
205216
for version in history_keys:
217+
if "diff" not in f['history'][version]:
218+
continue
219+
206220
v_diff_file = os.path.join(mp.meta_dir, '.cache',
207221
version + "-" + f['history'][version]['diff']['path'])
208222

@@ -211,10 +225,12 @@ def create_report(mc, directory, project, since, to, out_dir=tempfile.gettempdir
211225
rep = ChangesetReport(cr, schema)
212226
report = rep.report()
213227
# append version info to changeset info
228+
dt = datetime.fromisoformat(version_data["created"].rstrip("Z"))
214229
version_fields = {
215230
"file": f["path"],
216231
"author": version_data["author"],
217-
"timestamp": version_data["created"],
232+
"date": dt.date().isoformat(),
233+
"time": dt.time().isoformat(),
218234
"version": version_data["name"]
219235
}
220236
for row in report:

mergin/test/test_client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,8 +1637,9 @@ def test_report(mc):
16371637
# assert headers and content in report file
16381638
with open(report_file, "r") as rf:
16391639
content = rf.read()
1640-
assert ",".join(["file", "table", "author", "timestamp", "version", "quantity_type", "quantity"]) in content
1640+
headers = ",".join(["file", "table", "author", "date", "time", "version", "operation", "length", "area", "count"])
1641+
assert headers in content
16411642
assert "base.gpkg,simple,test_plugin" in content
1642-
assert "v3,update_count,2" in content
1643+
assert "v3,update,,,2" in content
16431644
# files not edited are not in reports
16441645
assert "inserted_1_A.gpkg" not in content

0 commit comments

Comments
 (0)