
Доброго времени, Хабр!
Немного истории: в ходе учебы программированию, искал я себе реальную задачу, да такую чтобы с пользой. Нашел. Увидел как знакомый геодезист, на работе, считает объем земельного участка. Очень долго и нудно…
Геодезический расчет объемов:
При возведении жилых сооружений, высокотехнологичных помещений, автомобильных и железных дорог, а так – же в целях определения объемов строительных материалов и подсчета объема земляных работ, требуется помощь геодезистов. Они “отстреливают” территорию, разбивая всю площадь на так называемую геосетку, далее полученые точки из прибора выгружаются в autoCAD и высчитывают объем всей территории. Ниже пример геосетки:

Каждая точка имеет свои координаты (x,y,z-высота) относительно балтийского моря. По порядку берутся 4 точки как показано на предыдущем изображении и вычисляется исходя из координат этих точек объем участка земли, так вычисляются все объемы получившихся на сетке фигур, плюсуются и в итоге мы имеем объем всей территории.
Работа очень кропотливая, а так как я уже писал, я хотел взяться за реальный проект, взамен на опыт. Поэтому я даже не стал рассматривать тот, вариант, что давно уже все есть до меня.
Несколько дней я был заморочен на алгоритме поиска соседей, в результате вышло не совсем то чего я ждал, алгоритм работал как хотел в связи с тем, что сетка не ровная как в теории, а кривая как ходил помощник геодезиста по полю и ставил отметки, затем я понял что и AutoCAD очень просто сортирует по времени создания точки. Далее немного включившись в AutoCAD я заметил интересный порядок выгрузки точек в XML файл, и нашел возможность назначения каждой точки ID, теперь мы назначили каждой точке имя с левого угла до правого края и так-же понимаясь на ряд выше про именовали все точки (это куда быстрее, чем два дня считать объем территории площадью 500х700 метров). Ниже пример такого готового файла с именованными по порядку точками:

