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.
Files changed (55) hide show
  1. package/.vscode/settings.json +5 -0
  2. package/LICENSE +15 -0
  3. package/__pycache__/common.cpython-312.pyc +0 -0
  4. package/dist/geojsonToTs.js +15 -0
  5. package/eslint.config.js +58 -0
  6. package/oxlintrc.json +4 -0
  7. package/package.json +35 -0
  8. package/prettier.config.js +14 -0
  9. package/rslib.config.ts +22 -0
  10. package/rstest.config.ts +5 -0
  11. package/scripts/__pycache__/common.cpython-312.pyc +0 -0
  12. package/scripts/classifyGeometries.py +56 -0
  13. package/scripts/common.py +969 -0
  14. package/scripts/copy.sh +36 -0
  15. package/scripts/extractAreas.py +41 -0
  16. package/scripts/extractAreas.sh +20 -0
  17. package/scripts/extractLinesByIds.py +48 -0
  18. package/scripts/extractMultiLineStringsByIds.py +48 -0
  19. package/scripts/extractPointsByIds.py +42 -0
  20. package/scripts/extractPolygonsByIds.py +42 -0
  21. package/scripts/geojson2ts.py +79 -0
  22. package/scripts/getOsm.sh +62 -0
  23. package/scripts/initQgisPrj.py +44 -0
  24. package/scripts/makeAreas.py +21 -0
  25. package/scripts/makeAreas.sh +20 -0
  26. package/scripts/makeExtent.py +21 -0
  27. package/scripts/makeMeasures.py +25 -0
  28. package/scripts/makeOrigin.py +30 -0
  29. package/scripts/makeViewbox.py +22 -0
  30. package/scripts/osmconf.ini +122 -0
  31. package/scripts/pyqgis-Ubuntu.sh +36 -0
  32. package/scripts/pyqgis-macOS.sh +40 -0
  33. package/scripts/pyqgis.sh +34 -0
  34. package/scripts/readOsm.py +37 -0
  35. package/scripts/readOsm.sh +27 -0
  36. package/scripts/regen.sh +21 -0
  37. package/scripts/run-common.sh +3 -0
  38. package/scripts/tagAddresses.py +51 -0
  39. package/scripts/update.sh +53 -0
  40. package/src/geojsonToTs.ts +15 -0
  41. package/src/lib/geojson/geojson-print.test.ts +60 -0
  42. package/src/lib/geojson/geojson-print.ts +211 -0
  43. package/src/lib/geojson/geojson-schema.test.ts +42 -0
  44. package/src/lib/geojson/geojson-schema.ts +151 -0
  45. package/src/lib/geojson/geojson-types.ts +116 -0
  46. package/src/lib/osm.test.ts +17 -0
  47. package/src/lib/osm.ts +18 -0
  48. package/src/lib/print-utils.test.ts +12 -0
  49. package/src/lib/print-utils.ts +22 -0
  50. package/src/lib/print.ts +122 -0
  51. package/test/geojson.json +22 -0
  52. package/tsconfig.app.json +34 -0
  53. package/tsconfig.json +7 -0
  54. package/tsconfig.node-browser.json +27 -0
  55. 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)