svgmapviewer-tools-osm 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +5 -0
- package/LICENSE +15 -0
- package/__pycache__/common.cpython-312.pyc +0 -0
- package/dist/geojsonToTs.js +15 -0
- package/eslint.config.js +58 -0
- package/oxlintrc.json +4 -0
- package/package.json +35 -0
- package/prettier.config.js +14 -0
- package/rslib.config.ts +22 -0
- package/rstest.config.ts +5 -0
- package/scripts/__pycache__/common.cpython-312.pyc +0 -0
- package/scripts/classifyGeometries.py +56 -0
- package/scripts/common.py +969 -0
- package/scripts/copy.sh +36 -0
- package/scripts/extractAreas.py +41 -0
- package/scripts/extractAreas.sh +20 -0
- package/scripts/extractLinesByIds.py +48 -0
- package/scripts/extractMultiLineStringsByIds.py +48 -0
- package/scripts/extractPointsByIds.py +42 -0
- package/scripts/extractPolygonsByIds.py +42 -0
- package/scripts/geojson2ts.py +79 -0
- package/scripts/getOsm.sh +62 -0
- package/scripts/initQgisPrj.py +44 -0
- package/scripts/makeAreas.py +21 -0
- package/scripts/makeAreas.sh +20 -0
- package/scripts/makeExtent.py +21 -0
- package/scripts/makeMeasures.py +25 -0
- package/scripts/makeOrigin.py +30 -0
- package/scripts/makeViewbox.py +22 -0
- package/scripts/osmconf.ini +122 -0
- package/scripts/pyqgis-Ubuntu.sh +36 -0
- package/scripts/pyqgis-macOS.sh +40 -0
- package/scripts/pyqgis.sh +34 -0
- package/scripts/readOsm.py +37 -0
- package/scripts/readOsm.sh +27 -0
- package/scripts/regen.sh +21 -0
- package/scripts/run-common.sh +3 -0
- package/scripts/tagAddresses.py +51 -0
- package/scripts/update.sh +53 -0
- package/src/geojsonToTs.ts +15 -0
- package/src/lib/geojson/geojson-print.test.ts +60 -0
- package/src/lib/geojson/geojson-print.ts +211 -0
- package/src/lib/geojson/geojson-schema.test.ts +42 -0
- package/src/lib/geojson/geojson-schema.ts +151 -0
- package/src/lib/geojson/geojson-types.ts +116 -0
- package/src/lib/osm.test.ts +17 -0
- package/src/lib/osm.ts +18 -0
- package/src/lib/print-utils.test.ts +12 -0
- package/src/lib/print-utils.ts +22 -0
- package/src/lib/print.ts +122 -0
- package/test/geojson.json +22 -0
- package/tsconfig.app.json +34 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node-browser.json +27 -0
- package/tsconfig.node.json +24 -0
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
import decimal
|
|
2
|
+
import glob
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
import os.path
|
|
6
|
+
import pathlib
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
################################################################################
|
|
12
|
+
|
|
13
|
+
#### QGIS INITIALIZATION
|
|
14
|
+
|
|
15
|
+
from PyQt5.QtCore import *
|
|
16
|
+
from qgis.core import *
|
|
17
|
+
from qgis.gui import *
|
|
18
|
+
|
|
19
|
+
qgs = QgsApplication([], False)
|
|
20
|
+
qgs.initQgis()
|
|
21
|
+
freq = QgsFeatureRequest()
|
|
22
|
+
freq.setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid)
|
|
23
|
+
|
|
24
|
+
from plugins import processing
|
|
25
|
+
from processing.core.Processing import Processing
|
|
26
|
+
from processing.tools import dataobjects
|
|
27
|
+
from qgis.core import QgsFeatureRequest
|
|
28
|
+
|
|
29
|
+
Processing.initialize()
|
|
30
|
+
|
|
31
|
+
context = dataobjects.createContext()
|
|
32
|
+
context.setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck)
|
|
33
|
+
|
|
34
|
+
prj: QgsProject = None
|
|
35
|
+
|
|
36
|
+
################################################################################
|
|
37
|
+
|
|
38
|
+
#### COMMON FILES
|
|
39
|
+
|
|
40
|
+
def path2name(p) -> str:
|
|
41
|
+
return pathlib.PurePath(os.path.basename(p)).stem
|
|
42
|
+
|
|
43
|
+
# QgsVectorLayer geometry type
|
|
44
|
+
# https://qgis.org/pyqgis/master/core/QgsVectorLayer.html
|
|
45
|
+
# memory data provider
|
|
46
|
+
# - “point”
|
|
47
|
+
# - “linestring”
|
|
48
|
+
# - “polygon”
|
|
49
|
+
# - “multipoint”
|
|
50
|
+
# - ”multilinestring”
|
|
51
|
+
# - ”multipolygon”
|
|
52
|
+
#type VectorGeometryType = typing.Literal['point', 'linestring', 'polygon', 'multipoint', 'multilinestring', 'multipolygon']
|
|
53
|
+
|
|
54
|
+
# layername -> QgsVectorLayer geometry type
|
|
55
|
+
# c.f. osmconf.ini
|
|
56
|
+
osmLayerNames = [
|
|
57
|
+
('points', 'point'),
|
|
58
|
+
('lines', 'linestring'),
|
|
59
|
+
('multipolygons', 'multipolygon'),
|
|
60
|
+
('multilinestrings', 'multilinestring'),
|
|
61
|
+
('other_relations', 'Polygon')
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
extraLayerNames = [
|
|
65
|
+
#('midpoints', 'point'),
|
|
66
|
+
#('centroids', 'point'),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# ./map*.osm
|
|
70
|
+
# ./map.qgz
|
|
71
|
+
# ./src/data/areas.geojson
|
|
72
|
+
# ./src/data/internals.geojson
|
|
73
|
+
# ./src/data/origin.geojson
|
|
74
|
+
# ./src/data/measures.geojson
|
|
75
|
+
# ./src/data/map-points.geojson
|
|
76
|
+
# ./src/data/map-lines.geojson
|
|
77
|
+
# ./src/data/map-multilinestrings.geojson
|
|
78
|
+
# ./src/data/map-multipolygons.geojson
|
|
79
|
+
# ./src/data/map-other_relations.geojson
|
|
80
|
+
|
|
81
|
+
class Context:
|
|
82
|
+
prjdir = ''
|
|
83
|
+
prj = ''
|
|
84
|
+
srcdir = ''
|
|
85
|
+
|
|
86
|
+
osmFiles = []
|
|
87
|
+
|
|
88
|
+
areasGJ = ''
|
|
89
|
+
areas_extentGJ = ''
|
|
90
|
+
internalsGJ = ''
|
|
91
|
+
internals_extentGJ = ''
|
|
92
|
+
originGJ = ''
|
|
93
|
+
measuresGJ = ''
|
|
94
|
+
viewboxGJ = ''
|
|
95
|
+
|
|
96
|
+
# points, lines, multilinestrings, multipolygons, other_relations
|
|
97
|
+
map_layerGJs = {}
|
|
98
|
+
|
|
99
|
+
def __init__(self):
|
|
100
|
+
prjdir = os.getcwd()
|
|
101
|
+
srcdir = '%s/src/data' % prjdir
|
|
102
|
+
|
|
103
|
+
self.prjdir = prjdir
|
|
104
|
+
self.srcdir = srcdir
|
|
105
|
+
|
|
106
|
+
self.prj = '%s/map.qgz' % prjdir
|
|
107
|
+
|
|
108
|
+
# XXX osmFiles must NOT be []
|
|
109
|
+
self.osmFiles = glob.glob('%s/map*.osm' % prjdir)
|
|
110
|
+
|
|
111
|
+
self.areasGJ = '%s/areas.geojson' % srcdir
|
|
112
|
+
self.areas_extentGJ = '%s/areas_extent.geojson' % srcdir
|
|
113
|
+
self.internalsGJ = '%s/internals.geojson' % srcdir
|
|
114
|
+
self.internals_extentGJ = '%s/internals_extent.geojson' % srcdir
|
|
115
|
+
self.originGJ = '%s/origin.geojson' % srcdir
|
|
116
|
+
self.measuresGJ = '%s/measures.geojson' % srcdir
|
|
117
|
+
self.viewboxGJ = '%s/viewbox.geojson' % srcdir
|
|
118
|
+
|
|
119
|
+
for (layername, _) in osmLayerNames:
|
|
120
|
+
self.map_layerGJs[layername] = '%s/map-%s.geojson' % (srcdir, layername)
|
|
121
|
+
for (layername, _) in extraLayerNames:
|
|
122
|
+
self.map_layerGJs[layername] = '%s/map-%s.geojson' % (srcdir, layername)
|
|
123
|
+
|
|
124
|
+
def jsonToGeoJson(self):
|
|
125
|
+
for json in glob.glob('%s/*.json' % self.srcdir):
|
|
126
|
+
(x, suffix) = os.path.splitext(json)
|
|
127
|
+
geojson = '%s.geojson' % x
|
|
128
|
+
if pathlib.Path(json).exists() and not pathlib.Path(geojson).exists():
|
|
129
|
+
shutil.move(json, geojson)
|
|
130
|
+
|
|
131
|
+
def geoJsonToJson(self):
|
|
132
|
+
for geojson in glob.glob('%s/*.geojson' % self.srcdir):
|
|
133
|
+
(x, suffix) = os.path.splitext(geojson)
|
|
134
|
+
json = '%s.json' % x
|
|
135
|
+
if pathlib.Path(geojson).exists() and not pathlib.Path(json).exists():
|
|
136
|
+
shutil.move(geojson, json)
|
|
137
|
+
|
|
138
|
+
addrTmpl = 'A-1f-%s-%s-%d'
|
|
139
|
+
|
|
140
|
+
ctx = Context()
|
|
141
|
+
ctx.jsonToGeoJson()
|
|
142
|
+
|
|
143
|
+
################################################################################
|
|
144
|
+
|
|
145
|
+
def exit():
|
|
146
|
+
global qgs
|
|
147
|
+
qgs.exitQgis()
|
|
148
|
+
del qgs
|
|
149
|
+
|
|
150
|
+
ctx.geoJsonToJson()
|
|
151
|
+
|
|
152
|
+
print('DONE!')
|
|
153
|
+
|
|
154
|
+
################################################################################
|
|
155
|
+
|
|
156
|
+
#### PROCESSING WRAPPERS
|
|
157
|
+
|
|
158
|
+
def mergeVectorLayers(layers: list[str], dst: QgsVectorLayer) -> QgsVectorLayer:
|
|
159
|
+
p = {
|
|
160
|
+
'CRS' : None,
|
|
161
|
+
'LAYERS' : layers,
|
|
162
|
+
'OUTPUT' : dst
|
|
163
|
+
}
|
|
164
|
+
print(p)
|
|
165
|
+
return processing.run("qgis:mergevectorlayers", p, context = context)['OUTPUT']
|
|
166
|
+
|
|
167
|
+
def fixGeometries(src: QgsVectorLayer, dst: QgsVectorLayer) -> QgsVectorLayer:
|
|
168
|
+
p = {
|
|
169
|
+
'INPUT': src,
|
|
170
|
+
'OUTPUT' : dst
|
|
171
|
+
}
|
|
172
|
+
return processing.run("qgis:fixgeometries", p, context = context)['OUTPUT']
|
|
173
|
+
|
|
174
|
+
def deleteColumn(src: QgsVectorLayer, dst: QgsVectorLayer, column) -> QgsVectorLayer:
|
|
175
|
+
p = {
|
|
176
|
+
'INPUT': src,
|
|
177
|
+
'OUTPUT': dst,
|
|
178
|
+
'COLUMN': column
|
|
179
|
+
}
|
|
180
|
+
return processing.run("qgis:deletecolumn", p, context = context)['OUTPUT']
|
|
181
|
+
|
|
182
|
+
def deleteDuplicateGeometries(src: QgsVectorLayer, dst: QgsVectorLayer) -> QgsVectorLayer:
|
|
183
|
+
p = {
|
|
184
|
+
'INPUT' : src,
|
|
185
|
+
'OUTPUT' : dst
|
|
186
|
+
}
|
|
187
|
+
return processing.run("qgis:deleteduplicategeometries", p, context = context)['OUTPUT']
|
|
188
|
+
|
|
189
|
+
def selectByLocation(src: QgsVectorLayer, predicate: int, intersect: QgsVectorLayer = 0, method = 0) -> QgsVectorLayer:
|
|
190
|
+
p = {
|
|
191
|
+
'INPUT' : src,
|
|
192
|
+
'PREDICATE' : predicate,
|
|
193
|
+
'INTERSECT' : intersect,
|
|
194
|
+
'METHOD' : method,
|
|
195
|
+
}
|
|
196
|
+
return processing.run("qgis:selectbylocation", p, context = context)['OUTPUT']
|
|
197
|
+
|
|
198
|
+
def joinAttributesByLocation(src: QgsVectorLayer, join, predicates: int, dst: QgsVectorLayer, method = 0, discard = False) -> QgsVectorLayer:
|
|
199
|
+
# https://docs.qgis.org/testing/en/docs/user_manual/processing_algs/qgis/vectorgeneral.html#id58
|
|
200
|
+
# PREDICATE:
|
|
201
|
+
# 0 — intersects
|
|
202
|
+
# 1 — contains
|
|
203
|
+
# 2 — equals
|
|
204
|
+
# 3 — touches
|
|
205
|
+
# 4 — overlaps
|
|
206
|
+
# 5 — within
|
|
207
|
+
# 6 — crosses
|
|
208
|
+
# METHOD:
|
|
209
|
+
# 0 — one-to-many
|
|
210
|
+
# 1 — one-to-one
|
|
211
|
+
p = {
|
|
212
|
+
'INPUT' : src,
|
|
213
|
+
'JOIN' : join,
|
|
214
|
+
'PREDICATE' : predicates, # within
|
|
215
|
+
'JOIN_FIELDS' : [],
|
|
216
|
+
'METHOD' : method, # 0:one-to-many, 1:one-to-one
|
|
217
|
+
'DISCARD_NONMATCHING' : discard,
|
|
218
|
+
'PREFIX' : '',
|
|
219
|
+
'OUTPUT' : dst
|
|
220
|
+
}
|
|
221
|
+
return processing.run("qgis:joinattributesbylocation", p, context = context)['OUTPUT']
|
|
222
|
+
|
|
223
|
+
locationPredicates = [
|
|
224
|
+
(0, 'intersects'),
|
|
225
|
+
(1, 'contains'),
|
|
226
|
+
(2, 'equals'),
|
|
227
|
+
(3, 'touches'),
|
|
228
|
+
(4, 'overlaps'),
|
|
229
|
+
(5, 'within'),
|
|
230
|
+
(6, 'crosses')
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
def centroids(src: QgsVectorLayer, dst: QgsVectorLayer) -> QgsVectorLayer:
|
|
234
|
+
p = {
|
|
235
|
+
'ALL_PARTS' : False,
|
|
236
|
+
'INPUT' : src,
|
|
237
|
+
'OUTPUT' : dst
|
|
238
|
+
}
|
|
239
|
+
return processing.run("qgis:centroids", p, context = context)['OUTPUT']
|
|
240
|
+
|
|
241
|
+
def getExtent(src: QgsVectorLayer, dst: QgsVectorLayer) -> QgsVectorLayer:
|
|
242
|
+
p = {
|
|
243
|
+
'INPUT': src,
|
|
244
|
+
'OUTPUT': dst,
|
|
245
|
+
'ROUND_TO': 0,
|
|
246
|
+
}
|
|
247
|
+
return processing.run("native:polygonfromlayerextent", p, context = context)['OUTPUT']
|
|
248
|
+
|
|
249
|
+
################################################################################
|
|
250
|
+
|
|
251
|
+
#### FILTER OPERATIONS
|
|
252
|
+
|
|
253
|
+
def filterPoint(l: QgsVectorLayer, exp: str) -> QgsVectorLayer:
|
|
254
|
+
return filter(l, "point", exp)
|
|
255
|
+
|
|
256
|
+
def filterMultiPoint(l: QgsVectorLayer, exp: str) -> QgsVectorLayer:
|
|
257
|
+
return filter(l, "multipoint", exp)
|
|
258
|
+
|
|
259
|
+
def filterLine(l: QgsVectorLayer, exp: str) -> QgsVectorLayer:
|
|
260
|
+
return filter(l, "line", exp)
|
|
261
|
+
|
|
262
|
+
def filterMultiLineString(l: QgsVectorLayer, exp: str) -> QgsVectorLayer:
|
|
263
|
+
return filter(l, "multilinestring", exp)
|
|
264
|
+
|
|
265
|
+
def filterPolygon(l: QgsVectorLayer, exp: str) -> QgsVectorLayer:
|
|
266
|
+
return filter(l, "polygon", exp)
|
|
267
|
+
|
|
268
|
+
def filterMultiPolygon(l: QgsVectorLayer, exp: str) -> QgsVectorLayer:
|
|
269
|
+
return filter(l, "multipolygon", exp)
|
|
270
|
+
|
|
271
|
+
def filter(l: QgsVectorLayer, typ: str, exp: str) -> QgsVectorLayer:
|
|
272
|
+
fields = l.fields()
|
|
273
|
+
|
|
274
|
+
m = makeVector(typ, "XXX")
|
|
275
|
+
m.startEditing()
|
|
276
|
+
md = m.dataProvider()
|
|
277
|
+
md.addAttributes(fields)
|
|
278
|
+
m.updateFields()
|
|
279
|
+
|
|
280
|
+
l.selectByExpression(exp, Qgis.SelectBehavior.SetSelection) # SelectBehavior = SetSelection
|
|
281
|
+
#print('l', len(l.selectedFeatures()))
|
|
282
|
+
for f in l.selectedFeatures():
|
|
283
|
+
m.addFeature(f)
|
|
284
|
+
m.commitChanges()
|
|
285
|
+
print("filter: exp: %s" % exp)
|
|
286
|
+
print("filter: %d/%d" % (m.featureCount(), l.featureCount()))
|
|
287
|
+
|
|
288
|
+
return m
|
|
289
|
+
|
|
290
|
+
################################################################################
|
|
291
|
+
|
|
292
|
+
#### OTHER OPERATIONS
|
|
293
|
+
|
|
294
|
+
def readOsmAll() -> dict[str, QgsVectorLayer]:
|
|
295
|
+
selector: typing.Callable[[QgsVectorLayer], None] = lambda l: l.selectAll()
|
|
296
|
+
return readOsm(selector)
|
|
297
|
+
|
|
298
|
+
def readOsmByAreas(areas: QgsVectorLayer) -> dict[str, QgsVectorLayer]:
|
|
299
|
+
selector: typing.Callable[[QgsVectorLayer], None] = lambda l: selectByLocation(l, 0, areas, 0)
|
|
300
|
+
return readOsm(selector)
|
|
301
|
+
|
|
302
|
+
def readOsm(selector: typing.Callable[[QgsVectorLayer], None]) -> dict[str, QgsVectorLayer]:
|
|
303
|
+
allLayers: dict[str, list[QgsVectorLayer]] = {}
|
|
304
|
+
mapLayers: dict[str, QgsVectorLayer] = {}
|
|
305
|
+
|
|
306
|
+
for (layername, typ) in osmLayerNames:
|
|
307
|
+
layers: list[QgsVectorLayer] = []
|
|
308
|
+
for osm in ctx.osmFiles:
|
|
309
|
+
uri = '%s|layername=%s' % (osm, layername)
|
|
310
|
+
name = path2name(osm)
|
|
311
|
+
l = openVector(uri, name)
|
|
312
|
+
layers.append(l)
|
|
313
|
+
allLayers[layername] = layers
|
|
314
|
+
|
|
315
|
+
for (layername, typ) in osmLayerNames:
|
|
316
|
+
layers = allLayers[layername]
|
|
317
|
+
fields = layers[0].fields()
|
|
318
|
+
|
|
319
|
+
name = 'map-%s' % layername
|
|
320
|
+
|
|
321
|
+
m = makeVector(typ, name)
|
|
322
|
+
m.startEditing()
|
|
323
|
+
md = m.dataProvider()
|
|
324
|
+
md.addAttributes(fields)
|
|
325
|
+
m.updateFields()
|
|
326
|
+
|
|
327
|
+
for l in layers:
|
|
328
|
+
selector(l)
|
|
329
|
+
for f in l.selectedFeatures():
|
|
330
|
+
m.addFeature(f)
|
|
331
|
+
m.commitChanges()
|
|
332
|
+
|
|
333
|
+
mapLayers[layername] = m
|
|
334
|
+
|
|
335
|
+
mapLayers['midpoints'] = centroids(mapLayers['lines'], 'memory:')
|
|
336
|
+
mapLayers['centroids'] = centroids(mapLayers['multipolygons'], 'memory:')
|
|
337
|
+
|
|
338
|
+
return mapLayers
|
|
339
|
+
|
|
340
|
+
def expandOsm(osm: str, layername: str, name: str, outGeoJSON: str) -> tuple[QgsVectorFileWriter.WriterError, str, str, str]:
|
|
341
|
+
uri = '%s|layername=%s' % (osm, layername)
|
|
342
|
+
l = openVector(uri, name)
|
|
343
|
+
print('layer', uri, l.featureCount())
|
|
344
|
+
tx = l.transformContext()
|
|
345
|
+
opts = QgsVectorFileWriter.SaveVectorOptions()
|
|
346
|
+
opts.driverName = "GeoJSON"
|
|
347
|
+
return QgsVectorFileWriter.writeAsVectorFormatV3(l, outGeoJSON, tx, opts)
|
|
348
|
+
|
|
349
|
+
def dumpGeoJSON(l: QgsVectorLayer, fn: str) -> tuple[QgsVectorFileWriter.WriterError, str, str, str]:
|
|
350
|
+
tx = l.transformContext()
|
|
351
|
+
if tx is None:
|
|
352
|
+
tx = prj.transformContext()
|
|
353
|
+
|
|
354
|
+
opts = QgsVectorFileWriter.SaveVectorOptions()
|
|
355
|
+
opts.driverName = "GeoJSON"
|
|
356
|
+
if not os.path.exists(ctx.srcdir):
|
|
357
|
+
os.makedirs(ctx.srcdir)
|
|
358
|
+
return QgsVectorFileWriter.writeAsVectorFormatV3(l, fn, tx, opts)
|
|
359
|
+
|
|
360
|
+
def dumpCSV(l: QgsVectorLayer, fn: str) -> tuple[QgsVectorFileWriter.WriterError, str, str, str]:
|
|
361
|
+
tx = l.transformContext()
|
|
362
|
+
opts = QgsVectorFileWriter.SaveVectorOptions()
|
|
363
|
+
opts.driverName = "CSV"
|
|
364
|
+
opts.layerOptions = [
|
|
365
|
+
"GEOMETRY=AS_WKT",
|
|
366
|
+
"SEPARATOR=COMMA"
|
|
367
|
+
]
|
|
368
|
+
return QgsVectorFileWriter.writeAsVectorFormatV3(l, fn, tx, opts)
|
|
369
|
+
|
|
370
|
+
# Merging multiple vectors
|
|
371
|
+
# Needed when the target area is large and covered by multiple map.osm
|
|
372
|
+
def mergeVectors(olayers, layername) -> QgsVectorLayer:
|
|
373
|
+
out = mergeVectorLayers(olayers, 'memory:')
|
|
374
|
+
# XXX GeoJSON needs fixgeometries
|
|
375
|
+
out = fixGeometries(out, 'memory:')
|
|
376
|
+
out = trimLayer(out)
|
|
377
|
+
return deleteDuplicateGeometries(out, 'memory:')
|
|
378
|
+
|
|
379
|
+
def trimLayer(l: QgsVectorLayer) -> QgsVectorLayer:
|
|
380
|
+
for c in ['layer', 'path']:
|
|
381
|
+
l = deleteColumn(l, 'memory:', c)
|
|
382
|
+
return l
|
|
383
|
+
|
|
384
|
+
def getBoundingBox(l: QgsVectorLayer) -> QgsRectangle:
|
|
385
|
+
l.selectAll()
|
|
386
|
+
return l.boundingBoxOfSelected()
|
|
387
|
+
|
|
388
|
+
def createEmptyPolygonGeoJSON(outGJ, rect: QgsRectangle):
|
|
389
|
+
g = emptyPolygon(rect)
|
|
390
|
+
return createEmptyGeoJSON(outGJ, "polygon", g)
|
|
391
|
+
|
|
392
|
+
def createEmptyLineGeoJSON(outGJ, rect: QgsRectangle):
|
|
393
|
+
g = emptyLine(rect)
|
|
394
|
+
return createEmptyGeoJSON(outGJ, "line", g)
|
|
395
|
+
|
|
396
|
+
def createEmptyMultiPointGeoJSON(outGJ, rect: QgsRectangle):
|
|
397
|
+
g = emptyMultiPoint(rect)
|
|
398
|
+
return createEmptyGeoJSON(outGJ, "multipoint", g)
|
|
399
|
+
|
|
400
|
+
def createEmptyPointGeoJSON(outGJ, rect: QgsRectangle):
|
|
401
|
+
g = emptyPoint(rect)
|
|
402
|
+
return createEmptyGeoJSON(outGJ, "point", g)
|
|
403
|
+
|
|
404
|
+
def emptyPoint(rect: QgsRectangle) -> QgsGeometry:
|
|
405
|
+
(x1, y1, x2, y2) = rect2tuple(rect)
|
|
406
|
+
x0 = (x1 + x2) / 2
|
|
407
|
+
y0 = (y1 + y2) / 2
|
|
408
|
+
p0 = QgsPointXY(x0, y0)
|
|
409
|
+
return QgsGeometry.fromPointXY(p0)
|
|
410
|
+
|
|
411
|
+
def emptyMultiPoint(rect: QgsRectangle) -> QgsGeometry:
|
|
412
|
+
(x1, y1, x2, y2) = rect2tuple(rect)
|
|
413
|
+
x0 = (x1 + x2) / 2
|
|
414
|
+
y0 = (y1 + y2) / 2
|
|
415
|
+
p0 = QgsPointXY(x0, y0)
|
|
416
|
+
mp = [p0]
|
|
417
|
+
return QgsGeometry.fromMultiPointXY(mp)
|
|
418
|
+
|
|
419
|
+
def emptyLine(rect: QgsRectangle) -> QgsGeometry:
|
|
420
|
+
(x1, y1, x2, y2) = rect2tuple(rect)
|
|
421
|
+
p1 = QgsPointXY(x1, y1)
|
|
422
|
+
p2 = QgsPointXY(x2, y2)
|
|
423
|
+
pl = [p1, p2]
|
|
424
|
+
mpl = [pl]
|
|
425
|
+
return QgsGeometry.fromMultiPolylineXY(mpl)
|
|
426
|
+
|
|
427
|
+
# XXX Line vs. MultiLineString
|
|
428
|
+
# XXX Polygon vs. MultiPolygon
|
|
429
|
+
|
|
430
|
+
def emptyPolygon(rect: QgsRectangle) -> QgsGeometry:
|
|
431
|
+
(x1, y1, x2, y2) = rect2tuple(rect)
|
|
432
|
+
pl = [
|
|
433
|
+
QgsPointXY(x1, y1),
|
|
434
|
+
QgsPointXY(x2, y1),
|
|
435
|
+
QgsPointXY(x2, y2),
|
|
436
|
+
QgsPointXY(x1, y2)
|
|
437
|
+
]
|
|
438
|
+
pg = [pl]
|
|
439
|
+
mpg = [pg]
|
|
440
|
+
return QgsGeometry.fromMultiPolygonXY(mpg)
|
|
441
|
+
|
|
442
|
+
def rect2tuple(rect: QgsRectangle) -> tuple[float, float, float, float]:
|
|
443
|
+
return (rect.xMinimum(), rect.yMinimum(), rect.xMaximum(), rect.yMaximum())
|
|
444
|
+
|
|
445
|
+
def createEmptyGeoJSON(outGJ: str, typ: str, g: QgsGeometry) -> tuple[QgsVectorFileWriter.WriterError, str, str, str]:
|
|
446
|
+
if os.path.exists(outGJ):
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
m = createEmptyLayer(typ, g)
|
|
450
|
+
|
|
451
|
+
tx = m.transformContext()
|
|
452
|
+
opts = QgsVectorFileWriter.SaveVectorOptions()
|
|
453
|
+
opts.driverName = "GeoJSON"
|
|
454
|
+
return QgsVectorFileWriter.writeAsVectorFormatV3(m, outGJ, tx, opts)
|
|
455
|
+
|
|
456
|
+
def createEmptyLayer(typ: str, g: QgsGeometry) -> QgsVectorLayer:
|
|
457
|
+
f = QgsFeature()
|
|
458
|
+
f.setGeometry(g)
|
|
459
|
+
l = makeVector(typ, "XXX")
|
|
460
|
+
l.startEditing()
|
|
461
|
+
l.addFeature(f)
|
|
462
|
+
l.commitChanges()
|
|
463
|
+
return l
|
|
464
|
+
|
|
465
|
+
def createPointGeoJSON(outGJ: str, p: QgsPoint) -> tuple[QgsVectorFileWriter.WriterError, str, str, str]:
|
|
466
|
+
g = QgsGeometry.fromPoint(p)
|
|
467
|
+
return createEmptyGeoJSON(outGJ, "point", g)
|
|
468
|
+
|
|
469
|
+
def createPrj():
|
|
470
|
+
if os.path.exists(ctx.prj):
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
global prj
|
|
474
|
+
prj = QgsProject.instance()
|
|
475
|
+
# XXX Add known vector layers
|
|
476
|
+
prj.write(ctx.prj)
|
|
477
|
+
|
|
478
|
+
def openPrj():
|
|
479
|
+
if not os.path.exists(ctx.prj):
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
global prj
|
|
483
|
+
prj = QgsProject.instance()
|
|
484
|
+
prj.read(ctx.prj)
|
|
485
|
+
|
|
486
|
+
def classifyGeometries(l: QgsVectorLayer, areas: QgsVectorLayer) -> QgsVectorLayer:
|
|
487
|
+
# - Classify each geometry in l by location
|
|
488
|
+
# - Have 7 criteria (intersects, ..., crosses) represented as 0/1
|
|
489
|
+
# - Append fields to l
|
|
490
|
+
m = l
|
|
491
|
+
for (p, pn) in locationPredicates:
|
|
492
|
+
m = classifyGeometries1(m, areas, p, pn)
|
|
493
|
+
# XXX l = packCriteria(l)
|
|
494
|
+
return m
|
|
495
|
+
|
|
496
|
+
def classifyGeometries1(l: QgsVectorLayer, areas: QgsVectorLayer, predicate, predicateName: str) -> QgsVectorLayer:
|
|
497
|
+
fields = QgsFields()
|
|
498
|
+
fields.append(QgsField('areas_%s' % predicateName, QVariant.Int))
|
|
499
|
+
|
|
500
|
+
m = makeVector("polygon", "XXX")
|
|
501
|
+
m.startEditing()
|
|
502
|
+
md = m.dataProvider()
|
|
503
|
+
md.addAttributes(fields)
|
|
504
|
+
m.updateFields()
|
|
505
|
+
areas.selectAll()
|
|
506
|
+
for f in areas.selectedFeatures():
|
|
507
|
+
g = QgsFeature()
|
|
508
|
+
g.setGeometry(f.geometry())
|
|
509
|
+
g.setFields(fields)
|
|
510
|
+
g['areas_%s' % predicateName] = 1
|
|
511
|
+
m.addFeature(g)
|
|
512
|
+
m.commitChanges()
|
|
513
|
+
|
|
514
|
+
return joinAttributesByLocation(
|
|
515
|
+
l,
|
|
516
|
+
m,
|
|
517
|
+
[predicate],
|
|
518
|
+
'memory:',
|
|
519
|
+
1
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def extractFields(l: QgsVectorLayer, typ: str, field: str, pattern: str) -> QgsVectorLayer:
|
|
523
|
+
print('extractFields: field=%s pattern=%s' % (field, pattern))
|
|
524
|
+
p = re.compile(pattern)
|
|
525
|
+
m = makeVector(typ, "XXX")
|
|
526
|
+
m.startEditing()
|
|
527
|
+
l.selectAll()
|
|
528
|
+
count = 0
|
|
529
|
+
for f in l.selectedFeatures():
|
|
530
|
+
v = f[field]
|
|
531
|
+
if v == None or v == 'NULL':
|
|
532
|
+
continue
|
|
533
|
+
print("field: %s" % v)
|
|
534
|
+
if p.match(str(v)) != None:
|
|
535
|
+
print("extractFields: match!")
|
|
536
|
+
count = count + 1
|
|
537
|
+
g = QgsFeature()
|
|
538
|
+
g.setGeometry(f.geometry())
|
|
539
|
+
m.addFeature(g)
|
|
540
|
+
m.commitChanges()
|
|
541
|
+
print("%d features matched" % count)
|
|
542
|
+
return m
|
|
543
|
+
|
|
544
|
+
def guessOrigin(l: QgsVectorLayer) -> typing.Union[QgsVectorLayer, None]:
|
|
545
|
+
l.selectAll()
|
|
546
|
+
for f in l.selectedFeatures():
|
|
547
|
+
bb: QgsRectangle = f.geometry().boundingBox()
|
|
548
|
+
oX = bb.xMinimum() - bb.width()
|
|
549
|
+
oY = bb.yMinimum() - bb.height()
|
|
550
|
+
p0 = QgsPointXY(oX, oY)
|
|
551
|
+
mp = [p0]
|
|
552
|
+
g = QgsGeometry.fromMultiPointXY(mp)
|
|
553
|
+
return createEmptyLayer("point", g)
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
# Represent 'areas' information in one integer (0xef)
|
|
557
|
+
# To reduce .geojson size
|
|
558
|
+
def packCriteria(l: QgsVectorLayer, m: QgsVectorLayer) -> QgsVectorLayer:
|
|
559
|
+
# Append 'areas'
|
|
560
|
+
fields = QgsFields()
|
|
561
|
+
fields.append(QgsField('areas', QVariant.Int))
|
|
562
|
+
l.startEditing()
|
|
563
|
+
ld = m.dataProvider()
|
|
564
|
+
ld.addAttributes(fields)
|
|
565
|
+
l.updateFields()
|
|
566
|
+
l.selectAll()
|
|
567
|
+
for f in l.selectedFeatures():
|
|
568
|
+
v = 0
|
|
569
|
+
for (i, n) in locationPredicates:
|
|
570
|
+
v |= f[n] << i
|
|
571
|
+
f['area'] = v
|
|
572
|
+
l.commitChanges()
|
|
573
|
+
|
|
574
|
+
# Trim 'areas_<predicateName>'
|
|
575
|
+
idxs = []
|
|
576
|
+
for (p, pn) in locationPredicates:
|
|
577
|
+
idxs.append(l.fields.indexFromName('areas_%s' % pn))
|
|
578
|
+
l.startEditing()
|
|
579
|
+
ld = m.dataProvider()
|
|
580
|
+
ld.deleteAttributes(idxs)
|
|
581
|
+
l.updateFields()
|
|
582
|
+
l.commitChanges()
|
|
583
|
+
|
|
584
|
+
return l
|
|
585
|
+
|
|
586
|
+
def tagAddresses(al: QgsVectorLayer, fname: str, srcShp, dstShp) -> QgsVectorLayer:
|
|
587
|
+
print("tagAddress: %d" % srcShp.featureCount())
|
|
588
|
+
al.selectAll()
|
|
589
|
+
|
|
590
|
+
fields = QgsFields()
|
|
591
|
+
fields.append(QgsField(fname, QVariant.Int))
|
|
592
|
+
|
|
593
|
+
m = makeVector("polygon", "XXX")
|
|
594
|
+
m.startEditing()
|
|
595
|
+
md = m.dataProvider()
|
|
596
|
+
md.addAttributes(fields)
|
|
597
|
+
m.updateFields()
|
|
598
|
+
|
|
599
|
+
#olayers = list()
|
|
600
|
+
for f in al.selectedFeatures():
|
|
601
|
+
# Rename attribute field: id -> address1
|
|
602
|
+
oid = f["id"] # Save
|
|
603
|
+
f.setFields(fields)
|
|
604
|
+
f[fname] = oid # Restore
|
|
605
|
+
m.addFeature(f)
|
|
606
|
+
|
|
607
|
+
m.commitChanges()
|
|
608
|
+
|
|
609
|
+
# Get buildings within this address's area
|
|
610
|
+
return joinAttributesByLocation(
|
|
611
|
+
srcShp,
|
|
612
|
+
m,
|
|
613
|
+
[5], # [within]
|
|
614
|
+
'memory:',
|
|
615
|
+
1,
|
|
616
|
+
True
|
|
617
|
+
)
|
|
618
|
+
#print("tagAddress: oid=%d: %d" % (oid, out.featureCount()))
|
|
619
|
+
#olayers.append(out)
|
|
620
|
+
#return mergeVectorLayers(olayers, dstShp)
|
|
621
|
+
|
|
622
|
+
def getAreas() -> QgsVectorLayer:
|
|
623
|
+
gj = ctx.areasGJ
|
|
624
|
+
return openVector('%s|geometrytype=MultiPolygon' % gj, "areas")
|
|
625
|
+
|
|
626
|
+
def getInternals() -> QgsVectorLayer:
|
|
627
|
+
gj = ctx.internalsGJ
|
|
628
|
+
return openVector('%s|geometrytype=MultiPolygon' % gj, "internals")
|
|
629
|
+
|
|
630
|
+
def getOrigin(gj: str) -> QgsPoint:
|
|
631
|
+
l = openVector('%s|geometrytype=Point' % gj, "origin")
|
|
632
|
+
f = next(l.getFeatures())
|
|
633
|
+
p = f.geometry().asPoint()
|
|
634
|
+
return p
|
|
635
|
+
|
|
636
|
+
def extentCenter(extent: QgsVectorLayer) -> QgsPoint:
|
|
637
|
+
f = next(extent.getFeatures())
|
|
638
|
+
ox = float(f['CNTX'])
|
|
639
|
+
oy = float(f['CNTY'])
|
|
640
|
+
return QgsPoint(ox, oy)
|
|
641
|
+
|
|
642
|
+
def getRoundedOrigin(extent: QgsVectorLayer) -> QgsPoint:
|
|
643
|
+
p = extentCenter(extent)
|
|
644
|
+
x = roundFloatToFracPrec(p.x(), 6)
|
|
645
|
+
y = roundFloatToFracPrec(p.y(), 6)
|
|
646
|
+
return QgsPoint(x, y)
|
|
647
|
+
|
|
648
|
+
def getViewbox():
|
|
649
|
+
(gj, name) = (None, None)
|
|
650
|
+
if os.path.exists(ctx.internals_extentGJ):
|
|
651
|
+
(gj, name) = (ctx.internals_extentGJ, "internals_extent")
|
|
652
|
+
else:
|
|
653
|
+
(gj, name) = (ctx.areas_extentGJ, "areas_extent")
|
|
654
|
+
extent = openVector('%s|geometrytype=Polygon' % gj, name)
|
|
655
|
+
f = next(extent.getFeatures())
|
|
656
|
+
minx = float(f['MINX'])
|
|
657
|
+
miny = float(f['MINY'])
|
|
658
|
+
maxx = float(f['MAXX'])
|
|
659
|
+
maxy = float(f['MAXY'])
|
|
660
|
+
p: QgsPoint = QgsPoint(minx, maxy)
|
|
661
|
+
q: QgsPoint = QgsPoint(maxx, miny)
|
|
662
|
+
|
|
663
|
+
o: QgsPoint = getOrigin(ctx.originGJ)
|
|
664
|
+
|
|
665
|
+
lop = QgsGeometry.fromPolyline(QgsLineString([o, p]))
|
|
666
|
+
lpq = QgsGeometry.fromPolyline(QgsLineString([p, q]))
|
|
667
|
+
|
|
668
|
+
m = createMeasures()
|
|
669
|
+
fields = m.fields()
|
|
670
|
+
|
|
671
|
+
m.startEditing()
|
|
672
|
+
for l in [lop, lpq]:
|
|
673
|
+
f = QgsFeature()
|
|
674
|
+
f.setGeometry(l)
|
|
675
|
+
f.setFields(fields)
|
|
676
|
+
m.addFeature(f)
|
|
677
|
+
m.commitChanges()
|
|
678
|
+
|
|
679
|
+
return m
|
|
680
|
+
|
|
681
|
+
# if fracprec = 3:
|
|
682
|
+
# 1.11111 -> 1.111
|
|
683
|
+
# 11.11111 -> 11.111
|
|
684
|
+
# 111.11111 -> 111.111
|
|
685
|
+
def roundFloatToFracPrec(n: float, fracprec: int) -> float:
|
|
686
|
+
print('roundFloatToFracPrec n=%f fracprec=%f' % (n, fracprec))
|
|
687
|
+
prec = round(math.log(abs(n), 10)) + 6
|
|
688
|
+
print('roundFloatToFracPrec prec=%f' % prec)
|
|
689
|
+
decimal.getcontext().prec = prec
|
|
690
|
+
return float(decimal.Decimal(n) * decimal.Decimal(1))
|
|
691
|
+
|
|
692
|
+
def getMeasures(extent: QgsVectorLayer, origin: QgsVectorLayer) -> QgsVectorLayer:
|
|
693
|
+
f = next(extent.getFeatures())
|
|
694
|
+
maxx = float(f['MAXX'])
|
|
695
|
+
miny = float(f['MINY'])
|
|
696
|
+
|
|
697
|
+
f = next(origin.getFeatures())
|
|
698
|
+
g = f.geometry()
|
|
699
|
+
o = QgsPoint(g.asPoint())
|
|
700
|
+
|
|
701
|
+
p = QgsPoint(maxx, o.y())
|
|
702
|
+
q = QgsPoint(o.x(), miny)
|
|
703
|
+
dp = o.distance(p.x(), p.y())
|
|
704
|
+
dq = o.distance(q.x(), q.y())
|
|
705
|
+
|
|
706
|
+
lp = QgsGeometry.fromPolyline(QgsLineString([o, p]))
|
|
707
|
+
lq = QgsGeometry.fromPolyline(QgsLineString([o, q]))
|
|
708
|
+
invx = -1 if (o.x() > p.x()) else 1
|
|
709
|
+
invy = -1 if (o.y() < q.y()) else 1
|
|
710
|
+
edp = calcEllipsoidalDistance(lp) * invx
|
|
711
|
+
edq = calcEllipsoidalDistance(lq) * invy
|
|
712
|
+
|
|
713
|
+
m = createMeasures()
|
|
714
|
+
fields = m.fields()
|
|
715
|
+
|
|
716
|
+
m.startEditing()
|
|
717
|
+
for (dir, l, d, ed) in [('x', lp, dp, edp), ('y', lq, dq, edq)]:
|
|
718
|
+
f = QgsFeature()
|
|
719
|
+
f.setGeometry(l)
|
|
720
|
+
f.setFields(fields)
|
|
721
|
+
f['direction'] = dir
|
|
722
|
+
f['distance'] = d
|
|
723
|
+
f['ellipsoidal_distance'] = ed
|
|
724
|
+
m.addFeature(f)
|
|
725
|
+
m.commitChanges()
|
|
726
|
+
|
|
727
|
+
return m
|
|
728
|
+
|
|
729
|
+
def createMeasures() -> QgsVectorLayer:
|
|
730
|
+
fields = QgsFields()
|
|
731
|
+
fields.append(QgsField('direction', QVariant.String))
|
|
732
|
+
fields.append(QgsField('distance', QVariant.Double))
|
|
733
|
+
fields.append(QgsField('ellipsoidal_distance', QVariant.Double))
|
|
734
|
+
|
|
735
|
+
m = makeVector("linestring", "XXX")
|
|
736
|
+
m.startEditing()
|
|
737
|
+
md = m.dataProvider()
|
|
738
|
+
md.addAttributes(fields)
|
|
739
|
+
m.updateFields()
|
|
740
|
+
m.commitChanges()
|
|
741
|
+
|
|
742
|
+
return m
|
|
743
|
+
|
|
744
|
+
def createViewbox() -> QgsVectorLayer:
|
|
745
|
+
fields = QgsFields()
|
|
746
|
+
|
|
747
|
+
m = makeVector("linestring", "XXX")
|
|
748
|
+
m.startEditing()
|
|
749
|
+
md = m.dataProvider()
|
|
750
|
+
md.addAttributes(fields)
|
|
751
|
+
m.updateFields()
|
|
752
|
+
m.commitChanges()
|
|
753
|
+
|
|
754
|
+
return m
|
|
755
|
+
|
|
756
|
+
# XXX
|
|
757
|
+
# XXX refactor
|
|
758
|
+
# XXX
|
|
759
|
+
def fixupAttributes(prefix: str, l: QgsVectorLayer, outGeoJSON, origin: QgsPoint) -> tuple[QgsVectorFileWriter.WriterError, str, str, str]:
|
|
760
|
+
addrTmpl = '%s-%%s-%%s-%%d' % prefix
|
|
761
|
+
|
|
762
|
+
#fields = QgsFields()
|
|
763
|
+
ofields: QgsFields = l.dataProvider().fields()
|
|
764
|
+
fields: QgsFields = QgsFields(ofields)
|
|
765
|
+
fields.append(QgsField('address1', QVariant.Double))
|
|
766
|
+
fields.append(QgsField('address2', QVariant.Double))
|
|
767
|
+
fields.append(QgsField('distance', QVariant.Double))
|
|
768
|
+
fields.append(QgsField('rank', QVariant.Double))
|
|
769
|
+
fields.append(QgsField('address', QVariant.String))
|
|
770
|
+
|
|
771
|
+
m: QgsVectorLayer = makeVector("polygon", "XXX")
|
|
772
|
+
m.startEditing()
|
|
773
|
+
md: QgsVectorDataProvider = m.dataProvider()
|
|
774
|
+
md.addAttributes(fields)
|
|
775
|
+
m.updateFields()
|
|
776
|
+
|
|
777
|
+
l.selectAll()
|
|
778
|
+
|
|
779
|
+
# Create a dict: (address1, address2) => [(id, distance)]
|
|
780
|
+
# Then order & number fields
|
|
781
|
+
# For example, 5 A-1f-2-3 fields are numbered as A-1f-2-3-1, A-1f-2-3-2, ...
|
|
782
|
+
print('Making addrs...')
|
|
783
|
+
addrs = {}
|
|
784
|
+
for f in l.selectedFeatures():
|
|
785
|
+
v1 = f['address1']
|
|
786
|
+
v2 = f['address2']
|
|
787
|
+
# Distance between f.centroid() and origin
|
|
788
|
+
d = f.geometry().centroid().asPoint().distance(origin.x(), origin.y())
|
|
789
|
+
k = (v1, v2)
|
|
790
|
+
v = (f.id(), d)
|
|
791
|
+
if k not in addrs:
|
|
792
|
+
addrs[k] = [v]
|
|
793
|
+
else:
|
|
794
|
+
addrs[k].append(v)
|
|
795
|
+
|
|
796
|
+
print('Making ranks...')
|
|
797
|
+
ranks = {}
|
|
798
|
+
for _, ovs in addrs.items():
|
|
799
|
+
# ov[0] == id
|
|
800
|
+
# ov[1] == distance
|
|
801
|
+
ovs.sort(key = lambda ov: ov[1])
|
|
802
|
+
for i, ov in enumerate(ovs):
|
|
803
|
+
ranks[ov[0]] = (i, ov[1])
|
|
804
|
+
|
|
805
|
+
for f in l.selectedFeatures():
|
|
806
|
+
v1 = f['address1']
|
|
807
|
+
v2 = f['address2']
|
|
808
|
+
(i, d) = ranks[f.id()]
|
|
809
|
+
r = i + 1 # address starts from 1
|
|
810
|
+
|
|
811
|
+
g = QgsFeature()
|
|
812
|
+
g.setGeometry(f.geometry())
|
|
813
|
+
g.setFields(fields)
|
|
814
|
+
for i in ofields:
|
|
815
|
+
fn = i.name()
|
|
816
|
+
g[fn] = f[fn]
|
|
817
|
+
g['address1'] = v1
|
|
818
|
+
g['address2'] = v2
|
|
819
|
+
g['distance'] = d
|
|
820
|
+
g['rank'] = r
|
|
821
|
+
g['address'] = addrTmpl % (v1, v2, r)
|
|
822
|
+
m.addFeature(g)
|
|
823
|
+
|
|
824
|
+
m.commitChanges()
|
|
825
|
+
|
|
826
|
+
tx = m.transformContext()
|
|
827
|
+
opts = QgsVectorFileWriter.SaveVectorOptions()
|
|
828
|
+
opts.driverName = "GeoJSON"
|
|
829
|
+
return QgsVectorFileWriter.writeAsVectorFormatV3(m, outGeoJSON, tx, opts)
|
|
830
|
+
|
|
831
|
+
def fixupVectorLayer(l: QgsVectorLayer) -> QgsVectorLayer:
|
|
832
|
+
provider = l.dataProvider()
|
|
833
|
+
caps = provider.capabilities()
|
|
834
|
+
if not (caps & QgsVectorDataProvider.ChangeGeometries):
|
|
835
|
+
return l
|
|
836
|
+
if not (caps & QgsVectorDataProvider.ChangeAttributeValues):
|
|
837
|
+
return l
|
|
838
|
+
|
|
839
|
+
# 0. de-duplicate features by osm_id/osm_way_id
|
|
840
|
+
all_osm_ids = {}
|
|
841
|
+
dups = {}
|
|
842
|
+
l.selectAll()
|
|
843
|
+
for f in l.selectedFeatures():
|
|
844
|
+
m = f.attributeMap()
|
|
845
|
+
osm_id = m['osm_id'] if ('osm_id' in m and m['osm_id'] != None) else m['osm_way_id'] if ('osm_way_id' in m and m['osm_way_id'] != None) else ''
|
|
846
|
+
if osm_id == '' or osm_id in all_osm_ids:
|
|
847
|
+
dups[f.id()] = osm_id
|
|
848
|
+
else:
|
|
849
|
+
all_osm_ids[osm_id] = 1
|
|
850
|
+
if dups != {}:
|
|
851
|
+
print('deleting duplicate features: ', list(dups.values()))
|
|
852
|
+
provider.deleteFeatures(list(dups.keys()))
|
|
853
|
+
|
|
854
|
+
# 1. fixup clockwise-ness
|
|
855
|
+
l.selectAll()
|
|
856
|
+
for f in l.selectedFeatures():
|
|
857
|
+
# fixup multipolygon clockwise-ness for SVG rendering
|
|
858
|
+
fid = f.id()
|
|
859
|
+
provider.changeGeometryValues({ fid: f.geometry().forcePolygonClockwise() })
|
|
860
|
+
|
|
861
|
+
# 2. add area field
|
|
862
|
+
sourceCrs = QgsCoordinateReferenceSystem.fromOgcWmsCrs("EPSG:4326")
|
|
863
|
+
destCrs = QgsCoordinateReferenceSystem.fromOgcWmsCrs("EPSG:3857") # XXX
|
|
864
|
+
tr = QgsCoordinateTransform(sourceCrs, destCrs, prj)
|
|
865
|
+
|
|
866
|
+
area_field = QgsField("area", QVariant.Double)
|
|
867
|
+
provider.addAttributes([area_field])
|
|
868
|
+
l.updateFields()
|
|
869
|
+
|
|
870
|
+
idx = provider.fieldNameIndex("area")
|
|
871
|
+
|
|
872
|
+
for f in l.getFeatures():
|
|
873
|
+
fid = f.id()
|
|
874
|
+
g = f.geometry()
|
|
875
|
+
g.transform(tr)
|
|
876
|
+
area = g.area()
|
|
877
|
+
if area > 0:
|
|
878
|
+
attrs = { idx: area }
|
|
879
|
+
provider.changeAttributeValues({ fid: attrs })
|
|
880
|
+
|
|
881
|
+
# 3. add centroid_x/centroid_y fields
|
|
882
|
+
centroid_x = QgsField("centroid_x", QVariant.Double)
|
|
883
|
+
centroid_y = QgsField("centroid_y", QVariant.Double)
|
|
884
|
+
provider.addAttributes([centroid_x, centroid_y])
|
|
885
|
+
l.updateFields()
|
|
886
|
+
|
|
887
|
+
idx_x = provider.fieldNameIndex("centroid_x")
|
|
888
|
+
idx_y = provider.fieldNameIndex("centroid_y")
|
|
889
|
+
|
|
890
|
+
for f in l.getFeatures():
|
|
891
|
+
fid = f.id()
|
|
892
|
+
g = f.geometry()
|
|
893
|
+
centroid = g.centroid()
|
|
894
|
+
for part in centroid.constParts():
|
|
895
|
+
for v in part.vertices():
|
|
896
|
+
attrs = { idx_x: v.x(), idx_y: v.y() }
|
|
897
|
+
provider.changeAttributeValues({ fid: attrs })
|
|
898
|
+
break
|
|
899
|
+
break
|
|
900
|
+
|
|
901
|
+
return l
|
|
902
|
+
|
|
903
|
+
def calcEllipsoidalDistance(g: QgsGeometry) -> float:
|
|
904
|
+
d = QgsDistanceArea()
|
|
905
|
+
d.setEllipsoid('WGS84')
|
|
906
|
+
return d.measureLength(g)
|
|
907
|
+
|
|
908
|
+
def copyFeature(f: QgsFeature, fields: QgsFields) -> QgsFeature:
|
|
909
|
+
g = QgsFeature()
|
|
910
|
+
g.setGeometry(f.geometry())
|
|
911
|
+
g.setFields(fields)
|
|
912
|
+
for i in fields:
|
|
913
|
+
fn = i.name()
|
|
914
|
+
#print('fn', fn)
|
|
915
|
+
g[fn] = f[fn]
|
|
916
|
+
return g
|
|
917
|
+
|
|
918
|
+
def openVector(uri: str, name: str) -> QgsVectorLayer:
|
|
919
|
+
return QgsVectorLayer(uri, name, "ogr")
|
|
920
|
+
|
|
921
|
+
# https://qgis.org/pyqgis/master/core/QgsVectorLayer.html
|
|
922
|
+
# memory data provider
|
|
923
|
+
# geometry type: “point”, “linestring”, “polygon”, “multipoint”, ”multilinestring”, ”multipolygon”
|
|
924
|
+
def makeVector(uri: str, name: str) -> QgsVectorLayer:
|
|
925
|
+
return QgsVectorLayer(uri, name, "memory")
|
|
926
|
+
|
|
927
|
+
####
|
|
928
|
+
|
|
929
|
+
def makeExtent():
|
|
930
|
+
areas = openVector(ctx.areasGJ, "areas")
|
|
931
|
+
extent = getExtent(areas, 'memory:')
|
|
932
|
+
res = dumpGeoJSON(extent, ctx.areas_extentGJ)
|
|
933
|
+
print(res)
|
|
934
|
+
|
|
935
|
+
if os.path.exists(ctx.internalsGJ):
|
|
936
|
+
internals = openVector(ctx.internalsGJ, "internals")
|
|
937
|
+
internals_extent = getExtent(internals, 'memory:')
|
|
938
|
+
res = dumpGeoJSON(internals_extent, ctx.internals_extentGJ)
|
|
939
|
+
print(res)
|
|
940
|
+
else:
|
|
941
|
+
shutil.copy(ctx.areasGJ, ctx.internalsGJ)
|
|
942
|
+
shutil.copy(ctx.areas_extentGJ, ctx.internals_extentGJ)
|
|
943
|
+
|
|
944
|
+
def makeOrigin():
|
|
945
|
+
areas = openVector(ctx.areasGJ, "areas")
|
|
946
|
+
extent = openVector(ctx.areas_extentGJ, "areas_extent")
|
|
947
|
+
|
|
948
|
+
l = extent.transformContext()
|
|
949
|
+
g = getRoundedOrigin(extent)
|
|
950
|
+
origin = createEmptyLayer("point", g)
|
|
951
|
+
res = dumpGeoJSON(origin, ctx.originGJ)
|
|
952
|
+
print(res)
|
|
953
|
+
|
|
954
|
+
def makeMeasures():
|
|
955
|
+
areas = openVector(ctx.areasGJ, "areas")
|
|
956
|
+
extent = openVector(ctx.areas_extentGJ, "areas_extent")
|
|
957
|
+
origin = openVector(ctx.originGJ, "origin")
|
|
958
|
+
|
|
959
|
+
measures = getMeasures(extent, origin)
|
|
960
|
+
res = dumpGeoJSON(measures, ctx.measuresGJ)
|
|
961
|
+
print(res)
|
|
962
|
+
|
|
963
|
+
def makeViewbox():
|
|
964
|
+
#extent = openVector(ctx.areas_extentGJ, "areas_extent")
|
|
965
|
+
#origin = openVector(ctx.originGJ, "origin")
|
|
966
|
+
|
|
967
|
+
viewbox = getViewbox()
|
|
968
|
+
res = dumpGeoJSON(viewbox, ctx.viewboxGJ)
|
|
969
|
+
print(res)
|