По идее объемы считаются по пифагору, но фигуры совершенно разные и как показывает практика очень редко встречаются квадраты и прямоугольники. Поэтому я прибегнул к формуле Герона в своих вычислениях. То есть принцип софтины, что я написал таков: Читаю XML файл, собираю точки, далее массив с массивами четырех угольников, получаю все фигуры которым нужно посчитать объем, теперь перед расчетом объема я проверяю по сторонам фигур(вектора) и углам на прямоугольник и квадрат (большая редкость, в случае конкретно нашего геодезиста, что таковые будут, и если есть то применяю пифагора) а фигуры типа трапеции, параллелепипеда и прочие я просто принимаю за неизвестный четырех угольник и считаю их по формуле Герона, делю по диагонали фигуру на два треугольника считаю их площади в пространстве (учитываю высоту), высота, берется самая высокая из 4-х точек (так мне объяснил, мой коллега-геодезист) и плюсуя площади двух треугольников далее я уже исходя из общей площади фигуры получаю ее объем.
Плюсую все эти объемы и получаю за 0.2 секунды результат к которому он идет пару дней, проверяли на трех проектах пока данные сходятся, программой даже более точнее получается объем. Продолжаем ее тестировать. Теперь на днях я решил прикрутить этот код к юзабельному интерфейсу, с PyQt4, мой первый опыт работы с написанием графики, ниже я опубликую код который относится только к GUI.
from PyQt4 import QtGui import sys import cvgLeicaXmlReader import cvgMath class myWindow(QtGui.QMainWindow): def __init__(self, parent=None): QtGui.QWidget.__init__(self, parent) self.setWindowTitle('CVG2014') self.setFixedSize(350, 350) self.setWindowIcon(QtGui.QIcon('static/Icon.png')) self.setStyleSheet("QMainWindow {background-image: url(static/background.png);}") self.directory = '' self.file = '' self.labelFilename = QtGui.QLabel('Select .XML file with points', self) self.labelFilename.setFixedWidth(300) self.labelFilename.setFixedHeight(25) self.labelFilename.move(10, 5) self.labelFilename.setStyleSheet("QLabel { background-color: white; \ border: 1px solid grey; \ color: grey;}") self.SB_WidthOfXAxis = QtGui.QSpinBox(self) self.SB_WidthOfXAxis.move(10, 35) self.SB_WidthOfXAxis.setFixedWidth(50) self.SB_WidthOfXAxis.setMaximum(9999) self.labelPointsWidth = QtGui.QLabel('Length of points on X axis', self) self.labelPointsWidth.setFixedWidth(250) self.labelPointsWidth.setFixedHeight(25) self.labelPointsWidth.move(65, 47.5) self.SB_HeightAboveSeaLevel = QtGui.QDoubleSpinBox(self) self.SB_HeightAboveSeaLevel.move(10, 75) self.SB_HeightAboveSeaLevel.setFixedWidth(50) self.SB_HeightAboveSeaLevel.setRange(-9999.99, 9999.99) self.labelPointsSeaLevel = QtGui.QLabel('Height above sea level', self) self.labelPointsSeaLevel.setFixedWidth(250) self.labelPointsSeaLevel.setFixedHeight(25) self.labelPointsSeaLevel.move(65, 87) self.buttonOpenFile = QtGui.QPushButton('...', self) self.buttonOpenFile.setFixedWidth(30) self.buttonOpenFile.setFixedHeight(27) self.buttonOpenFile.move(311, 4) self.buttonOpenFile.clicked.connect(self.getXmlFile) self.buttonGetVolume = QtGui.QPushButton('RUN', self) self.buttonGetVolume.setFixedWidth(52) self.buttonGetVolume.setFixedHeight(35) self.buttonGetVolume.move(9, 115) self.buttonGetVolume.clicked.connect(self.getVolume) self.showVolume = QtGui.QLabel('Get Vol', self) self.showVolume.setFixedWidth(140) self.showVolume.setFixedHeight(32) self.showVolume.move(72, 117) self.showVolume.setStyleSheet("QLabel { background-color: white; \ border: 1px solid grey; \ color: grey;}") def getVolume(self): xml_file = self.getFileName() points = cvgLeicaXmlReader.getPointsFromXmlFile(xml_file) if(xml_file and points): # length_of_points = len(points) QUANTITY_POINTS_AT_X_AXIS = self.SB_WidthOfXAxis.value() STATIC_HEIGHT = self.SB_HeightAboveSeaLevel.value() rows = cvgLeicaXmlReader.getRowsFromPoints(points, QUANTITY_POINTS_AT_X_AXIS) quads = cvgLeicaXmlReader.getAllQuads(rows) volumes = [] if (STATIC_HEIGHT or QUANTITY_POINTS_AT_X_AXIS) != 0: for quadrangle in quads: Quadrangle_type = cvgMath.getTypeQuadrangle(quadrangle) v = cvgMath.getVolumeQuadrangle(quadrangle, Quadrangle_type, STATIC_HEIGHT) volumes.append(v) else: volumes = 0 volumes = 0 if STATIC_HEIGHT == 0 else (round(sum(volumes), 3)) result = '-'+str(volumes) if STATIC_HEIGHT < 0 else str(volumes) result = '0' if result == '-0' else result self.showVolume.setStyleSheet("QLabel { background-color: white; \ border: 1px solid grey; \ color: grey;}") self.showVolume.setText(result) else: self.showVolume.setText('Select the correct file!') self.showVolume.setStyleSheet("QLabel { background-color: white; \ border: 1px solid grey; \ color: red; \ font-weight: bold}") def getFileName(self): return self.file def getXmlFile(self): sender = self.sender() path = QtGui.QFileDialog.getOpenFileName(sender, 'Open Xml file with points', self.directory, 'XML *.xml') fileName = path[path.rfind('/')+1:] self.directory = path[:path.rfind('/')] if(len(path) > 54): start = len(path)-54 pathSlice = path[start:] pathSlice = pathSlice[pathSlice.find('/'):] pathSlice = '..'+pathSlice else: pathSlice = path self.labelFilename.setText(pathSlice) print(len(path)) self.file = path if __name__ == '__main__': app = QtGui.QApplication(sys.argv) win = myWindow() win.show() app.exec_()
С остальными модулями кому интересно, можно ознакомиться на гитхабе
Так же я загрузил .rar архив со скомпилированным кодом в exe для windows.

Что требуется от нас: открываем XML файл с точками (два файла для примера лежат так-же в на гите (2 корректных и один битый, для теста)), указываем количество точек по ширине поля, ниже указываем высоту относительно уровня моря, и ждем кнопку RUN, получаем объем в поле с право от кнопки запуска.
Если честно я не знаю, можно ли применять этот инструмент на производстве. Но свое желание я удовлетворил. Всем спасибо за внимание.
Файл с 65 точками (ширина 13 точек)
Файл с 78 точками (ширина 13 точек)
Модуль чтения XML файла
Модуль с математическими вычислениями
Модуль визуального оформления
Архив со скомпилированным кодом в Exe
