def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join(self.plugin_dir, 'i18n', 'SurvexImport_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr(u'&Import .3d file') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'SurvexImport') self.toolbar.setObjectName(u'SurvexImport') self.dlg = SurvexImportDialog() self.dlg.selectedFile.clear() self.dlg.fileSelector.clicked.connect(self.select_3d_file)
def __init__(self, iface): """Constructor""" self.iface = iface # Save reference to the QGIS interface self.plugin_dir = os.path.dirname( __file__) # initialize plugin directory locale = QSettings().value('locale/userLocale')[0: 2] # initialize locale locale_path = os.path.join(self.plugin_dir, 'i18n', 'SurvexImport_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) self.actions = [] # Declare instance attributes self.menu = self.tr(u'&Import .3d file') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'SurvexImport') self.toolbar.setObjectName(u'SurvexImport') self.dlg = SurvexImportDialog() self.dlg.selectedFile.clear() self.dlg.fileSelector.clicked.connect(self.select_3d_file) self.dlg.selectedGPKG.clear() self.dlg.GPKGSelector.clicked.connect(self.select_gpkg) self.dlg.CRSFromProject.setChecked(False) self.dlg.CRSFromFile.clicked.connect(self.crs_from_file) self.dlg.CRSFromFile.setChecked(False) self.dlg.CRSFromProject.clicked.connect(self.crs_from_project)
class SurvexImport: """QGIS Plugin Implementation.""" def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join( self.plugin_dir, 'i18n', 'SurvexImport_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr(u'&Import .3d file') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'SurvexImport') self.toolbar.setObjectName(u'SurvexImport') self.dlg = SurvexImportDialog() self.dlg.selectedFile.clear() self.dlg.fileSelector.clicked.connect(self.select_3d_file) # noinspection PyMethodMayBeStatic def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate('SurvexImport', message) def add_action( self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None): """Add a toolbar icon to the toolbar. :param icon_path: Path to the icon for this action. Can be a resource path (e.g. ':/plugins/foo/bar.png') or a normal file system path. :type icon_path: str :param text: Text that should be shown in menu items for this action. :type text: str :param callback: Function to be called when the action is triggered. :type callback: function :param enabled_flag: A flag indicating if the action should be enabled by default. Defaults to True. :type enabled_flag: bool :param add_to_menu: Flag indicating whether the action should also be added to the menu. Defaults to True. :type add_to_menu: bool :param add_to_toolbar: Flag indicating whether the action should also be added to the toolbar. Defaults to True. :type add_to_toolbar: bool :param status_tip: Optional text to show in a popup when mouse pointer hovers over the action. :type status_tip: str :param parent: Parent widget for the new action. Defaults None. :type parent: QWidget :param whats_this: Optional text to show in the status bar when the mouse pointer hovers over the action. :returns: The action that was created. Note that the action is also added to self.actions list. :rtype: QAction """ # Create the dialog (after translation) and keep reference # self.dlg = SurvexImportDialog() icon = QIcon(icon_path) action = QAction(icon, text, parent) action.triggered.connect(callback) action.setEnabled(enabled_flag) if status_tip is not None: action.setStatusTip(status_tip) if whats_this is not None: action.setWhatsThis(whats_this) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToVectorMenu( self.menu, action) self.actions.append(action) return action def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" icon_path = ':/plugins/SurvexImport/icon.png' self.add_action( icon_path, text=self.tr(u'Import features from .3d files'), callback=self.run, parent=self.iface.mainWindow()) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginVectorMenu( self.tr(u'&Import .3d file'), action) self.iface.removeToolBarIcon(action) # remove the toolbar del self.toolbar def select_3d_file(self): filename = QFileDialog.getOpenFileName(self.dlg, "Select .3d file ","", '*.3d') self.dlg.selectedFile.setText(filename) # Functions to deal with .3d CRS, adding legs, and adding stations # << perfection is achieved not when nothing more can be added # but when nothing more can be taken away >> # Extract EPSG number from PROJ.4 string from 3d file using GDAL tools. # First try to match an explicit EPSG number, and check this is recognised. # If this fails, try to match the entire PROJ.4 string. The reason for # this somewhat convoluted route is to ensure if there is an EPSG number # in the passed argument, it is returned 'as is' and not transmuted # into another EPSG number with ostensibly the same CRS. def extract_epsg(self, proj4string): srs = SpatialReference() epsg_match = search('epsg:([0-9]*)', proj4string) if epsg_match: return_code = srs.ImportFromEPSG(int(epsg_match.group(1))) else: return_code = srs.ImportFromProj4(proj4string) if return_code: raise Exception("Invalid proj4 string: %s" % proj4string) code = srs.GetAttrValue('AUTHORITY', 1) epsg = int(code) QgsMessageLog.logMessage("proj4string %s --> EPSG:%i" % (proj4string, epsg), tag='Import .3d', level=QgsMessageLog.INFO) return epsg # Add a memory layer with title and geom 'Point' or 'LineString' # Note that 'PointZ' and 'LineStringZ' are not possible in QGIS 2.18 # However the z-dimension data is respected. def add_layer(self, title, subtitle, geom, epsg): uri = '%s?crs=epsg:%i' % (geom, epsg) if epsg else geom name = '%s - %s' % (title, subtitle) if title else subtitle layer = QgsVectorLayer(uri, name, 'memory') if not layer.isValid(): raise Exception("Invalid layer with %s" % uri) QgsMessageLog.logMessage("Memory layer '%s' called '%s' added" % (uri, name), tag='Import .3d', level=QgsMessageLog.INFO) return layer # Add attributes (fields) for legs leg_flags = ['DUPLICATE', 'SPLAY', 'SURFACE'] def add_leg_fields(self, layer): attrs = [QgsField(flag, QVariant.Int) for flag in self.leg_flags] attrs.insert(0, QgsField('NAME', QVariant.String)) layer.dataProvider().addAttributes(attrs) layer.updateFields() # Add a leg into the legs layer, style is raided for the attributes def add_leg(self, layer, xyz_start, xyz_end, name, style): if not layer: return xyz_pair = [QgsPointV2(QgsWKBTypes.PointZ, *xyz) for xyz in [xyz_start, xyz_end]] attrs = [1 if flag in style else 0 for flag in self.leg_flags] attrs.insert(0, name) linestring = QgsLineStringV2() linestring.setPoints(xyz_pair) feat = QgsFeature() geom = QgsGeometry(linestring) feat.setGeometry(geom) feat.setAttributes(attrs) layer.dataProvider().addFeatures([feat]) # Add attributes (fields) for stations station_flags = ['SURFACE', 'EXPORTED', 'FIXED', 'ENTRANCE'] def add_station_fields(self, layer): attrs = [QgsField(flag, QVariant.Int) for flag in self.station_flags] attrs.insert(0, QgsField('NAME', QVariant.String)) layer.dataProvider().addAttributes(attrs) layer.updateFields() # Add a station into the stations layer, using flags as attributes def add_station(self, layer, xyz, name, flags): if not layer: return attrs = [1 if flag in flags else 0 for flag in self.station_flags] attrs.insert(0, name) feat = QgsFeature() geom = QgsGeometry(QgsPointV2(QgsWKBTypes.PointZ, *xyz)) feat.setGeometry(geom) feat.setAttributes(attrs) layer.dataProvider().addFeatures([feat]) def run(self): """Run method that performs all the real work""" # show the dialog self.dlg.show() # Run the dialog event loop result = self.dlg.exec_() # See if OK was pressed if result: # This is where all the work is done. survex3dfile = self.dlg.selectedFile.text() include_legs = self.dlg.checkLegs.isChecked() include_stations = self.dlg.checkStations.isChecked() exclude_surface_legs = not self.dlg.checkLegsSurface.isChecked() exclude_splay_legs = not self.dlg.checkLegsSplay.isChecked() exclude_duplicate_legs = not self.dlg.checkLegsDuplicate.isChecked() exclude_surface_stations = not self.dlg.checkStationsSurface.isChecked() get_crs = self.dlg.checkGetCRS.isChecked() if not os.path.exists(survex3dfile): raise Exception("File '%s' doesn't exist" % survex3dfile) # Run dump3d and slurp the output (note currently stderr is unused) # First, try to figure out where the executable is. # TO BE DONE: for MAC OS X add 'Darwin' : '...' option in here... dump3d_dict = {'Linux' : '/usr/bin/dump3d', 'Windows' : 'C:\Program Files (x86)\Survex\dump3d.exe'} try: dump3d_exe = dump3d_dict[platform.system()] except KeyError: raise Exception("Unrecognised system '%s'" % platform.system()) if not os.path.exists(dump3d_exe): raise Exception("Executable '%s' doesn't exist" % dump3d_exe) p = Popen([dump3d_exe, survex3dfile], stdin=PIPE, stdout=PIPE, stderr=PIPE) dump3d_out, dump3d_err = p.communicate() if p.returncode: for line in dump3d_out.splitlines(): QgsMessageLog.logMessage(line, tag='Import .3d', level=QgsMessageLog.CRITICAL) raise Exception("dump3d failed, see log for details") # Now parse the dump3d output leg_layer, station_layer = None, None title, epsg = None, None # We run this like a gawk /pattern/ { action } script # The relevant layer is created when the first leg or station is encountered. for line in dump3d_out.splitlines(): fields = line.split() if fields[0] == 'TITLE': title = ' '.join(fields[1:]).strip('"') if get_crs and fields[0] == 'CS': proj4string = ' '.join(fields[1:]) epsg = self.extract_epsg(proj4string) if fields[0] == 'MOVE': xyz_start = [float(v) for v in fields[1:4]] if include_legs and fields[0] == 'LINE': xyz_end = [float(v) for v in fields[1:4]] name = fields[4].strip('[]') style = ' '.join(fields[5:]) if not leg_layer: leg_layer = self.add_layer(title, 'legs', 'LineString', epsg) self.add_leg_fields(leg_layer) while (True): if exclude_surface_legs and 'SURFACE' in style: break if exclude_splay_legs and 'SPLAY' in style: break if exclude_duplicate_legs and 'DUPLICATE' in style: break self.add_leg(leg_layer, xyz_start, xyz_end, name, style) break xyz_start = xyz_end if include_stations and fields[0] == 'NODE': xyz = [float(v) for v in fields[1:4]] name = fields[4].strip('[]') flags = ' '.join(fields[5:]) if not station_layer: station_layer = self.add_layer(title, 'stations', 'Point', epsg) self.add_station_fields(station_layer) while (True): if exclude_surface_stations and 'SURFACE' in flags: break self.add_station(station_layer, xyz, name, flags) break if leg_layer: leg_layer.updateExtents() QgsMapLayerRegistry.instance().addMapLayers([leg_layer]) if station_layer: station_layer.updateExtents() QgsMapLayerRegistry.instance().addMapLayers([station_layer])
class SurvexImport: """QGIS Plugin Implementation.""" # The following are some dictionaries for flags in the .3d file station_attr = { 0x01: 'SURFACE', 0x02: 'UNDERGROUND', 0x04: 'ENTRANCE', 0x08: 'EXPORTED', 0x10: 'FIXED', 0x20: 'ANON' } leg_attr = {0x01: 'SURFACE', 0x02: 'DUPLICATE', 0x04: 'SPLAY'} style_type = { 0x00: 'NORMAL', 0x01: 'DIVING', 0x02: 'CARTESIAN', 0x03: 'CYLPOLAR', 0x04: 'NOSURVEY', 0xff: 'NOSTYLE' } # lists of keys of above, sorted to restore ordering station_flags = sorted(station_attr.keys()) leg_flags = sorted(leg_attr.keys()) # field names if there is error data error_fields = ('ERROR_VERT', 'ERROR_HORIZ', 'ERROR', 'LENGTH') # map from QGIS geometry type to OGR geometry type with z dimension ogr_vec_type = { QGis.WKBPoint: ogr.wkbPoint25D, QGis.WKBLineString: ogr.wkbLineString25D, QGis.WKBPolygon: ogr.wkbPolygon25D } # map from QGIS field type to OGR field type ogr_type = { QVariant.Int: ogr.OFTInteger, QVariant.Double: ogr.OFTReal, QVariant.String: ogr.OFTString, QVariant.Date: ogr.OFTDate } # replacements needed for QGIS geometry WKT, indexed by OGR geometry type wkt_replace = { ogr.wkbPoint25D: ('PointZ', 'POINT'), ogr.wkbLineString25D: ('LineStringZ', 'LINESTRING'), ogr.wkbPolygon25D: ('PolygonZ', 'POLYGON') } leg_list = [] # accumulates legs + metadata station_list = [] # ditto stations xsect_list = [] # ditto for cross sections for walls station_xyz = {} # map station names to xyz coordinates epsg = None # used to set layer CRS in memory provider title = '' # used to set layer title in memory provider path_3d = '' # to remember the path to the survex .3d file path_gpkg = '' # ditto for path to save GeoPackage (.gpkg) def __init__(self, iface): """Constructor""" self.iface = iface # Save reference to the QGIS interface self.plugin_dir = os.path.dirname( __file__) # initialize plugin directory locale = QSettings().value('locale/userLocale')[0: 2] # initialize locale locale_path = os.path.join(self.plugin_dir, 'i18n', 'SurvexImport_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) self.actions = [] # Declare instance attributes self.menu = self.tr(u'&Import .3d file') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'SurvexImport') self.toolbar.setObjectName(u'SurvexImport') self.dlg = SurvexImportDialog() self.dlg.selectedFile.clear() self.dlg.fileSelector.clicked.connect(self.select_3d_file) self.dlg.selectedGPKG.clear() self.dlg.GPKGSelector.clicked.connect(self.select_gpkg) self.dlg.CRSFromProject.setChecked(False) self.dlg.CRSFromFile.clicked.connect(self.crs_from_file) self.dlg.CRSFromFile.setChecked(False) self.dlg.CRSFromProject.clicked.connect(self.crs_from_project) # noinspection PyMethodMayBeStatic def tr(self, message): """Get the translation for a string using Qt translation API.""" # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate('SurvexImport', message) def add_action(self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None): """Add a toolbar icon to the toolbar.""" # Create the dialog (after translation) and keep reference # self.dlg = SurvexImportDialog() icon = QIcon(icon_path) action = QAction(icon, text, parent) action.triggered.connect(callback) action.setEnabled(enabled_flag) if status_tip is not None: action.setStatusTip(status_tip) if whats_this is not None: action.setWhatsThis(whats_this) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToVectorMenu(self.menu, action) self.actions.append(action) return action def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" icon_path = ':/plugins/SurvexImport/icon.png' self.add_action(icon_path, text=self.tr(u'Import features from .3d files'), callback=self.run, parent=self.iface.mainWindow()) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginVectorMenu(self.tr(u'&Import .3d file'), action) self.iface.removeToolBarIcon(action) # remove the toolbar del self.toolbar def crs_from_file(self): """Enforce consistent CRS selector state""" if self.dlg.CRSFromFile.isChecked(): self.dlg.CRSFromProject.setChecked(False) def crs_from_project(self): """Enforce consistent CRS selector state""" if self.dlg.CRSFromProject.isChecked(): self.dlg.CRSFromFile.setChecked(False) def select_3d_file(self): """Select 3d file""" file_3d = QFileDialog.getOpenFileName(self.dlg, "Select .3d file ", self.path_3d, '*.3d') self.dlg.selectedFile.setText(file_3d) self.path_3d = QFileInfo(file_3d).path() # memorise path selection def select_gpkg(self): """Select GeoPackage (.gpkg)""" file_gpkg = QFileDialog.getSaveFileName( self.dlg, "Enter or select existing .gpkg file ", self.path_gpkg, '*.gpkg') self.dlg.selectedGPKG.setText(file_gpkg) self.path_gpkg = QFileInfo(file_gpkg).path() # memorise path selection # First try to extract an explicit EPSG number, otherwise try # assuming the string is PROJ.4. The reason for this somewhat # convoluted route is to ensure if there is an EPSG number in the # passed string, it is returned 'as is' and not transmuted into # another EPSG number with ostensibly the same CRS. def extract_epsg(self, s): """Extract EPSG number from string""" srs = osr.SpatialReference() match = search('epsg:([0-9]*)', s) if match: return_code = srs.ImportFromEPSG(int(match.group(1))) else: return_code = srs.ImportFromProj4(s) if return_code: raise Exception("Invalid proj4 string: " + s) code = srs.GetAttrValue('AUTHORITY', 1) srs = None self.epsg = int(code) msg = "%s --> EPSG:%i" % (s, self.epsg) QgsMessageLog.logMessage(msg, tag='Import .3d', level=QgsMessageLog.INFO) # Note that 'PointZ', 'LineStringZ', 'PolygonZ' are not possible # in QGIS 2.18 However the z-dimension data is respected. def add_layer(self, subtitle, geom): """Add a memory layer with title and geom, and CRS if epsg defined""" uri = '%s?crs=epsg:%i' % (geom, self.epsg) if self.epsg else geom name = '%s - %s' % (self.title, subtitle) if self.title else subtitle layer = QgsVectorLayer(uri, name, 'memory') if not layer.isValid(): raise Exception("Invalid layer with %s" % uri) msg = "Memory layer '%s' called '%s' added" % (uri, name) QgsMessageLog.logMessage(msg, tag='Import .3d', level=QgsMessageLog.INFO) return layer def read_xyz(self, fp): """Read xyz as integers, according to .3d spec""" return unpack('<iii', fp.read(12)) def read_len(self, fp): """Read a number as a length according to .3d spec""" byte = ord(fp.read(1)) if byte != 0xff: return byte else: return unpack('<I', fp.read(4))[0] def read_label(self, fp, current_label): """Read a string as a label, or part thereof, according to .3d spec""" byte = ord(fp.read(1)) if byte != 0x00: ndel = byte >> 4 nadd = byte & 0x0f else: ndel = self.read_len(fp) nadd = self.read_len(fp) oldlen = len(current_label) return current_label[:oldlen - ndel] + fp.read(nadd).decode('ascii') def run(self): """Run method that performs all the real work""" self.dlg.show() # show the dialog result = self.dlg.exec_() # Run the dialog event loop if result: # The user pressed OK, and this is what happened next! survex3dfile = self.dlg.selectedFile.text() gpkg_file = self.dlg.selectedGPKG.text() include_legs = self.dlg.Legs.isChecked() include_stations = self.dlg.Stations.isChecked() include_polygons = self.dlg.Polygons.isChecked() include_walls = self.dlg.Walls.isChecked() include_xsections = self.dlg.XSections.isChecked() include_traverses = self.dlg.Traverses.isChecked() exclude_surface_legs = not self.dlg.LegsSurface.isChecked() exclude_splay_legs = not self.dlg.LegsSplay.isChecked() exclude_duplicate_legs = not self.dlg.LegsDuplicate.isChecked() exclude_surface_stations = not self.dlg.StationsSurface.isChecked() use_clino_wgt = self.dlg.UseClinoWeights.isChecked() include_up_down = self.dlg.IncludeUpDown.isChecked() discard_features = not self.dlg.KeepFeatures.isChecked() get_crs_from_file = self.dlg.CRSFromFile.isChecked() get_crs_from_project = self.dlg.CRSFromProject.isChecked() if not os.path.exists(survex3dfile): raise Exception("File '%s' doesn't exist" % survex3dfile) if discard_features: self.leg_list = [] self.station_list = [] self.station_xyz = {} self.xsect_list = [] # Read .3d file as binary, parse, and save data structures with open(survex3dfile, 'rb') as fp: line = fp.readline().rstrip() # File ID check if not line.startswith(b'Survex 3D Image File'): raise IOError('Not a survex .3d file: ' + survex3dfile) line = fp.readline().rstrip() # File format version if not line.startswith(b'v'): raise IOError('Unrecognised survex .3d version in ' + survex3dfile) version = int(line[1:]) if version < 8: raise IOError('Survex .3d version >= 8 required in ' + survex3dfile) line = fp.readline().rstrip( ) # Metadata (title and coordinate system) fields = line.split(b'\x00') previous_title = '' if discard_features else self.title if previous_title: self.title = previous_title + ' + ' + fields[0] else: self.title = fields[0] # Try to work out EPSG number from second field if available. # The project_crs should end up as a lowercase string like 'epsg:27700' if get_crs_from_project: project_crs = self.iface.mapCanvas().mapRenderer( ).destinationCrs() self.extract_epsg(project_crs.authid().lower()) elif get_crs_from_file and len(fields) > 1: self.extract_epsg(fields[1]) else: self.epsg = None line = fp.readline().rstrip( ) # Timestamp, unused in present application if not line.startswith(b'@'): raise IOError('Unrecognised timestamp in ' + survex3dfile) # timestamp = int(line[1:]) flag = ord(fp.read(1)) # file-wide flag if flag & 0x80: # abort if extended elevation raise IOError("Can't deal with extended elevation in " + survex3dfile) # All front-end data read in, now read byte-wise # according to .3d spec. Note that all elements must # be processed, in order, otherwise we get out of sync. # We first define some baseline dates date0 = QDate(1900, 1, 1) date1 = QDate(1900, 1, 1) date2 = QDate(1900, 1, 1) label, style = '', 0xff # initialise label and style legs = [] # will be used to capture leg data between MOVEs xsect = [] # will be used to capture XSECT data nlehv = None # .. remains None if there isn't any error data... while True: # start of byte-gobbling while loop char = fp.read(1) if not char: # End of file (reached prematurely?) raise IOError('Premature end of file in ' + survex3dfile) byte = ord(char) if byte <= 0x05: # STYLE if byte == 0x00 and style == 0x00: # this signals end of data if legs: # there may be a pending list of legs to save self.leg_list.append((legs, nlehv)) break # escape from byte-gobbling while loop else: style = byte elif byte <= 0x0e: # Reserved continue elif byte == 0x0f: # MOVE xyz = self.read_xyz(fp) if legs: self.leg_list.append((legs, nlehv)) legs = [] elif byte == 0x10: # DATE (none) date1 = date2 = date0 elif byte == 0x11: # DATE (single date) days = unpack('<H', fp.read(2))[0] date1 = date2 = date0.addDays(days) elif byte == 0x12: # DATE (date range, short format) days, extra = unpack('<HB', fp.read(3)) date1 = date0.addDays(days) date2 = date0.addDays(days + extra + 1) elif byte == 0x13: # DATE (date range, long format) days1, days2 = unpack('<HH', fp.read(4)) date1 = date0.addDays(days1) date2 = date0.addDays(days2) elif byte <= 0x1e: # Reserved continue elif byte == 0x1f: # Error info nlehv = unpack('<iiiii', fp.read(20)) elif byte <= 0x2f: # Reserved continue elif byte <= 0x33: # XSECT label = self.read_label(fp, label) if byte & 0x02: lrud = unpack('<iiii', fp.read(16)) else: lrud = unpack('<hhhh', fp.read(8)) xsect.append((label, lrud)) if byte & 0x01: # XSECT_END self.xsect_list.append(xsect) xsect = [] elif byte <= 0x3f: # Reserved continue elif byte <= 0x7f: # LINE flag = byte & 0x3f if not (flag & 0x20): label = self.read_label(fp, label) xyz_prev = xyz xyz = self.read_xyz(fp) while (True): # code pattern to implement logic if exclude_surface_legs and flag & 0x01: break if exclude_duplicate_legs and flag & 0x02: break if exclude_splay_legs and flag & 0x04: break legs.append(((xyz_prev, xyz), label, style, date1, date2, flag)) break elif byte <= 0xff: # LABEL (or NODE) flag = byte & 0x7f label = self.read_label(fp, label) xyz = self.read_xyz(fp) while (True): # code pattern to implement logic if exclude_surface_stations and flag & 0x01 and not flag & 0x02: break self.station_list.append((xyz, label, flag)) break self.station_xyz[label] = xyz # End of byte-gobbling while loop # file closes automatically, with open(survex3dfile, 'rb') as fp: # Now create the layers in QGIS. Attributes are inserted # like pushing onto a stack, so in reverse order. Layers # are created only if required and data is available. # If nlehv is still None, then no error data has been provided. layers = [] # used to keep a list of the created layers if include_stations and self.station_list: # station layer station_layer = self.add_layer('stations', 'Point') attrs = [ QgsField(self.station_attr[k], QVariant.Int) for k in self.station_flags ] attrs.insert(0, QgsField('ELEVATION', QVariant.Double)) attrs.insert(0, QgsField('NAME', QVariant.String)) station_layer.dataProvider().addAttributes(attrs) station_layer.updateFields() features = [] for (xyz, label, flag) in self.station_list: xyz = [0.01 * v for v in xyz] attrs = [1 if flag & k else 0 for k in self.station_flags] attrs.insert(0, round(xyz[2], 2)) # elevation attrs.insert(0, label) feat = QgsFeature() geom = QgsGeometry(QgsPointV2(QgsWKBTypes.PointZ, *xyz)) feat.setGeometry(geom) feat.setAttributes(attrs) features.append(feat) station_layer.dataProvider().addFeatures(features) layers.append(station_layer) if include_legs and self.leg_list: # leg layer leg_layer = self.add_layer('legs', 'LineString') attrs = [ QgsField(self.leg_attr[k], QVariant.Int) for k in self.leg_flags ] if nlehv: [ attrs.insert(0, QgsField(s, QVariant.Double)) for s in self.error_fields ] attrs.insert(0, QgsField('NLEGS', QVariant.Int)) attrs.insert(0, QgsField('DATE2', QVariant.Date)) attrs.insert(0, QgsField('DATE1', QVariant.Date)) attrs.insert(0, QgsField('STYLE', QVariant.String)) attrs.insert(0, QgsField('ELEVATION', QVariant.Double)) attrs.insert(0, QgsField('NAME', QVariant.String)) leg_layer.dataProvider().addAttributes(attrs) leg_layer.updateFields() features = [] for legs, nlehv in self.leg_list: for (xyz_pair, label, style, from_date, to_date, flag) in legs: elev = 0.5 * sum([0.01 * xyz[2] for xyz in xyz_pair]) points = [] for xyz in xyz_pair: xyz = [0.01 * v for v in xyz] points.append(QgsPointV2(QgsWKBTypes.PointZ, *xyz)) attrs = [1 if flag & k else 0 for k in self.leg_flags] if nlehv: [ attrs.insert(0, 0.01 * v) for v in reversed(nlehv[1:5]) ] attrs.insert(0, nlehv[0]) attrs.insert(0, to_date) attrs.insert(0, from_date) attrs.insert(0, self.style_type[style]) attrs.insert(0, round(elev, 2)) attrs.insert(0, label) linestring = QgsLineStringV2() linestring.setPoints(points) feat = QgsFeature() geom = QgsGeometry(linestring) feat.setGeometry(geom) feat.setAttributes(attrs) features.append(feat) leg_layer.dataProvider().addFeatures(features) layers.append(leg_layer) # Now do wall features if asked if (include_traverses or include_xsections or include_walls or include_polygons) and self.xsect_list: trav_features = [] wall_features = [] xsect_features = [] quad_features = [] for xsect in self.xsect_list: if len(xsect) < 2: # if there's only one station .. continue # .. give up as we don't know which way to face centerline = [ ] # will contain the station position and LRUD data for label, lrud in xsect: xyz = self.station_xyz[ label] # look up coordinates from label lrud_or_zero = tuple([max(0, v) for v in lrud ]) # deal with missing data centerline.append( xyz + lrud_or_zero) # and collect as 7-uple direction = [ ] # will contain the corresponding direction vectors # The calculations below use integers for xyz and lrud, and # conversion to metres is left to the end. Then dh2 is an # integer and the test for a plumb is safely dh2 = 0. # The directions are unit vectors optionally weighted by # cos(inclination) = dh/dl where dh^2 = dx^2 + dy^2 + dz^2 # and dl^2 = dh^2 + dz^2. The normalisation is correspondingly # either 1/dh, or 1/dh * dh/dl = 1/dl. for i, xyzlrud in enumerate(centerline): x, y, z = xyzlrud[0:3] if i > 0: dx, dy, dz = x - xp, y - yp, z - zp dh2 = dx * dx + dy * dy # integer horizontal displacement (mm^2) norm = sqrt(dh2 + dz * dz) if use_clino_wgt else sqrt(dh2) dx, dy = (dx / norm, dy / norm) if dh2 > 0 and norm > 0 else (0, 0) direction.append((dx, dy)) xp, yp, zp = x, y, z left_wall = [] right_wall = [] up_down = [] # We build the walls by walking through the list # of stations and directions, with simple defaults # for the start and end stations for i, (x, y, z, l, r, u, d) in enumerate(centerline): d1x, d1y = direction[i - 1] if i > 0 else (0, 0) d2x, d2y = direction[i] if i + 1 < len( centerline) else (0, 0) dx, dy = d1x + d2x, d1y + d2y # mean (sum of) direction vectors norm = sqrt(dx * dx + dy * dy) # normalise to unit vector ex, ey = (dx / norm, dy / norm) if norm > 0 else (0, 0) # Convert to metres when saving the points left_wall.append((0.01 * (x - l * ey), 0.01 * (y + l * ex), 0.01 * z)) right_wall.append((0.01 * (x + r * ey), 0.01 * (y - r * ex), 0.01 * z)) up_down.append((0.01 * u, 0.01 * d)) # Mean elevation of centerline, used for elevation attribute elev = 0.01 * sum([xyzlrud[2] for xyzlrud in centerline ]) / len(centerline) attrs = [round(elev, 2)] # Now create the feature sets - first the centerline traverse points = [] for xyzlrud in centerline: xyz = [0.01 * v for v in xyzlrud[0:3] ] # These were mm, convert to metres points.append(QgsPointV2(QgsWKBTypes.PointZ, *xyz)) linestring = QgsLineStringV2() linestring.setPoints(points) feat = QgsFeature() geom = QgsGeometry(linestring) feat.setGeometry(geom) feat.setAttributes(attrs) trav_features.append(feat) # The walls as line strings for wall in (left_wall, right_wall): points = [ QgsPointV2(QgsWKBTypes.PointZ, *xyz) for xyz in wall ] linestring = QgsLineStringV2() linestring.setPoints(points) feat = QgsFeature() geom = QgsGeometry(linestring) feat.setGeometry(geom) feat.setAttributes(attrs) wall_features.append(feat) # Slightly more elaborate, pair up points on left # and right walls, and build a cross section as a # 2-point line string, and a quadrilateral polygon # with a closed 5-point line string for the # exterior ring. Note that QGIS polygons are # supposed to have their points ordered clockwise. for i, xyz_pair in enumerate(zip(left_wall, right_wall)): elev = 0.01 * centerline[i][ 2] # elevation of station in centerline attrs = [round(elev, 2)] points = [ QgsPointV2(QgsWKBTypes.PointZ, *xyz) for xyz in xyz_pair ] linestring = QgsLineStringV2() linestring.setPoints(points) feat = QgsFeature() geom = QgsGeometry(linestring) feat.setGeometry(geom) feat.setAttributes(attrs) xsect_features.append(feat) if i > 0: elev = 0.5 * (prev_xyz_pair[0][2] + xyz_pair[0][2] ) # average elevation attrs = [round(elev, 2)] if include_up_down: # average up / down attrs += [ 0.5 * (v1 + v2) for (v1, v2) in zip(up_down[i - 1], up_down[i]) ] points = [ ] # will contain the exterior 5-point ring, as follows... for xyz in tuple( reversed(prev_xyz_pair)) + xyz_pair + ( prev_xyz_pair[1], ): points.append( QgsPointV2(QgsWKBTypes.PointZ, *xyz)) linestring = QgsLineStringV2() linestring.setPoints(points) polygon = QgsPolygonV2() polygon.setExteriorRing(linestring) feat = QgsFeature() geom = QgsGeometry(polygon) feat.setGeometry(geom) feat.setAttributes(attrs) quad_features.append(feat) prev_xyz_pair = xyz_pair # End of processing xsect_list - now add features to requested layers attrs = [QgsField('ELEVATION', QVariant.Double)] # common to all if include_traverses and trav_features: # traverse layer travs_layer = self.add_layer('traverses', 'LineString') travs_layer.dataProvider().addAttributes(attrs) travs_layer.updateFields() travs_layer.dataProvider().addFeatures(trav_features) layers.append(travs_layer) if include_xsections and xsect_features: # xsection layer xsects_layer = self.add_layer('xsections', 'LineString') xsects_layer.dataProvider().addAttributes(attrs) xsects_layer.updateFields() xsects_layer.dataProvider().addFeatures(xsect_features) layers.append(xsects_layer) if include_walls and wall_features: # wall layer walls_layer = self.add_layer('walls', 'LineString') walls_layer.dataProvider().addAttributes(attrs) walls_layer.updateFields() walls_layer.dataProvider().addFeatures(wall_features) layers.append(walls_layer) if include_up_down: # add fields if requested for polygons attrs += [ QgsField(s, QVariant.Double) for s in ('MEAN_UP', 'MEAN_DOWN') ] if include_polygons and quad_features: # polygon layer quads_layer = self.add_layer('polygons', 'Polygon') quads_layer.dataProvider().addAttributes(attrs) quads_layer.updateFields() quads_layer.dataProvider().addFeatures(quad_features) layers.append(quads_layer) # All layers have been created, now update extents and add to QGIS registry if layers: [layer.updateExtents() for layer in layers] QgsMapLayerRegistry.instance().addMapLayers(layers) # Save layers to a GeoPackage if selected. # QgsVectorFileWriter would be ideal but it can only write # single layers (afaik!), so the GeoPackage layers, # fields, and attributes are created using OGR calls, # translating the corresponding QGIS objects on the fly. # It's possible this could be done faster by iterating # numerically over the attributes but I'm not sure the OGR # features would be visited in the right order, so here # use the field names as indices. Meanwhile, the user is # appraised of progress by a progress bar. if gpkg_file: nfeatures = 0 # how many features in total for layer in layers: nfeatures += layer.featureCount() if nfeatures > 100: # create a progress bar progress_bar = QProgressBar() progress_bar.setMaximum(nfeatures) progress_bar.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) msg = 'Saving ' + QFileInfo(gpkg_file).fileName() progressMessageBar = self.iface.messageBar().createMessage( msg) progressMessageBar.layout().addWidget(progress_bar) self.iface.messageBar().pushWidget( progressMessageBar, self.iface.messageBar().INFO) ntrack = int(10**(floor(log10(nfeatures)) - 1)) # update frequency else: progress_bar = None gpkg_driver = ogr.GetDriverByName('GPKG') if os.path.exists(gpkg_file): gpkg_driver.DeleteDataSource(gpkg_file) ogr_dataset = gpkg_driver.CreateDataSource(gpkg_file) if self.epsg: # figure out the spatial reference system in OGR terms ogr_srs = osr.SpatialReference() ogr_srs.ImportFromEPSG(self.epsg) else: ogr_srs = None ncount = 0 # to keep track of number of features processed for layer in layers: # go through all the layers qgis_name = layer.name() match = search(' - ([a-z]*)', qgis_name) ogr_name = str(match.group( 1)) if match else qgis_name # ie, legs, stations, etc ogr_type = self.ogr_vec_type[layer.wkbType()] ogr_layer = ogr_dataset.CreateLayer(ogr_name, srs=ogr_srs, geom_type=ogr_type) qgis_fields = layer.pendingFields() names = [str(field.name()) for field in qgis_fields] types = [ self.ogr_type[field.type()] for field in qgis_fields ] ogr_type_of_ = dict(zip( names, types)) # map field names to OGR field types [ ogr_layer.CreateField( ogr.FieldDefn(name, ogr_type_of_[name])) for name in names ] ogr_schema = ogr_layer.GetLayerDefn( ) # for creating features in the OGR layer for qgis_feat in layer.getFeatures( ): # go through all the features ncount += 1 # update feature count if progress_bar and ncount % ntrack: # update progress bar progress_bar.setValue(ncount) ogr_feat = ogr.Feature(ogr_schema) ogr_wkt = qgis_feat.geometry().exportToWkt().replace( *self.wkt_replace[ogr_type]) ogr_feat.SetGeometry( ogr.CreateGeometryFromWkt(ogr_wkt)) qgis_attrs = qgis_feat.attributes() for name, qgis_attr in zip(names, qgis_attrs): if ogr_type_of_[ name] == ogr.OFTString: # fix for strings ogr_feat.SetField(name, str(qgis_attr)) elif ogr_type_of_[ name] == ogr.OFTDate: # translate dates ogr_feat.SetField( name, str(qgis_attr.toString(Qt.ISODate))) else: # everything else just passes through ogr_feat.SetField(name, qgis_attr) ogr_layer.CreateFeature(ogr_feat) ogr_dataset = None # all done, flush to disk if progress_bar: # clean up and free resources self.iface.messageBar().clearWidgets() progress_bar = None msg = QFileInfo(gpkg_file).fileName() + ' to ' + QFileInfo( gpkg_file).path() QgsMessageLog.logMessage('Saved ' + msg, tag='Import .3d', level=QgsMessageLog.INFO) self.iface.messageBar().pushMessage('Saved', msg, level=QgsMessageBar.INFO, duration=5)
class SurvexImport: """QGIS Plugin Implementation.""" def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join(self.plugin_dir, 'i18n', 'SurvexImport_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr(u'&Import .3d file') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'SurvexImport') self.toolbar.setObjectName(u'SurvexImport') self.dlg = SurvexImportDialog() self.dlg.selectedFile.clear() self.dlg.fileSelector.clicked.connect(self.select_3d_file) # noinspection PyMethodMayBeStatic def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate('SurvexImport', message) def add_action(self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None): """Add a toolbar icon to the toolbar. :param icon_path: Path to the icon for this action. Can be a resource path (e.g. ':/plugins/foo/bar.png') or a normal file system path. :type icon_path: str :param text: Text that should be shown in menu items for this action. :type text: str :param callback: Function to be called when the action is triggered. :type callback: function :param enabled_flag: A flag indicating if the action should be enabled by default. Defaults to True. :type enabled_flag: bool :param add_to_menu: Flag indicating whether the action should also be added to the menu. Defaults to True. :type add_to_menu: bool :param add_to_toolbar: Flag indicating whether the action should also be added to the toolbar. Defaults to True. :type add_to_toolbar: bool :param status_tip: Optional text to show in a popup when mouse pointer hovers over the action. :type status_tip: str :param parent: Parent widget for the new action. Defaults None. :type parent: QWidget :param whats_this: Optional text to show in the status bar when the mouse pointer hovers over the action. :returns: The action that was created. Note that the action is also added to self.actions list. :rtype: QAction """ # Create the dialog (after translation) and keep reference # self.dlg = SurvexImportDialog() icon = QIcon(icon_path) action = QAction(icon, text, parent) action.triggered.connect(callback) action.setEnabled(enabled_flag) if status_tip is not None: action.setStatusTip(status_tip) if whats_this is not None: action.setWhatsThis(whats_this) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToVectorMenu(self.menu, action) self.actions.append(action) return action def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" icon_path = ':/plugins/SurvexImport/icon.png' self.add_action(icon_path, text=self.tr(u'Import features from .3d files'), callback=self.run, parent=self.iface.mainWindow()) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginVectorMenu(self.tr(u'&Import .3d file'), action) self.iface.removeToolBarIcon(action) # remove the toolbar del self.toolbar def select_3d_file(self): filename = QFileDialog.getOpenFileName(self.dlg, "Select .3d file ", "", '*.3d') self.dlg.selectedFile.setText(filename) # Functions to deal with .3d CRS, adding legs, and adding stations # << perfection is achieved not when nothing more can be added # but when nothing more can be taken away >> # First try to match an explicit EPSG number, and check this is recognised. # If this fails, try to match the entire PROJ.4 string. The reason for # this somewhat convoluted route is to ensure if there is an EPSG number # in the passed argument, it is returned 'as is' and not transmuted # into another EPSG number with ostensibly the same CRS. def extract_epsg(self, proj4string): """Extract EPSG number from PROJ.4 string using GDAL tools""" srs = SpatialReference() epsg_match = search('epsg:([0-9]*)', proj4string) if epsg_match: return_code = srs.ImportFromEPSG(int(epsg_match.group(1))) else: return_code = srs.ImportFromProj4(proj4string) if return_code: raise Exception("Invalid proj4 string: " + proj4string) code = srs.GetAttrValue('AUTHORITY', 1) epsg = int(code) QgsMessageLog.logMessage("proj4string %s --> EPSG:%i" % (proj4string, epsg), tag='Import .3d', level=QgsMessageLog.INFO) return epsg # Note that 'PointZ' and 'LineStringZ' are not possible in QGIS 2.18 # However the z-dimension data is respected. def add_layer(self, title, subtitle, geom, epsg): """Add a memory layer with title and geom 'Point' or 'LineString'""" uri = '%s?crs=epsg:%i' % (geom, epsg) if epsg else geom name = '%s - %s' % (title, subtitle) if title else subtitle layer = QgsVectorLayer(uri, name, 'memory') if not layer.isValid(): raise Exception("Invalid layer with %s" % uri) QgsMessageLog.logMessage("Memory layer '%s' called '%s' added" % (uri, name), tag='Import .3d', level=QgsMessageLog.INFO) return layer leg_attr = {0x01: 'SURFACE', 0x02: 'DUPLICATE', 0x04: 'SPLAY'} leg_flags = sorted(leg_attr.keys()) style_type = { 0x00: 'NORMAL', 0x01: 'DIVING', 0x02: 'CARTESIAN', 0x03: 'CYLPOLAR', 0x04: 'NOSURVEY', 0xff: 'NOSTYLE' } # Attributes are inserted like pushing onto a stack, so in reverse order def add_leg_fields(self, layer): """Add attributes (fields) for legs""" attrs = [ QgsField(self.leg_attr[k], QVariant.Int) for k in self.leg_flags ] attrs.insert(0, QgsField('DATE2', QVariant.Date)) attrs.insert(0, QgsField('DATE1', QVariant.Date)) attrs.insert(0, QgsField('STYLE', QVariant.String)) attrs.insert(0, QgsField('NAME', QVariant.String)) layer.dataProvider().addAttributes(attrs) layer.updateFields() def add_leg(self, layer, xyz_start, xyz_end, name, style, date_from, date_to, flag): """Add a leg into the legs layer, attributes from flag""" if not layer: return xyz_pair = [ QgsPointV2(QgsWKBTypes.PointZ, *xyz) for xyz in [xyz_start, xyz_end] ] attrs = [1 if flag & k else 0 for k in self.leg_flags] attrs.insert(0, date_to) attrs.insert(0, date_from) attrs.insert(0, self.style_type[style]) attrs.insert(0, name) linestring = QgsLineStringV2() linestring.setPoints(xyz_pair) feat = QgsFeature() geom = QgsGeometry(linestring) feat.setGeometry(geom) feat.setAttributes(attrs) layer.dataProvider().addFeatures([feat]) station_attr = { 0x01: 'SURFACE', 0x02: 'UNDERGROUND', 0x04: 'ENTRANCE', 0x08: 'EXPORTED', 0x10: 'FIXED', 0x20: 'ANON' } station_flags = sorted(station_attr.keys()) def add_station_fields(self, layer): """Add attributes (fields) for stations""" attrs = [ QgsField(self.station_attr[k], QVariant.Int) for k in self.station_flags ] attrs.insert(0, QgsField('NAME', QVariant.String)) layer.dataProvider().addAttributes(attrs) layer.updateFields() def add_station(self, layer, xyz, name, flag): """Add a station into the stations layer, atributes from flags""" if not layer: return attrs = [1 if flag & k else 0 for k in self.station_flags] attrs.insert(0, name) feat = QgsFeature() geom = QgsGeometry(QgsPointV2(QgsWKBTypes.PointZ, *xyz)) feat.setGeometry(geom) feat.setAttributes(attrs) layer.dataProvider().addFeatures([feat]) def read_xyz(self, fp): """Read xyz and convert to metres, according to .3d spec""" return [0.01 * v for v in unpack('<iii', fp.read(12))] def read_len(self, fp): """Read a number as a length according to .3d spec""" byte = ord(fp.read(1)) if byte != 0xff: return byte else: return unpack('<I', fp.read(4))[0] def read_label(self, fp, current_label): """Read a string as a label, or part thereof, according to .3d spec""" byte = ord(fp.read(1)) if byte != 0x00: ndel = byte >> 4 nadd = byte & 0x0f else: ndel = self.read_len(fp) nadd = self.read_len(fp) oldlen = len(current_label) return current_label[:oldlen - ndel] + fp.read(nadd).decode('ascii') def run(self): """Run method that performs all the real work""" # show the dialog self.dlg.show() # Run the dialog event loop result = self.dlg.exec_() # See if OK was pressed if result: # This is where all the work is done. survex3dfile = self.dlg.selectedFile.text() include_legs = self.dlg.checkLegs.isChecked() include_stations = self.dlg.checkStations.isChecked() exclude_surface_legs = not self.dlg.checkLegsSurface.isChecked() exclude_splay_legs = not self.dlg.checkLegsSplay.isChecked() exclude_duplicate_legs = not self.dlg.checkLegsDuplicate.isChecked( ) exclude_surface_stations = not self.dlg.checkStationsSurface.isChecked( ) get_crs = self.dlg.checkGetCRS.isChecked() if not os.path.exists(survex3dfile): raise Exception("File '%s' doesn't exist" % survex3dfile) # Read .3d file as binary with open(survex3dfile, 'rb') as fp: line = fp.readline().rstrip() # File ID check if not line.startswith(b'Survex 3D Image File'): raise IOError('Not a Survex 3D File: ' + survex3dfile) line = fp.readline().rstrip() # File format version if not line.startswith(b'v'): raise IOError('Unrecognised .3d version: ' + survex3dfile) version = int(line[1:]) if version < 8: raise IOError('Version >= 8 required: ' + survex3dfile) line = fp.readline().rstrip( ) # Metadata (title and coordinate system) fields = line.split(b'\x00') title = fields[0] epsg = self.extract_epsg( fields[1]) if get_crs and len(fields) > 1 else None line = fp.readline().rstrip() # Timestamp if not line.startswith(b'@'): raise IOError('Unrecognised timestamp: ' + survex3dfile) timestamp = int(line[1:]) # Saved, but not used at present # System-wide flags - abort if extended elevation flag = ord(fp.read(1)) if flag & 0x80: raise IOError('Extended elevation: ' + survex3dfile) # All front-end data read in, now read byte-wise # according to .3d spec. Note that all elements must # be processed, in order, otherwise we get out of sync if include_legs: leg_layer = self.add_layer(title, 'legs', 'LineString', epsg) self.add_leg_fields(leg_layer) else: leg_layer = None if include_stations: station_layer = self.add_layer(title, 'stations', 'Point', epsg) self.add_station_fields(station_layer) else: station_layer = None date0 = date1 = date2 = QDate(1900, 1, 1) label, style = '', 0xff while True: char = fp.read(1) if not char: # End of file reached (prematurely?) raise IOError('Premature end of file: ' + survex3dfile) byte = ord(char) if byte <= 0x05: # STYLE if byte == 0x00 and style == 0x00: # this signals end of data break # escape from byte-gobbling while loop else: style = byte elif byte <= 0x0e: # Reserved continue elif byte == 0x0f: # MOVE xyz = self.read_xyz(fp) elif byte == 0x10: # DATE (none) date1 = date2 = date0 elif byte == 0x11: # DATE (single date) days = unpack('<H', fp.read(2))[0] date1 = date2 = date0.addDays(days) elif byte == 0x12: # DATE (date range, short format) days, extra = unpack('<HB', fp.read(3)) date1 = date0.addDays(days) date2 = date0.addDays(days + extra + 1) elif byte == 0x13: # DATE (date range, long format) days1, days2 = unpack('<HH', fp.read(4)) date1 = date0.addDays(days1) date2 = date0.addDays(days2) elif byte <= 0x1e: # Reserved continue elif byte == 0x1f: # Error info -- not currently captured nlehv = unpack('<iiiii', fp.read(20)) elif byte <= 0x2f: # Reserved continue elif byte <= 0x33: # XSECT -- not currently captured label = self.read_label(fp, label) if byte & 0x02: lrud = unpack('<iiii', fp.read(16)) else: lrud = unpack('<hhhh', fp.read(8)) elif byte <= 0x3f: # Reserved continue elif byte <= 0x7f: # LINE flag = byte & 0x3f if not (flag & 0x20): label = self.read_label(fp, label) xyz_prev = xyz xyz = self.read_xyz(fp) if leg_layer: while (True): if exclude_surface_legs and flag & 0x01: break if exclude_duplicate_legs and flag & 0x02: break if exclude_splay_legs and flag & 0x04: break self.add_leg(leg_layer, xyz_prev, xyz, label, style, date1, date2, flag) break elif byte <= 0xff: # LABEL (or NODE) flag = byte & 0x7f label = self.read_label(fp, label) xyz = self.read_xyz(fp) if station_layer: while (True): if exclude_surface_stations and flag & 0x01 and not flag & 0x02: break self.add_station(station_layer, xyz, label, flag) break # End of byte-gobbling while loop # file closes automatically, with open(survex3dfile, 'rb') as fp: if leg_layer: leg_layer.updateExtents() QgsMapLayerRegistry.instance().addMapLayers([leg_layer]) if station_layer: station_layer.updateExtents() QgsMapLayerRegistry.instance().addMapLayers([station_layer])
class SurvexImport: """QGIS Plugin Implementation.""" def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join(self.plugin_dir, 'i18n', 'SurvexImport_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr(u'&Import .3d file') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'SurvexImport') self.toolbar.setObjectName(u'SurvexImport') self.dlg = SurvexImportDialog() self.dlg.selectedFile.clear() self.dlg.fileSelector.clicked.connect(self.select_3d_file) # noinspection PyMethodMayBeStatic def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate('SurvexImport', message) def add_action(self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None): """Add a toolbar icon to the toolbar. :param icon_path: Path to the icon for this action. Can be a resource path (e.g. ':/plugins/foo/bar.png') or a normal file system path. :type icon_path: str :param text: Text that should be shown in menu items for this action. :type text: str :param callback: Function to be called when the action is triggered. :type callback: function :param enabled_flag: A flag indicating if the action should be enabled by default. Defaults to True. :type enabled_flag: bool :param add_to_menu: Flag indicating whether the action should also be added to the menu. Defaults to True. :type add_to_menu: bool :param add_to_toolbar: Flag indicating whether the action should also be added to the toolbar. Defaults to True. :type add_to_toolbar: bool :param status_tip: Optional text to show in a popup when mouse pointer hovers over the action. :type status_tip: str :param parent: Parent widget for the new action. Defaults None. :type parent: QWidget :param whats_this: Optional text to show in the status bar when the mouse pointer hovers over the action. :returns: The action that was created. Note that the action is also added to self.actions list. :rtype: QAction """ # Create the dialog (after translation) and keep reference # self.dlg = SurvexImportDialog() icon = QIcon(icon_path) action = QAction(icon, text, parent) action.triggered.connect(callback) action.setEnabled(enabled_flag) if status_tip is not None: action.setStatusTip(status_tip) if whats_this is not None: action.setWhatsThis(whats_this) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToVectorMenu(self.menu, action) self.actions.append(action) return action def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" icon_path = ':/plugins/SurvexImport/icon.png' self.add_action(icon_path, text=self.tr(u'Import features from .3d files'), callback=self.run, parent=self.iface.mainWindow()) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginVectorMenu(self.tr(u'&Import .3d file'), action) self.iface.removeToolBarIcon(action) # remove the toolbar del self.toolbar def select_3d_file(self): filename = QFileDialog.getOpenFileName(self.dlg, "Select .3d file ", "", '*.3d') self.dlg.selectedFile.setText(filename) # Check newbie exception handling is correctly done in the below! # Extract EPSG number from proj4 string from 3d file using GDAL tools. def extract_epsg(self, proj4string): srs = SpatialReference() rc = srs.ImportFromProj4(proj4string) if rc: raise Exception("Invalid proj4 string: %s" % proj4string) code = srs.GetAttrValue('AUTHORITY', 1) epsg = int(code) QgsMessageLog.logMessage("proj4string %s --> EPSG:%i" % (proj4string, epsg), tag='Import .3d', level=QgsMessageLog.INFO) return epsg # Add a memory layer with title and geometry Point or LineString # Note that PointZ and LineStringZ are not possible in QGIS 2.18 # However the z-info is respected. def add_layer(self, title, subtitle, geom, epsg): uri = '%s?crs=epsg:%i' % (geom, epsg) if epsg else geom name = '%s %s' % (title, subtitle) if title else subtitle layer = QgsVectorLayer(uri, name, 'memory') if not layer.isValid(): raise Exception("Invalid layer with %s" % uri) QgsMessageLog.logMessage("Memory layer '%s' called '%s' added" % (uri, name), tag='Import .3d', level=QgsMessageLog.INFO) return layer # Add attributes (fields) for legs leg_flags = ['DUPLICATE', 'SPLAY', 'SURFACE'] def add_leg_fields(self, layer): pr = layer.dataProvider() attrs = [QgsField(flag, QVariant.Int) for flag in self.leg_flags] pr.addAttributes(attrs) layer.updateFields() # Add a leg into the legs layer, style is raided for the attributes def add_leg(self, layer, xyz_start, xyz_end, style): if layer is None: return pr = layer.dataProvider() xyz_pair = [ QgsPointV2(QgsWKBTypes.PointZ, *xyz) for xyz in [xyz_start, xyz_end] ] attrs = [1 if flag in style else 0 for flag in self.leg_flags] linestring = QgsLineStringV2() linestring.setPoints(xyz_pair) feat = QgsFeature() geom = QgsGeometry(linestring) feat.setGeometry(geom) feat.setAttributes(attrs) pr.addFeatures([feat]) # Add attributes (fields) for stations station_flags = ['SURFACE', 'EXPORTED', 'FIXED', 'ENTRANCE'] def add_station_fields(self, layer): pr = layer.dataProvider() attrs = [QgsField(flag, QVariant.Int) for flag in self.station_flags] attrs.insert(0, QgsField('NAME', QVariant.String)) pr.addAttributes(attrs) layer.updateFields() # Add a station into the stations layer, with attributes def add_station(self, layer, xyz, name, flags): if layer is None: return attrs = [1 if flag in flags else 0 for flag in self.station_flags] attrs.insert(0, name) pr = layer.dataProvider() feat = QgsFeature() feat.setGeometry(QgsGeometry(QgsPointV2(QgsWKBTypes.PointZ, *xyz))) feat.setAttributes(attrs) pr.addFeatures([feat]) def run(self): """Run method that performs all the real work""" # show the dialog self.dlg.show() # Run the dialog event loop result = self.dlg.exec_() # See if OK was pressed if result: # This is where all the work is done survex3dfile = self.dlg.selectedFile.text() include_legs = self.dlg.checkLegs.isChecked() include_stations = self.dlg.checkStations.isChecked() exclude_surface_legs = not self.dlg.checkLegsSurface.isChecked() exclude_splay_legs = not self.dlg.checkLegsSplay.isChecked() exclude_duplicate_legs = not self.dlg.checkLegsDuplicate.isChecked( ) exclude_surface_stations = not self.dlg.checkStationsSurface.isChecked( ) get_crs = self.dlg.checkGetCRS.isChecked() # catch all exceptions and deal with them at the end try: # Create a temporary file we can write dump3d output into f = NamedTemporaryFile(delete=False) f.close() dump3dfile = f.name if not os.path.exists(dump3dfile): raise Exception("Couldn't create temporary file") # Run dump3d saving the output command = "dump3d %s > %s" % (survex3dfile, dump3dfile) p = Popen(command, shell=True, stderr=PIPE) rc = p.wait() err = p.stderr.read() if rc: raise Exception("dump3d failed with return code %i" % rc) # Now parse the dump3d output leg_layer, station_layer = None, None title, epsg = None, None legs = [] # We run this like a gawk /pattern/ { action } script with open(dump3dfile) as fp: for line in iter(fp): fields = line.split() legs_append = False if fields[0] == 'TITLE': title = ' '.join(fields[1:]).strip('"') if get_crs and fields[0] == 'CS': proj4string = ' '.join(fields[1:]) epsg = self.extract_epsg(proj4string) if fields[0] == 'MOVE': xyz_start = [float(v) for v in fields[1:4]] if include_legs and fields[0] == 'LINE': xyz_end = [float(v) for v in fields[1:4]] style = ' '.join(fields[5:]) if not leg_layer: leg_layer = self.add_layer( title, 'legs', 'LineString', epsg) self.add_leg_fields(leg_layer) while (True): if exclude_surface_legs and 'SURFACE' in style: break if exclude_splay_legs and 'SPLAY' in style: break if exclude_duplicate_legs and 'DUPLICATE' in style: break self.add_leg(leg_layer, xyz_start, xyz_end, style) break xyz_start = xyz_end if include_stations and fields[0] == 'NODE': xyz = [float(v) for v in fields[1:4]] name = fields[4].strip('[]') flags = ' '.join(fields[5:]) if not station_layer: station_layer = self.add_layer( title, 'stations', 'Point', epsg) self.add_station_fields(station_layer) while (True): if exclude_surface_stations and 'SURFACE' in flags: break self.add_station(station_layer, xyz, name, flags) break if leg_layer: leg_layer.updateExtents() QgsMapLayerRegistry.instance().addMapLayers([leg_layer]) if station_layer: station_layer.updateExtents() QgsMapLayerRegistry.instance().addMapLayers( [station_layer]) unlink(dump3dfile) # Here's where we catch all exceptions, cleaning up a possible # temporary file and re-raising the exception except Exception as e: if dump3dfile and os.path.exists(dump3dfile): unlink(dump3dfile) raise