export-cad-layers.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. # -*- coding: utf-8 -*-
  2. """Report extents and area statistics for chosen CAD layers, and export them to shapefile.
  3. Run with ArcGIS Desktop Python 2.7:
  4. python.exe tools/city-massing/export-cad-layers.py <cad.gdb> <outDir> [layer,layer,...]
  5. Only the named layers are exported, so the delivered shapefile is a deliberate subset rather than
  6. a dump of the whole survey drawing.
  7. """
  8. import sys
  9. import os
  10. import io
  11. import json
  12. import arcpy
  13. def polygon_area_stats(layer):
  14. areas = []
  15. with arcpy.da.SearchCursor(layer, ['SHAPE@AREA']) as cursor:
  16. for row in cursor:
  17. value = row[0]
  18. # Drawing-space entities (title blocks, symbols) have no meaningful area here.
  19. if value is None:
  20. continue
  21. areas.append(float(value))
  22. if not areas:
  23. return {}
  24. areas.sort()
  25. pick = lambda p: round(areas[int(p * (len(areas) - 1))], 1)
  26. return {
  27. 'count': len(areas),
  28. 'minSqm': round(areas[0], 1),
  29. 'p25Sqm': pick(0.25),
  30. 'medianSqm': pick(0.5),
  31. 'p75Sqm': pick(0.75),
  32. 'p95Sqm': pick(0.95),
  33. 'maxSqm': round(areas[-1], 1),
  34. 'under15Sqm': sum(1 for a in areas if a < 15),
  35. 'over3000Sqm': sum(1 for a in areas if a > 3000),
  36. }
  37. def main():
  38. if len(sys.argv) < 3:
  39. print('usage: export-cad-layers.py <cad.gdb> <outDir> [layer,layer,...] [window=XMIN,YMIN,XMAX,YMAX]')
  40. return 2
  41. gdb = os.path.abspath(sys.argv[1])
  42. out_dir = os.path.abspath(sys.argv[2])
  43. wanted = None
  44. window = None
  45. for arg in sys.argv[3:]:
  46. if arg.startswith('window='):
  47. window = [float(v) for v in arg[len('window='):].split(',')]
  48. elif wanted is None:
  49. wanted = arg.split(',')
  50. if not os.path.isdir(out_dir):
  51. os.makedirs(out_dir)
  52. arcpy.env.workspace = gdb
  53. targets = []
  54. for dataset in arcpy.ListDatasets() or []:
  55. for name in arcpy.ListFeatureClasses(feature_dataset=dataset) or []:
  56. targets.append(name)
  57. # The drawing carries no coordinate system, but its survey coordinates are CGCS2000
  58. # 3-degree Gauss-Kruger zone CM 111E (EPSG:4546), the same system as the sibling shapefiles.
  59. SURVEY_SR = arcpy.SpatialReference(4546)
  60. report = {'gdb': gdb, 'assumedSpatialReference': 'CGCS2000_3_Degree_GK_CM_111E (EPSG:4546)', 'window': window, 'layers': []}
  61. print('assumed spatial reference: %s' % report['assumedSpatialReference'])
  62. def in_window(shape):
  63. if window is None:
  64. return True
  65. try:
  66. extent = shape.extent
  67. cx = (extent.XMin + extent.XMax) / 2.0
  68. cy = (extent.YMin + extent.YMax) / 2.0
  69. except Exception:
  70. # MultiPatch and other 3D/unsupported shapes fall back to their envelope.
  71. return True
  72. return (window[0] <= cx <= window[2]) and (window[1] <= cy <= window[3])
  73. for name in targets:
  74. describe = arcpy.Describe(name)
  75. entry = {'featureClass': name, 'featureType': describe.shapeType, 'featureCount': 0, 'inWindow': 0}
  76. areas = []
  77. with arcpy.da.SearchCursor(name, ['SHAPE@', 'Layer'] if 'Layer' in [f.name for f in arcpy.ListFields(name)] else ['SHAPE@']) as cursor:
  78. for row in cursor:
  79. entry['featureCount'] += 1
  80. shape = row[0]
  81. if shape is None or not in_window(shape):
  82. continue
  83. entry['inWindow'] += 1
  84. if describe.shapeType == 'Polygon':
  85. try:
  86. if shape.area is not None:
  87. areas.append(float(shape.area))
  88. except Exception:
  89. pass
  90. if areas:
  91. areas.sort()
  92. pick = lambda p: round(areas[int(p * (len(areas) - 1))], 1)
  93. entry['areaInWindow'] = {
  94. 'count': len(areas), 'minSqm': round(areas[0], 1), 'p25Sqm': pick(0.25),
  95. 'medianSqm': pick(0.5), 'p75Sqm': pick(0.75), 'p95Sqm': pick(0.95),
  96. 'maxSqm': round(areas[-1], 1),
  97. 'under15Sqm': sum(1 for a in areas if a < 15),
  98. 'over3000Sqm': sum(1 for a in areas if a > 3000),
  99. }
  100. report['layers'].append(entry)
  101. print('%-12s %-9s total=%6d inWindow=%6d' % (name, entry['featureType'], entry['featureCount'], entry['inWindow']))
  102. if 'areaInWindow' in entry:
  103. print(' %s' % json.dumps(entry['areaInWindow']))
  104. if not wanted:
  105. continue
  106. fields = [f.name for f in arcpy.ListFields(name)]
  107. if 'Layer' not in fields:
  108. continue
  109. # MultiPatch repeats the polygon geometry in 3D and Annotation is text placement;
  110. # neither is a footprint source, so only true 2D geometry is exported.
  111. if describe.shapeType not in ('Polygon', 'Polyline', 'Point'):
  112. print(' skipping %s (%s)' % (name, describe.shapeType))
  113. continue
  114. for layer_name in wanted:
  115. safe = layer_name.encode('ascii', 'replace').replace('?', '_')
  116. out_shp = os.path.join(out_dir, '%s-%s.shp' % (name, safe))
  117. where = "Layer = '%s'" % layer_name.replace("'", "''")
  118. selected = os.path.join(out_dir, '_sel.shp')
  119. for stale in (selected, out_shp):
  120. if arcpy.Exists(stale):
  121. arcpy.Delete_management(stale)
  122. arcpy.Select_analysis(name, selected, where)
  123. n = int(arcpy.GetCount_management(selected).getOutput(0))
  124. if n == 0:
  125. arcpy.Delete_management(selected)
  126. print(' no features for layer %s in %s' % (layer_name, name))
  127. continue
  128. # The drawing carries no CRS: declare the survey system, then reproject to WGS84.
  129. # Spatial clipping happens downstream in Node, where Turf already owns those rules.
  130. arcpy.DefineProjection_management(selected, SURVEY_SR)
  131. arcpy.Project_management(selected, out_shp, arcpy.SpatialReference(4326))
  132. arcpy.Delete_management(selected)
  133. final = int(arcpy.GetCount_management(out_shp).getOutput(0))
  134. print(' exported %-10s %-9s %6d features -> %s' % (layer_name, name, final, os.path.basename(out_shp)))
  135. entry.setdefault('exported', []).append({'layer': layer_name, 'features': final, 'shapefile': os.path.basename(out_shp)})
  136. out = os.path.join(out_dir, 'cad-layer-report.json')
  137. with io.open(out, 'w', encoding='utf-8') as handle:
  138. handle.write(unicode(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)))
  139. print('report: %s' % out)
  140. return 0
  141. if __name__ == '__main__':
  142. sys.exit(main())