def test_regenerate_fid(self): """Test RegeneratePrimaryKey flag""" temp_dir = QTemporaryDir() temp_path = temp_dir.path() gpkg_name = 'bug_31634_Multi_to_Singleparts_FID.gpkg' gpkg_path = os.path.join(temp_path, gpkg_name) shutil.copyfile(os.path.join(unitTestDataPath(), gpkg_name), gpkg_path) gpkg_layer = QgsVectorLayer( gpkg_path + '|layername=Multi_to_Singleparts_FID_bug', 'lyr', 'ogr') self.assertTrue(gpkg_layer.isValid()) f = next(gpkg_layer.getFeatures()) self.assertEqual(f['fid'], 1) res = QgsVectorLayerUtils.makeFeatureCompatible(f, gpkg_layer) self.assertEqual([ff['fid'] for ff in res], [1]) # if RegeneratePrimaryKey set then we should discard fid field res = QgsVectorLayerUtils.makeFeatureCompatible( f, gpkg_layer, QgsFeatureSink.RegeneratePrimaryKey) self.assertEqual([ff['fid'] for ff in res], [None])
def add_geojson_features(geojson, project, qgis_layer_id=None, connection_id=None, new_layer_name=None, name=None, style=None): """Add geojson features to a destination layer, the layer can be specified by passing a QgsVectorLayer instance or by specifying a connection and a new layer name plus the QDjango project for the new layer. The connection may assume the special values `__shapefile__`, `__spatialite__` or `__geopackage__` for the creation of new OGR files of the corresponding type. The creation of the new layer may raise an exception is a layer with the same name as new_layer_name already exists. For already existing layers the `qgis_layer_id` can be used, provided that the layer belongs to the current `project`. Returns the qgis_layer_id :param geojson: new features in GeoJSON format :type geojson: str :param project: QDjango Project instance for the new or the existing layer :type project: Project instance :param layer_id: optional, QGIS layer id :type layer_id: QGIS layer id :param connection: optional, connection id or the special value `__shapefile__`, `__spatialite__` or `__geopackage__` :type connection: str :param new_layer_name: optional, name of the new layer :type new_layer_name: str :param name: optional, name of the isochrone, default to current datetime :type name: str :param style: optional, dictionary with style properties: example {'color': [100, 50, 123], 'transparency': 0.5, 'stroke_width: 3 } :type style: dict :raises Exception: raise on error :rtype: str """ # Additional fields that are not returned by the service as feature attributes json_data = json.loads(geojson) if name is None: name = "Isochrone %s" % QDateTime.currentDateTime().toString( Qt.ISODateWithMs) metadata = { 'range_type': json_data['metadata']['query']['range_type'], 'name': name, # 'timestamp': json_data['metadata']['timestamp'], // Not supported } for f in json_data['features']: f['properties'].update(metadata) geojson = json.dumps(json_data) fields = QgsJsonUtils.stringToFields(geojson) # Patch timestamp type to DateTime // Not supported # fields.remove(fields.lookupField('timestamp')) #fields.append(QgsField('timestamp', QVariant.DateTime)) # Create the new layer if connection_id is not None: def _write_to_ogr(destination_path, new_layer_name, driverName=None): """Writes features to new or existing OGR layer""" tmp_dir = QTemporaryDir() tmp_path = os.path.join(tmp_dir.path(), 'isochrone.json') with open(tmp_path, 'w+') as f: f.write(geojson) tmp_layer = QgsVectorLayer(tmp_path, 'tmp_isochrone', 'ogr') if not tmp_layer.isValid(): raise Exception( _('Cannot create temporary layer for isochrone result.')) # Note: shp attribute names are max 10 chars long save_options = QgsVectorFileWriter.SaveVectorOptions() if driverName is not None: save_options.driverName = driverName save_options.layerName = new_layer_name save_options.fileEncoding = 'utf-8' # This is nonsense to me: if the file does not exist the actionOnExistingFile # should be ignored instead of raising an error, probable QGIS bug if os.path.exists(destination_path): # Check if the layer already exists layer_exists = QgsVectorFileWriter.targetLayerExists( destination_path, new_layer_name) if layer_exists: raise Exception( _('Cannot save isochrone result to destination layer: layer already exists (use "qgis_layer_id" instead)!' )) save_options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer error_code, error_message = QgsVectorFileWriter.writeAsVectorFormatV2( tmp_layer, destination_path, project.qgis_project.transformContext(), save_options) if error_code != QgsVectorFileWriter.NoError: raise Exception( _('Cannot save isochrone result to destination layer: ') + error_message) layer_uri = destination_path if driverName != 'ESRI Shapefile': layer_uri += '|layername=' + new_layer_name provider = 'ogr' return layer_uri, provider destination_path = None if connection_id == '__shapefile__': # new shapefile driverName = 'ESRI Shapefile' extension = 'shp' elif connection_id == '__spatialite__': # new sqlite driverName = 'SpatiaLite' extension = 'sqlite' elif connection_id == '__geopackage__': # new gpkg driverName = 'GPKG' extension = 'gpkg' else: # Add new table to an existing DB connection try: connection = get_db_connections( project.qgis_project)[connection_id] except: raise Exception(_('Wrong connection id.')) if connection['provider'] == 'ogr': destination_path = connection_id driverName = 'GPKG' if destination_path.lower().endswith( '.gpkg') else 'SpatiaLite' else: driverName = None # Create a new file/layer if driverName is not None: new_layer_name = os.path.basename(new_layer_name) if destination_path is None: # new files! destination_path = os.path.join( settings.DATASOURCE_PATH, "{}.{}".format(new_layer_name, extension)) i = 0 while os.path.exists(destination_path): i += 1 destination_path = os.path.join( settings.DATASOURCE_PATH, "{}_{}.{}".format(new_layer_name, i, extension)) layer_uri, provider = _write_to_ogr(destination_path, new_layer_name, driverName) # Create a new DB table else: assert connection['provider'] != 'ogr' md = QgsProviderRegistry.instance().providerMetadata( connection['provider']) if not md: raise Exception( _('Error creating destination layer connection.')) conn = md.createConnection(connection_id, {}) try: conn.createVectorTable(connection['schema'], new_layer_name, fields, QgsWkbTypes.Polygon, QgsCoordinateReferenceSystem(4326), False, {'geometryColumn': 'geom'}) except QgsProviderConnectionException as ex: raise Exception( _('Error creating destination layer: ') + str(ex)) uri = QgsDataSourceUri(conn.uri()) uri.setTable(new_layer_name) uri.setSchema(connection['schema']) uri.setSrid('4326') uri.setGeometryColumn('geom') provider = connection['provider'] layer_uri = uri.uri() # Now reload the new layer and add it to the project qgis_layer = QgsVectorLayer(layer_uri, new_layer_name, provider) if not qgis_layer.isValid(): raise Exception( _('Error creating destination layer: layer is not valid!')) qgis_layer_id = qgis_layer.id() with QgisProjectFileLocker(project) as project: apply_style(qgis_layer, style, True, name) project.qgis_project.addMapLayers([qgis_layer]) root = project.qgis_project.layerTreeRoot() if qgis_layer_id not in root.findLayerIds(): # Append layer at the end root.insertLayer(-1, qgis_layer) if not project.update_qgis_project(): raise Exception( _('Error saving the destination layer: could not write project!' )) # Retrieve the layer again because saving the project deleted it qgis_layer = project.qgis_project.mapLayer(qgis_layer_id) # Create Layer object instance, created = Layer.objects.get_or_create( qgs_layer_id=qgis_layer_id, project=project, defaults={ 'origname': new_layer_name, 'name': new_layer_name, 'title': new_layer_name, 'is_visible': True, 'layer_type': provider, 'srid': 4326, 'datasource': layer_uri, 'geometrytype': 'Polygon', # TODO: make this a property of the Layer object 'database_columns': str([{ 'name': f.name(), 'type': QVariant.typeToName(f.type()).upper(), 'label': f.displayName() } for f in qgis_layer.fields()]), }) if not created: raise Exception( _('Error adding destination Layer to the project: layer already exists.' )) # for OGR (already filled with features) returns the id of the new layer if driverName is not None: return qgis_layer_id # Append to an existing layer qgis_layer = project.qgis_project.mapLayer(qgis_layer_id) if qgis_layer is None: raise Exception( _('Error opening destination layer %s: layer not found in QGIS project!' % qgis_layer_id)) features = QgsJsonUtils.stringToFeatureList(geojson, fields) compatible_features = [] for f in features: compatible_features.extend( QgsVectorLayerUtils.makeFeatureCompatible(f, qgis_layer)) if qgis_layer.crs().isValid( ) and qgis_layer.crs() != QgsCoordinateReferenceSystem(4326): ct = QgsCoordinateTransform(QgsCoordinateReferenceSystem(4326), qgis_layer.crs(), project.qgis_project) for f in compatible_features: geom = f.geometry() if geom.transform(ct) != QgsGeometry.Success: raise Exception( _('Error transforming geometry from 4326 to destination layer CRS.' )) f.setGeometry(geom) if len(compatible_features) == 0: raise Exception(_('No compatible features returned from isochrone.')) if not qgis_layer.startEditing(): raise Exception(_('Destination layer is not editable.')) # Add features to the layer if not qgis_layer.addFeatures(compatible_features): raise Exception(_('Error adding features to the destination layer.')) if not qgis_layer.commitChanges(): raise Exception( _('Error committing features to the destination layer.')) if style is not None: with QgisProjectFileLocker(project) as project: apply_style(qgis_layer, style, False, name) project.update_qgis_project() return qgis_layer_id