3
3
import os
4
4
import tempfile
5
5
from collections import defaultdict
6
+ from datetime import datetime
6
7
from itertools import groupby
7
8
8
9
from mergin import ClientError
9
10
from mergin .merginproject import MerginProject , pygeodiff
10
11
from mergin .utils import int_version
11
12
13
+ try :
14
+ from qgis .core import QgsGeometry , QgsDistanceArea , QgsCoordinateReferenceSystem , QgsCoordinateTransformContext , QgsWkbTypes
15
+ has_qgis = True
16
+ except ImportError :
17
+ has_qgis = False
18
+
12
19
13
20
# inspired by C++ implementation https://github.com/lutraconsulting/geodiff/blob/master/geodiff/src/drivers/sqliteutils.cpp
14
21
# in geodiff lib (MIT licence)
@@ -39,76 +46,78 @@ def parse_gpkgb_header_size(gpkg_wkb):
39
46
return no_envelope_header_size + envelope_size
40
47
41
48
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
+
42
59
class ChangesetReportEntry :
43
60
""" Derivative of geodiff ChangesetEntry suitable for further processing/reporting """
44
61
def __init__ (self , changeset_entry , geom_idx , geom ):
45
62
self .table = changeset_entry .table .name
46
63
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
48
67
49
68
if changeset_entry .operation == changeset_entry .OP_DELETE :
50
69
self .operation = "delete"
51
- self .old_geom = changeset_entry .old_values [geom_idx ]
52
- self .new_geom = None
53
70
elif changeset_entry .operation == changeset_entry .OP_UPDATE :
54
71
self .operation = "update"
55
- self .old_geom = changeset_entry .old_values [geom_idx ]
56
- self .new_geom = changeset_entry .new_values [geom_idx ]
57
72
elif changeset_entry .operation == changeset_entry .OP_INSERT :
58
73
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 )
84
74
else :
85
- # regardless of geometry change count as 1
86
- self .metric = "count"
87
- self .count = 1
75
+ self .operation = "unknown"
88
76
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
96
80
97
81
d = QgsDistanceArea ()
98
82
d .setEllipsoid ('WGS84' )
99
83
crs = QgsCoordinateReferenceSystem ()
100
84
crs .createFromString (self .crs )
101
85
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 ]
110
93
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
112
121
113
122
114
123
class ChangesetReport :
@@ -139,15 +148,17 @@ def report(self):
139
148
tables [obj .table ].append (obj )
140
149
141
150
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 ))
143
152
for k , v in items :
144
- quantity_type = "_" .join (k )
145
153
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
147
156
records .append ({
148
157
"table" : table ,
149
- "quantity_type" : quantity_type ,
150
- "quantity" : quantity
158
+ "operation" : k [0 ],
159
+ "length" : length ,
160
+ "area" : area ,
161
+ "count" : len (values )
151
162
})
152
163
return records
153
164
@@ -166,7 +177,7 @@ def create_report(mc, directory, project, since, to, out_dir=tempfile.gettempdir
166
177
mp = MerginProject (directory )
167
178
mp .log .info (f"--- Creating changesets report for { project } from { since } to { to } versions ----" )
168
179
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 " ]
170
181
records = []
171
182
info = mc .project_info (project , since = since )
172
183
num_since = int_version (since )
@@ -203,6 +214,9 @@ def create_report(mc, directory, project, since, to, out_dir=tempfile.gettempdir
203
214
204
215
# add records for every version (diff) and all tables within geopackage
205
216
for version in history_keys :
217
+ if "diff" not in f ['history' ][version ]:
218
+ continue
219
+
206
220
v_diff_file = os .path .join (mp .meta_dir , '.cache' ,
207
221
version + "-" + f ['history' ][version ]['diff' ]['path' ])
208
222
@@ -211,10 +225,12 @@ def create_report(mc, directory, project, since, to, out_dir=tempfile.gettempdir
211
225
rep = ChangesetReport (cr , schema )
212
226
report = rep .report ()
213
227
# append version info to changeset info
228
+ dt = datetime .fromisoformat (version_data ["created" ].rstrip ("Z" ))
214
229
version_fields = {
215
230
"file" : f ["path" ],
216
231
"author" : version_data ["author" ],
217
- "timestamp" : version_data ["created" ],
232
+ "date" : dt .date ().isoformat (),
233
+ "time" : dt .time ().isoformat (),
218
234
"version" : version_data ["name" ]
219
235
}
220
236
for row in report :
0 commit comments