def testContains(self): rect1 = QgsRectangle(0.0, 0.0, 5.0, 5.0) rect2 = QgsRectangle(2.0, 2.0, 7.0, 7.0) pnt1 = QgsPointXY(4.0, 4.0) pnt2 = QgsPointXY(6.0, 2.0) rect3 = rect1.intersect(rect2) self.assertTrue(rect1.contains(rect3)) self.assertTrue(rect2.contains(rect3)) # test for point self.assertTrue(rect1.contains(pnt1)) self.assertTrue(rect1.contains(pnt1.x(), pnt1.y())) self.assertTrue(rect2.contains(pnt1)) self.assertTrue(rect2.contains(pnt1.x(), pnt1.y())) self.assertTrue(rect3.contains(pnt1)) self.assertTrue(rect3.contains(pnt1.x(), pnt1.y())) self.assertFalse(rect1.contains(pnt2)) self.assertFalse(rect1.contains(pnt2.x(), pnt2.y())) self.assertTrue(rect2.contains(pnt2)) self.assertTrue(rect2.contains(pnt2.x(), pnt2.y())) self.assertFalse(rect3.contains(pnt2)) self.assertFalse(rect3.contains(pnt2.x(), pnt2.y())) self.assertTrue(rect3.contains(pnt1)) self.assertTrue(rect3.contains(pnt1.x(), pnt1.y()))
def getBlockRecAndItemFromPointInRaster(self, layer, p): pt = p # QgsCoordinateTransform(QgsProject.instance().crs(),layer.crs(),QgsProject.instance()).transform(p) dp = layer.dataProvider() finalExtent = dp.extent() # Calculate the row / column where the point falls xres = layer.rasterUnitsPerPixelX() yres = layer.rasterUnitsPerPixelY() from math import floor col = abs(floor((pt.x() - finalExtent.xMinimum()) / xres)) row = abs(floor((finalExtent.yMaximum() - pt.y()) / yres)) xMin = finalExtent.xMinimum() + col * xres xMax = xMin + xres yMax = finalExtent.yMaximum() - row * yres yMin = yMax - yres pixelExtent = QgsRectangle(xMin, yMin, xMax, yMax) # 1 is referring to band 1 if not (self.LAYER is None or self.BLOCK is None) and layer == self.LAYER: block = self.BLOCK else: block = dp.block(1, finalExtent, layer.width(), layer.height()) self.BLOCK = block self.LAYER = layer del dp if pixelExtent.contains(pt): return block, pixelExtent, row, col else: return False, False, False, False
def testUnion(self): rect1 = QgsRectangle( 0.0, 0.0, 5.0, 5.0) rect2 = QgsRectangle( 2.0, 2.0, 7.0, 7.0) pnt1 = QgsPoint(6.0, 2.0) rect1.combineExtentWith(rect2) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect1.contains(rect2))) assert rect1.contains(rect2), myMessage print rect1.toString() assert rect1 == QgsRectangle(0.0, 0.0, 7.0, 7.0), "Wrong combine with rectangle result" rect1 = QgsRectangle( 0.0, 0.0, 5.0, 5.0) rect1.combineExtentWith(6.0, 2.0) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect1.contains(pnt1))) assert rect1.contains(pnt1), myMessage myExpectedResult = QgsRectangle(0.0, 0.0, 6.0, 5.0).toString() myResult = rect1.toString() myMessage = ('Expected: %s\nGot: %s\n' % (myExpectedResult, myResult)) self.assertEquals(myResult, myExpectedResult, myMessage) rect1 = QgsRectangle( 0.0, 0.0, 5.0, 5.0) rect1.unionRect(rect2) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect1.contains(rect2))) assert rect1.contains(rect2), myMessage assert rect1 == QgsRectangle(0.0, 0.0, 7.0, 7.0), "Wrong union result"
def testUnion(self): rect1 = QgsRectangle(0.0, 0.0, 5.0, 5.0) rect2 = QgsRectangle(2.0, 2.0, 7.0, 7.0) pnt1 = QgsPointXY(6.0, 2.0) rect1.combineExtentWith(rect2) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect1.contains(rect2))) assert rect1.contains(rect2), myMessage print((rect1.toString())) assert rect1 == QgsRectangle( 0.0, 0.0, 7.0, 7.0), 'Wrong combine with rectangle result' rect1 = QgsRectangle(0.0, 0.0, 5.0, 5.0) rect1.combineExtentWith(6.0, 2.0) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect1.contains(pnt1))) assert rect1.contains(pnt1), myMessage myExpectedResult = QgsRectangle(0.0, 0.0, 6.0, 5.0).toString() myResult = rect1.toString() myMessage = ('Expected: %s\nGot: %s\n' % (myExpectedResult, myResult)) self.assertEqual(myResult, myExpectedResult, myMessage) rect1 = QgsRectangle(0.0, 0.0, 5.0, 5.0) rect1.combineExtentWith(rect2) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect1.contains(rect2))) assert rect1.contains(rect2), myMessage assert rect1 == QgsRectangle(0.0, 0.0, 7.0, 7.0), "Wrong union result"
def testUnion(self): rect1 = QgsRectangle(0.0, 0.0, 5.0, 5.0) rect2 = QgsRectangle(2.0, 2.0, 7.0, 7.0) pnt1 = QgsPointXY(6.0, 2.0) rect1.combineExtentWith(rect2) self.assertTrue(rect1.contains(rect2)) self.assertEqual(rect1, QgsRectangle(0.0, 0.0, 7.0, 7.0)) rect1 = QgsRectangle(0.0, 0.0, 5.0, 5.0) rect1.combineExtentWith(6.0, 2.0) self.assertTrue(rect1.contains(pnt1)) self.assertEqual(rect1.toString(), QgsRectangle(0.0, 0.0, 6.0, 5.0).toString()) rect1 = QgsRectangle(0.0, 0.0, 5.0, 5.0) rect1.combineExtentWith(rect2) self.assertTrue(rect1.contains(rect2)) self.assertEqual(rect1, QgsRectangle(0.0, 0.0, 7.0, 7.0))
def canvasReleaseEvent(self, event): x = event.pos().x() y = event.pos().y() point = self.canvas.getCoordinateTransform().toMapCoordinates(x, y) bbox = QgsRectangle(4999.99,4999.69,660000.06,1225000.12) if bbox.contains(point): os_ref = xy_to_osgb(point.x(), point.y(), 10) point_4326 = reproject_point_to_4326(self.canvas, point) QApplication.clipboard().setText(os_ref) msg = "Grid Ref: {}\n\nLong,Lat: {:.2f}, {:.2f}\n\nCopied to clipboard".format(os_ref, point_4326.x(), point_4326.y()) QMessageBox.information(None, "OS Grid Reference", msg) else: QMessageBox.information(None, "OS Grid Reference", "Point out of bounds")
def updatecanvasfromgps(self, position, gpsinfo): # Recenter map if we go outside of the 95% of the area if not self.lastgpsposition == position: self.lastposition = position rect = QgsRectangle(position, position) extentlimt = QgsRectangle(self.canvas.extent()) extentlimt.scale(0.95) if not extentlimt.contains(position): self.canvas.setExtent(rect) self.canvas.refresh() self.marker.show() self.marker.setCenter(position) self.gpslabel.setText("GPS: PDOP {} HDOP {} VDOP {}".format( gpsinfo.pdop, gpsinfo.hdop, gpsinfo.vdop))
def updatecanvasfromgps(self, position, gpsinfo): # Recenter map if we go outside of the 95% of the area if not self.lastgpsposition == position: self.lastposition = position rect = QgsRectangle(position, position) extentlimt = QgsRectangle(self.canvas.extent()) extentlimt.scale(0.95) if not extentlimt.contains(position): self.canvas.setExtent(rect) self.canvas.refresh() self.marker.show() self.marker.setCenter(position) self.gpslabel.setText("GPS: PDOP {} HDOP {} VDOP {}".format(gpsinfo.pdop, gpsinfo.hdop, gpsinfo.vdop))
def gps_update_canvas(self, position, gpsinfo): # Recenter map if we go outside of the 95% of the area if self.gpslogging.logging: self.gpsband.addPoint(position) self.gpsband.show() if roam.config.settings.get('gpscenter', True): if not self.lastgpsposition == position: self.lastposition = position rect = QgsRectangle(position, position) extentlimt = QgsRectangle(self.canvas.extent()) extentlimt.scale(0.95) if not extentlimt.contains(position): self.zoom_to_location(position) self.marker.show() self.marker.setCenter(position)
def canvasReleaseEvent(self, event): x = event.pos().x() y = event.pos().y() point = self.canvas.getCoordinateTransform().toMapCoordinates(x, y) bbox = QgsRectangle(4999.99, 4999.69, 660000.06, 1225000.12) if bbox.contains(point): os_ref = xy_to_osgb(point.x(), point.y(), self.precision) point_4326 = reproject_point_to_4326(self.canvas, point) msg = "Grid Ref: {}\n\nLong,Lat: {:.2f}, {:.2f}\n".format( os_ref, point_4326.x(), point_4326.y()) if self.clipboard_enable: QApplication.clipboard().setText(os_ref) msg += "\nCopied to clipboard" QMessageBox.information(None, "OS Grid Reference", msg) else: QMessageBox.information(None, "OS Grid Reference", "Point out of bounds")
def xform(extent, srcCRS, destCRS): if srcCRS == destCRS: return extent # nothing to do # cut down the extent so it's not too big # clamped to the projection's valid area (usually small) projectionClampedExtent = clampToProjectionExtent(extent, srcCRS) # clamped to the world (might not work in some projections) worldClampedExtent = clampToWorld(extent, srcCRS) # xform the two extents to the dest CRS transform = QgsCoordinateTransform(srcCRS, destCRS, QgsProject.instance()) xformed_projectionClamped = QgsRectangle() # null rect if not projectionClampedExtent.isNull(): xformed_projectionClamped = transform.transformBoundingBox(projectionClampedExtent) xformed_worldClamped = QgsRectangle() # null rect if not worldClampedExtent.isNull(): xformed_worldClamped = transform.transformBoundingBox(worldClampedExtent) # make final choice of what to use # we WANT to use the world one, but sometimes cannot # indicates everything is ok -- likely no projection blowing up if xformed_worldClamped.contains(xformed_projectionClamped): return xformed_worldClamped # likely good # if one is null, return the other if xformed_projectionClamped.isNull(): return xformed_worldClamped if xformed_worldClamped.isNull(): return xformed_projectionClamped # clamp to the destination CRS (last resort to try to get something valid) return clampToProjectionExtent(xformed_worldClamped, destCRS)
def locationChanged(self, lat, lng, yaw, angle): transform = self.coordinatetransform() point = transform.transform(float(lng), float(lat)) self.marker.setCenter(point) yaw = float(yaw) self.marker.setAngle(angle) self.marker.setYaw(yaw) self.marker.setTracking(self.viewer.tracking) if self.marker.tracking: rect = QgsRectangle(point, point) extentlimt = QgsRectangle(self.canvas.extent()) extentlimt.scale(0.95) if not extentlimt.contains(point): self.canvas.setExtent(rect) self.canvas.refresh() # Clear old features self.viewer.clear_features() self.load_layer_features(point)
def gps_update_canvas(self, position, gpsinfo): """ Updates the map canvas based on the GPS position. By default if the GPS is outside the canvas extent the canvas will move to center on the GPS. Can be turned off in settings. :param postion: The current GPS position. :param gpsinfo: The extra GPS information """ # Recenter map if we go outside of the 95% of the area if self.gpslogging.logging: self.gpsband.addPoint(position) self.gpsband.show() if roam.config.settings.get('gpscenter', True): if not self.lastgpsposition == position: self.lastgpsposition = position rect = QgsRectangle(position, position) extentlimt = QgsRectangle(self.canvas.extent()) extentlimt.scale(0.95) if not extentlimt.contains(position): self.zoom_to_location(position) self.gpsMarker.show() self.gpsMarker.setCenter(position, gpsinfo)
def gps_update_canvas(self, position, gpsinfo): """ Updates the map canvas based on the GPS position. By default if the GPS is outside the canvas extent the canvas will move to center on the GPS. Can be turned off in settings. :param postion: The current GPS position. :param gpsinfo: The extra GPS information """ # Recenter map if we go outside of the 95% of the area if self.gpslogging.logging: self.gpsband.addPoint(position) self.gpsband.show() if roam.config.settings.get('gpscenter', True): if not self.lastgpsposition == position: self.lastposition = position rect = QgsRectangle(position, position) extentlimt = QgsRectangle(self.canvas.extent()) extentlimt.scale(0.95) if not extentlimt.contains(position): self.zoom_to_location(position) self.marker.show() self.marker.setCenter(position, gpsinfo)
class GSIElevTileProvider: def __init__(self, dest_wkt): self.dest_wkt = dest_wkt # crs transformer, which aims to calculate bbox in EPSG:3857 self.crs3857 = QgsCoordinateReferenceSystem(3857) self.dest_crs = QgsCoordinateReferenceSystem() if not self.dest_crs.createFromWkt(dest_wkt): logMessage("Failed to create CRS from WKT: {0}".format(dest_wkt)) self.transform = QgsCoordinateTransform(self.dest_crs, self.crs3857) # approximate bbox of this data self.boundingbox = QgsRectangle(13667807, 2320477, 17230031, 5713298) self.downloader = Downloader() self.downloader.userAgent = "QGIS/{0} Qgis2threejs GSIElevTileProvider".format(QGis.QGIS_VERSION) # not written since QGIS 2.2 self.downloader.DEFAULT_CACHE_EXPIRATION = QSettings().value("/qgis/defaultTileExpiry", 24, type=int) self.driver = gdal.GetDriverByName("MEM") self.last_dataset = None def name(self): return "GSI Elevation Tile" def read(self, width, height, extent): # calculate bounding box in EPSG:3857 geometry = extent.geometry() geometry.transform(self.transform) merc_rect = geometry.boundingBox() # if the bounding box doesn't intersect with the bounding box of this data, return a list filled with nodata value if not self.boundingbox.intersects(merc_rect): return [NODATA_VALUE] * width * height # get tiles over_smpl = 1 segments_x = 1 if width == 1 else width - 1 res = extent.width() / segments_x / over_smpl ds = self.getDataset(merc_rect.xMinimum(), merc_rect.yMinimum(), merc_rect.xMaximum(), merc_rect.yMaximum(), res) geotransform = extent.geotransform(width, height) return self._read(ds, width, height, geotransform) def readValue(self, x, y): """Get value at the position using 1px * 1px memory raster. The value is calculated using a tile of max zoom level""" # coordinate transformation into EPSG:3857 pt = self.transform.transform(QgsPoint(x, y)) # if the point is not within the bounding box of this data, return nodata value if not self.boundingbox.contains(pt): return NODATA_VALUE res = 0.1 hres = res / 2 ds = self.getDataset(pt.x() - hres, pt.y() - hres, pt.x() + hres, pt.y() + hres, res) geotransform = [x - hres, res, 0, y + hres, 0, -res] return self._read(ds, 1, 1, geotransform)[0] def _read(self, ds, width, height, geotransform): # create a memory dataset warped_ds = self.driver.Create("", width, height, 1, gdal.GDT_Float32) warped_ds.SetProjection(self.dest_wkt) warped_ds.SetGeoTransform(geotransform) # reproject image gdal.ReprojectImage(ds, warped_ds, None, None, gdal.GRA_Bilinear) # load values into an array band = warped_ds.GetRasterBand(1) fs = "f" * width * height return struct.unpack(fs, band.ReadRaster(0, 0, width, height, buf_type=gdal.GDT_Float32)) def getDataset(self, xmin, ymin, xmax, ymax, mapUnitsPerPixel): # calculate zoom level mpp1 = TSIZE1 / TILE_SIZE zoom = int(math.ceil(math.log(mpp1 / mapUnitsPerPixel, 2) + 1)) zoom = max(0, min(zoom, ZMAX)) # calculate tile range (yOrigin is top) size = TSIZE1 / 2 ** (zoom - 1) matrixSize = 2 ** zoom ulx = max(0, int((xmin + TSIZE1) / size)) uly = max(0, int((TSIZE1 - ymax) / size)) lrx = min(int((xmax + TSIZE1) / size), matrixSize - 1) lry = min(int((TSIZE1 - ymin) / size), matrixSize - 1) cols = lrx - ulx + 1 rows = lry - uly + 1 # download count limit if cols * rows > 128: logMessage("Number of tiles to fetch is too large!") width = height = 1 return self.driver.Create("", width, height, 1, gdal.GDT_Float32, []) if self.last_dataset and self.last_dataset[0] == [zoom, ulx, uly, lrx, lry]: # if same as last tile set, return cached dataset return self.last_dataset[1] urltmpl = "http://cyberjapandata.gsi.go.jp/xyz/dem/{z}/{x}/{y}.txt" #urltmpl = "http://localhost/xyz/dem/{z}/{x}/{y}.txt" tiles = self.fetchFiles(urltmpl, zoom, ulx, uly, lrx, lry) # create a memory dataset width = cols * TILE_SIZE height = rows * TILE_SIZE res = size / TILE_SIZE geotransform = [ulx * size - TSIZE1, res, 0, TSIZE1 - uly * size, 0, -res] #mem_driver = gdal.GetDriverByName("GTiff") #ds = mem_driver.Create("D:/fetched_tile.tif", width, height, 1, gdal.GDT_Float32, []) ds = self.driver.Create("", width, height, 1, gdal.GDT_Float32, []) ds.SetProjection(str(self.crs3857.toWkt())) ds.SetGeoTransform(geotransform) band = ds.GetRasterBand(1) for i, tile in enumerate(tiles): if tile: col = i % cols row = i / cols band.WriteRaster(col * TILE_SIZE, row * TILE_SIZE, TILE_SIZE, TILE_SIZE, tile) ds.FlushCache() self.last_dataset = [[zoom, ulx, uly, lrx, lry], ds] # cache dataset return ds def fetchFiles(self, urltmpl, zoom, xmin, ymin, xmax, ymax): downloadTimeout = 60 urls = [] for y in range(ymin, ymax + 1): for x in range(xmin, xmax + 1): urls.append(urltmpl.replace("{x}", str(x)).replace("{y}", str(y)).replace("{z}", str(zoom))) files = self.downloader.fetchFiles(urls, downloadTimeout) for url in urls: data = files[url] if data: yield numpy.fromstring(data.replace("e", str(NODATA_VALUE)).replace("\n", ","), dtype=numpy.float32, sep=",").tostring() # to byte array else: array = numpy.empty(TILE_SIZE * TILE_SIZE, dtype=numpy.float32) array.fill(NODATA_VALUE) yield array.tostring()
def testContains(self): rect1 = QgsRectangle( 0.0, 0.0, 5.0, 5.0) rect2 = QgsRectangle( 2.0, 2.0, 7.0, 7.0) pnt1 = QgsPoint(4.0, 4.0) pnt2 = QgsPoint(6.0, 2.0) rect3 = rect1.intersect(rect2) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect1.contains(rect3))) assert rect1.contains(rect3), myMessage myMessage = ('Expected: %s\nGot: %s\n' % (True, rect2.contains(rect3))) assert rect2.contains(rect3), myMessage # test for point myMessage = ('Expected: %s\nGot: %s\n' % (True, rect1.contains(pnt1))) assert rect1.contains(pnt1), myMessage myMessage = ('Expected: %s\nGot: %s\n' % (True, rect2.contains(pnt1))) assert rect2.contains(pnt1), myMessage myMessage = ('Expected: %s\nGot: %s\n' % (True, rect3.contains(pnt1))) assert rect3.contains(pnt1), myMessage myMessage = ('Expected: %s\nGot: %s\n' % (False, rect1.contains(pnt2))) self.assertFalse(rect1.contains(pnt2), myMessage) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect2.contains(pnt2))) assert rect2.contains(pnt2), myMessage myMessage = ('Expected: %s\nGot: %s\n' % (False, rect3.contains(pnt2))) self.assertFalse(rect3.contains(pnt2), myMessage) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect3.contains(pnt1))) self.assertTrue(rect3.contains(pnt1), myMessage)
def make_coco_dataset(self): layers = QgsProject.instance().mapLayers().values() vectorlayers = [layer for layer in layers if layer.type() == VectorLayer] rasterLayers = [layer for layer in layers if layer.type() == RasterLayer] vectorlayer = vectorlayers[self.dlg.vectorlayerselector.currentIndex()] rasterlayer = rasterLayers[self.dlg.rasterlayerselector.currentIndex()] img_size = int(self.dlg.imagesizeselector.currentText()) scale_realworld = int(self.dlg.zoomlevel.currentText()) buffer_size = int(self.dlg.buffersize.text()) / 100 target_dir = self.dlg.dirselectline.text() dataset_description = str(self.dlg.datasetdescription.text()) contributor = str(self.dlg.creatorname.text()) url = str(self.dlg.url.text()) version = str(self.dlg.datasetversion.text()) license = str(self.dlg.licenseselector.currentText()) # to implement scale_to_fit = self.dlg.scaletofit.isChecked() use_fieldcategory = bool(self.dlg.usecategoryfields.isChecked()) categoryfield = self.dlg.categoryfields.currentText() QgsMessageLog.logMessage(str(scale_to_fit), "cocotrainer", level=Qgis.Info) QgsMessageLog.logMessage(str(use_fieldcategory), "cocotrainer", level=Qgis.Info) # prepare directories os.makedirs(os.path.join(target_dir, "train"), exist_ok=True) os.makedirs(os.path.join(target_dir, "val"), exist_ok=True) QgsMessageLog.logMessage("====================================", "cocotrainer", level=Qgis.Info) # TODO: # - use field categories features_iterator = vectorlayer.getFeatures(QgsFeatureRequest().setFilterExpression('$area > 1')) features = [feature for feature in features_iterator] QgsMessageLog.logMessage("vector layer: " + vectorlayer.name(), "cocotrainer", level=Qgis.Info) QgsMessageLog.logMessage("Filtered polygons smaller than 1m2 from data", "cocotrainer", level=Qgis.Info) QgsMessageLog.logMessage("features: " + str(len(list(features))), "cocotrainer", level=Qgis.Info) options = QgsMapSettings() options.setLayers([rasterlayer]) options.setOutputSize(QSize(int(img_size), int(img_size))) QgsMessageLog.logMessage(str(scale_realworld), "cocotrainer", level=Qgis.Info) QgsMessageLog.logMessage(str(img_size), "cocotrainer", level=Qgis.Info) date = datetime.now() cat_dict = {} if use_fieldcategory: categories = [] # uniqueprovider = rasterlayer.dataProvider() fields = vectorlayer.fields() id = fields.indexFromName(categoryfield) uniquevalues = vectorlayer.uniqueValues(id) for i, val in enumerate(uniquevalues): categories.append({"supercategory": "object", "id": i, "name": str(val)}) cat_dict[str(val)] = i else: categories = [{"supercategory": "object", "id": 0, "name": "CATEGORYNAME"}] cat_dict["CATEGORYNAME"] = 0 coco_annotation = { "info": { "description": dataset_description, "url": url, "version": version, "year": int(date.strftime("%Y")), "contributor": contributor, "date_created": date.strftime("%d/%m/%y") }, "licenses": [self.license_dict[license]], "images": [], "annotations": [], "categories": categories, # < -- Not in Captionsannotations "segment_info": [] # < -- Only in Panoptic annotations } bboxes, polygons = {}, {} for i, feature in enumerate(features): polygons[i] = feature.geometry() bboxes[i] = feature.geometry().boundingBox() if use_fieldcategory: cats = {} for i, feature in enumerate(features): cats[i] = str(feature[categoryfield]) image_id = 0 annotation_id = 0 for i, feature in enumerate(features): self.dlg.progressBar.setValue((i / len(polygons) * 100)) QgsMessageLog.logMessage("something is happening", "cocotrainer", level=Qgis.Info) geoms = feature.geometry() bbox = geoms.boundingBox() if scale_to_fit: xmax = bbox.xMaximum() ymax = bbox.yMaximum() ymin = bbox.yMinimum() xmin = bbox.xMinimum() diffx = xmax - xmin diffy = ymax - ymin xmax = xmax + (buffer_size * diffx) xmin = xmin - (buffer_size * diffx) ymax = ymax + (buffer_size * diffy) ymin = ymin - (buffer_size * diffy) else: midpoint = bbox.center() QgsMessageLog.logMessage("trying to log centerpoint", "cocotrainer", level=Qgis.Info) QgsMessageLog.logMessage(str(midpoint.x()), "cocotrainer", level=Qgis.Info) QgsMessageLog.logMessage(str(midpoint.y()), "cocotrainer", level=Qgis.Info) xmin = midpoint.x() - scale_realworld / 2 xmax = midpoint.x() + scale_realworld / 2 ymin = midpoint.y() - scale_realworld / 2 ymax = midpoint.y() + scale_realworld / 2 extent = QgsRectangle(xmin, ymin, xmax, ymax) fileid = "{}_{}_{}".format(xmin, ymin, scale_realworld) image = { "license": 0, "file_name": fileid + ".png", "coco_url": None, "height": img_size, "width": img_size, "date_captured": date.strftime("%d/%m/%y"), "flickr_url": None, "id": image_id } coco_annotation["images"].append(image) for j, geo in bboxes.items(): if extent.contains(geo) or extent.intersects(geo): points = polygons[j].asMultiPolygon() xs = np.array([p.x() - xmin for p in points[0][0]]) ys = np.array([(ymax - ymin) - (p.y() - ymin) for p in points[0][0]]) xs *= (img_size / scale_realworld) xs = np.round(xs) xs = xs.astype(np.int32) ys *= (img_size / scale_realworld) ys = np.round(ys) ys = ys.astype(np.int32) ys[ys < 0] = 0 xs[xs < 0] = 0 polygon = [[int(p[0]), int(p[1])] for p in zip(xs, ys)] flat_list = [item for sublist in polygon for item in sublist] QgsMessageLog.logMessage(str(flat_list), "cocotrainer", level=Qgis.Info) pixelposbbox = [int(np.min(xs)), (int(np.min(ys))), geo.width() * (img_size / scale_realworld), geo.height() * (img_size / scale_realworld)] pixelposbbox = [number if number > 0 else 0 for number in pixelposbbox] QgsMessageLog.logMessage(str([geo.xMinimum(), geo.yMaximum(), geo.width(), geo.height()]), "cocotrainer", level=Qgis.Info) QgsMessageLog.logMessage(str(pixelposbbox), "cocotrainer", level=Qgis.Info) annotation = { "segmentation": [flat_list], # format is [x1,y1,x2,y2] "area": polygons[j].area(), "iscrowd": 0, "image_id": image_id, "bbox": pixelposbbox, # format is [top left x position, top left y position, width, height] "category_id": cat_dict[cats[j]], "id": annotation_id } annotation_id += 1 coco_annotation["annotations"].append(annotation) options.setExtent(extent) render = QgsMapRendererParallelJob(options) def finished(): QgsMessageLog.logMessage("saving image to disk", "cocotrainer", level=Qgis.Info) fname = os.path.join(target_dir, 'train', "{}.png".format(fileid)) img = render.renderedImage() if not img.save(fname, "png"): print("Error saving image") render.finished.connect(finished) render.start() render.waitForFinished() image_id += 1 QgsMessageLog.logMessage(str(coco_annotation), "cocotrainer", level=Qgis.Info) with open(os.path.join(target_dir, "annotation.json"), "w") as outfile: json.dump(coco_annotation, outfile) self.dlg.progressBar.setValue(100) self.dlg.finished_label.setText("parsed {} features".format(len(list(features))))
class GSIElevTileProvider: def __init__(self, dest_wkt): self.dest_wkt = dest_wkt # crs transformer, which aims to calculate bbox in EPSG:3857 self.crs3857 = QgsCoordinateReferenceSystem(3857) self.dest_crs = QgsCoordinateReferenceSystem() if not self.dest_crs.createFromWkt(dest_wkt): logMessage("Failed to create CRS from WKT: {0}".format(dest_wkt)) self.transform = QgsCoordinateTransform(self.dest_crs, self.crs3857, QgsProject.instance()) # approximate bbox of this data self.boundingbox = QgsRectangle(13667807, 2320477, 17230031, 5713298) self.downloader = Downloader() self.downloader.userAgent = "QGIS/{0} Qgis2threejs GSIElevTileProvider".format( Qgis.QGIS_VERSION_INT ) # will be overwritten in QgsNetworkAccessManager::createRequest() since 2.2 self.downloader.DEFAULT_CACHE_EXPIRATION = QSettings().value( "/qgis/defaultTileExpiry", 24, type=int) self.driver = gdal.GetDriverByName("MEM") self.last_dataset = None def name(self): return "GSI Elevation Tile" def read(self, width, height, extent): # calculate bounding box in EPSG:3857 geometry = extent.geometry() geometry.transform(self.transform) merc_rect = geometry.boundingBox() # if the bounding box doesn't intersect with the bounding box of this data, return a list filled with nodata value if not self.boundingbox.intersects(merc_rect): return [NODATA_VALUE] * width * height # get tiles over_smpl = 1 segments_x = 1 if width == 1 else width - 1 res = extent.width() / segments_x / over_smpl ds = self.getDataset(merc_rect.xMinimum(), merc_rect.yMinimum(), merc_rect.xMaximum(), merc_rect.yMaximum(), res) geotransform = extent.geotransform(width, height) return self._read(ds, width, height, geotransform) def readValue(self, x, y): """Get value at the position using 1px * 1px memory raster. The value is calculated using a tile of max zoom level""" # coordinate transformation into EPSG:3857 pt = self.transform.transform(QgsPoint(x, y)) # if the point is not within the bounding box of this data, return nodata value if not self.boundingbox.contains(pt): return NODATA_VALUE res = 0.1 hres = res / 2 ds = self.getDataset(pt.x() - hres, pt.y() - hres, pt.x() + hres, pt.y() + hres, res) geotransform = [x - hres, res, 0, y + hres, 0, -res] return self._read(ds, 1, 1, geotransform)[0] def _read(self, ds, width, height, geotransform): # create a memory dataset warped_ds = self.driver.Create("", width, height, 1, gdal.GDT_Float32) warped_ds.SetProjection(self.dest_wkt) warped_ds.SetGeoTransform(geotransform) # reproject image gdal.ReprojectImage(ds, warped_ds, None, None, gdal.GRA_Bilinear) # load values into an array band = warped_ds.GetRasterBand(1) fs = "f" * width * height return struct.unpack( fs, band.ReadRaster(0, 0, width, height, buf_type=gdal.GDT_Float32)) def getDataset(self, xmin, ymin, xmax, ymax, mapUnitsPerPixel): # calculate zoom level mpp1 = TSIZE1 / TILE_SIZE zoom = int(math.ceil(math.log(mpp1 / mapUnitsPerPixel, 2) + 1)) zoom = max(0, min(zoom, ZMAX)) # calculate tile range (yOrigin is top) size = TSIZE1 / 2**(zoom - 1) matrixSize = 2**zoom ulx = max(0, int((xmin + TSIZE1) / size)) uly = max(0, int((TSIZE1 - ymax) / size)) lrx = min(int((xmax + TSIZE1) / size), matrixSize - 1) lry = min(int((TSIZE1 - ymin) / size), matrixSize - 1) cols = lrx - ulx + 1 rows = lry - uly + 1 # download count limit if cols * rows > 128: logMessage("Number of tiles to fetch is too large!") width = height = 1 return self.driver.Create("", width, height, 1, gdal.GDT_Float32, []) if self.last_dataset and self.last_dataset[0] == [ zoom, ulx, uly, lrx, lry ]: # if same as last tile set, return cached dataset return self.last_dataset[1] urltmpl = "http://cyberjapandata.gsi.go.jp/xyz/dem/{z}/{x}/{y}.txt" #urltmpl = "http://localhost/xyz/dem/{z}/{x}/{y}.txt" tiles = self.fetchFiles(urltmpl, zoom, ulx, uly, lrx, lry) # create a memory dataset width = cols * TILE_SIZE height = rows * TILE_SIZE res = size / TILE_SIZE geotransform = [ ulx * size - TSIZE1, res, 0, TSIZE1 - uly * size, 0, -res ] #mem_driver = gdal.GetDriverByName("GTiff") #ds = mem_driver.Create("D:/fetched_tile.tif", width, height, 1, gdal.GDT_Float32, []) ds = self.driver.Create("", width, height, 1, gdal.GDT_Float32, []) ds.SetProjection(str(self.crs3857.toWkt())) ds.SetGeoTransform(geotransform) band = ds.GetRasterBand(1) for i, tile in enumerate(tiles): if tile: col = i % cols row = i // cols band.WriteRaster(col * TILE_SIZE, row * TILE_SIZE, TILE_SIZE, TILE_SIZE, tile) ds.FlushCache() self.last_dataset = [[zoom, ulx, uly, lrx, lry], ds] # cache dataset return ds def fetchFiles(self, urltmpl, zoom, xmin, ymin, xmax, ymax): downloadTimeout = 60 urls = [] for y in range(ymin, ymax + 1): for x in range(xmin, xmax + 1): urls.append( urltmpl.replace("{x}", str(x)).replace("{y}", str(y)).replace( "{z}", str(zoom))) files = self.downloader.fetchFiles(urls, downloadTimeout) for url in urls: data = files[url] if data: yield numpy.fromstring(data.replace( b"e", NODATA_VALUE_BYTES).replace(b"\n", b","), dtype=numpy.float32, sep=",").tostring() # to byte array else: array = numpy.empty(TILE_SIZE * TILE_SIZE, dtype=numpy.float32) array.fill(NODATA_VALUE) yield array.tostring()
def runContextMeasure(self, feedback, reference, test, method, threshold) -> MatchPairManager: """ Processing the matching using Context measure. Return: the MatchPairManager """ # 1) Initial calculus # 1.1) Descobrir quem eh menor, que serve de param # SearchLength = Diagonal / PointCount * 20 -> distance for 20 points in Diagonal boxA = reference.extent() boxB = test.extent() searchLengthA = math.sqrt(boxA.width()**2 + boxA.height()**2 ) / reference.featureCount() * 20. searchLengthB = math.sqrt(boxB.width()**2 + boxB.height()**2) / test.featureCount() * 20. # parameters for context cttSearchLength = searchLengthA if searchLengthA > searchLengthB else searchLengthB cttDistanceStep = cttSearchLength / 20. cttAngleStep = math.pi / 6. # outros refIsMulti = QgsWkbTypes.isMultiType(int(reference.wkbType())) testIsMulti = QgsWkbTypes.isMultiType(int(test.wkbType())) # 2) Calculate the context context = ContextMeasure() shapeContextA = context.calculateShapeContext(reference, cttSearchLength, cttAngleStep, cttDistanceStep) feedback.setProgress(10) shapeContextB = context.calculateShapeContext(test, cttSearchLength, cttAngleStep, cttDistanceStep) feedback.setProgress(20) # 3) Run the processing ncols = reference.featureCount() nrows = test.featureCount() distMatrix = [[1. for x in range(ncols + 1)] for y in range(nrows + 1)] # 3.1) Compute the number of steps to display within the progress bar and # get features from source total = 80.0 / ncols # 3.2) Itera sobre os ref e procura o equivalente em test for j, refFeat in enumerate(reference.getFeatures()): # running chks if feedback.isCanceled(): break # Coloque o ID no lugar distMatrix[0][j + 1] = refFeat.id() refGeom = refFeat.geometry() refHist = shapeContextA.get(refFeat.id()) # Chks habituais if refGeom.isEmpty() or refHist is None: continue # obtem a geometria e monta o box de busca - baseado no cttSearchLength refPoint = refGeom.asPoint( ) if not refIsMulti else refGeom.asMultiPoint()[0].asPoint() refBox = QgsRectangle(refPoint.x() - cttSearchLength, refPoint.y() - cttSearchLength, refPoint.x() + cttSearchLength, refPoint.y() + cttSearchLength) for i, testFeat in enumerate(test.getFeatures()): # Coloque o ID no lugar - repetindo distMatrix[i + 1][0] = testFeat.id() testGeom = testFeat.geometry() testHist = shapeContextB.get(testFeat.id()) # Chks habituais if testGeom.isEmpty() or testHist is None: continue # obtem a geometria testPoint = testGeom.asPoint( ) if not testIsMulti else testGeom.asMultiPoint()[0].asPoint() # ignore o disjoint if not refBox.contains(testPoint): continue # agora sim, o que a gente veio fazer aqui: distance distMatrix[i + 1][j + 1] = context.distanceContext( refHist, testHist) feedback.setProgress(20 + int(j * total)) # fim for_feat # debug # 4) OK, tenho tudo, faltam os pares pairMgr = MatchPairManager() # check o criteria criteria = pairMgr.CriteriaType.ISMINIMUM if method % 2 == 0 else pairMgr.CriteriaType.BOTHMIN pairMgr.buildFromMatrix(distMatrix, criteria, threshold) return pairMgr
def runEuclideanDistance(self, feedback, reference, test, method, threshold) -> MatchPairManager: """ Processing the matching using Euclidean distance. Return: the MatchPairManager """ # 2) test parameters testExtent = test.extent() maxDistance = testExtent.width( ) if testExtent.width() > testExtent.height() else testExtent.height() if maxDistance < 2. * threshold: raise Exception( self. tr("Test data with a small bounding box. It should be at least twice the threshold." ), "INVALIDPARAMETERVALUE") refIsMulti = QgsWkbTypes.isMultiType(int(reference.wkbType())) testIsMulti = QgsWkbTypes.isMultiType(int(test.wkbType())) # 3) Run the processing ncols = reference.featureCount() nrows = test.featureCount() distMatrix = [[int(maxDistance) for x in range(ncols + 1)] for y in range(nrows + 1)] # 3.1) Compute the number of steps to display within the progress bar and # get features from source total = 100.0 / ncols # 3.2) Itera sobre os ref e procura o equivalente em test for j, refFeat in enumerate(reference.getFeatures()): # running chks if feedback.isCanceled(): break # Coloque o ID no lugar distMatrix[0][j + 1] = refFeat.id() #refFeat.id() refGeom = refFeat.geometry() # Chks habituais if refGeom.isEmpty(): continue # obtem a geometria e monta o box de busca refPoint = refGeom.asPoint( ) if not refIsMulti else refGeom.asMultiPoint()[0].asPoint() refBox = QgsRectangle(refPoint.x() - threshold, refPoint.y() - threshold, refPoint.x() + threshold, refPoint.y() + threshold) for i, testFeat in enumerate(test.getFeatures()): # Coloque o ID no lugar - repetindo distMatrix[i + 1][0] = testFeat.id() #testFeat.id() testGeom = testFeat.geometry() # Chks habituais if testGeom.isEmpty(): continue # obtem a geometria testPoint = testGeom.asPoint( ) if not testIsMulti else testGeom.asMultiPoint()[0].asPoint() # ignore o disjoint if not refBox.contains(testPoint): continue # agora sim, o que a gente veio fazer aqui: distance distMatrix[i + 1][j + 1] = refPoint.distance(testPoint) feedback.setProgress(int(j * total)) # debug # 4) OK, tenho tudo, faltam os pares pairMgr = MatchPairManager() # check o criteria criteria = pairMgr.CriteriaType.ISMINIMUM if method % 2 == 0 else pairMgr.CriteriaType.BOTHMIN pairMgr.buildFromMatrix(distMatrix, criteria, threshold) return pairMgr
def calculateShapeContext(self, pointLayer, searchLength, angleStep, distanceStep, normalize=True): """ Calculate the shape context for a set of points using the Shape Context method developed by Belongie et al. For details see: Belongie, S., Malik, J., and Puzicha, J., 2002. Shape Matching and Object Recognition Using Shape Contexts. IEEE Transactions on Pattern Analysis and Machine Intelligence, 24 (4), 509–522. Available at: http://ieeexplore.ieee.org/document/993558/?arnumber=993558 @param pointLayer: Input point layer. @param searchLength: Maximum search lenght to consider as a neighbourhood. 4 cm at data scale is a good value. @param angleStep: The radial size of each bin. A value of pi/6 is a good choice. pi/6 is a good value. @param distanceStep: The initial distance step for the bins. It will grow by its value plus radial size. 1 mm at data scale is a good value. @param normalize: Normalizes the histogram count to [0, 1]. Defaults to yes. @returns Histogram (bin, count) for the point set. """ # 1) initial vars - O contexto eh um histograma # 0 isMulti = QgsWkbTypes.isMultiType(int(pointLayer.wkbType())) # 1.1) Le o layer para pontos points = dict() for feat in pointLayer.getFeatures(): # Processe a geom se valida geom = feat.geometry() # Chks habituais if geom.isEmpty(): continue # obtem a geometria point = geom.asPoint() if not isMulti else geom.asMultiPoint( )[0].asPoint() #points.append( point ) points[feat.id()] = point # 1.2) Abordagem radial angleSlices = int(round(2. * math.pi / angleStep)) # fill distance distance map distStepMap = [] currDist = distanceStep while currDist < searchLength: distStepMap.append(currDist) # atlz valores currDist += currDist * angleStep distanceSlices = len(distStepMap) + 1 slices = angleSlices * distanceSlices # ultimo slice - para todos distStepMap.append(searchLength) # 2) Varre todos os pontos - jah tenho os limites, preciso acertar qm faz o q # saida eh um dict de histogramas retval = dict() for idA, ptA in points.items(): # monta o Box searchBox = QgsRectangle(ptA.x() - searchLength, ptA.y() - searchLength, ptA.x() + searchLength, ptA.y() + searchLength) neighCount = 0 histog = dict() # resultado para esse ponto # checa sua relacao com os demais #for j, ptB in enumerate( points ): for idB, ptB in points.items(): # ignora o mesmo #if i == j: if idA == idB: continue # ignore o disjoint if not searchBox.contains(ptB): continue # Ok, estah na area de busca, qual o valor do ang e distancia? distance = ptA.distance(ptB) # distance aqui eh radial - mudanca em relacao aos demais if distance > searchLength: continue angle = math.atan2(ptB.y() - ptA.y(), ptB.x() - ptA.x()) # angulo negativo, corrija if angle < 0.: angle += 2. * math.pi # 3) agora coloca no histograma # 3.1) angulo angQuad = 1. + math.floor(angle / angleStep) # 3.2) distancia - usando o map - um lower_bound resolveria... distQuad = 0 for k, dist in enumerate(distStepMap): if distance < dist: distQuad = k break # 3.3) colocando no histograma idx = int(angleSlices * distQuad + angQuad) if histog.get(idx) == None: histog[idx] = 1 else: histog[idx] += 1 neighCount += 1 # fim for points(search) # seguindo o padrao anterior, soh considero os 3 vizinhos if neighCount >= 3: retval[idA] = histog #i += 1 # fim for points(search) # 4) zera todos que nao existem - para iterar """ temp comment ---- gasta muito tempo! for histog in retval: if len( histog ) == 0: retval.remove( histog ) # preenche, se vazio for k in range (1, slices+1 ): if histog.get(k) is None: histog[k] = 0 """ # 5) normalizar if normalize: for histog in retval.values(): sum = 0 for val in histog.values(): sum += val # OK, agora divida - garantindo div0 if sum > 0: for key, val in histog.items(): histog[key] = val / sum return retval
def testContains(self): rect1 = QgsRectangle(0.0, 0.0, 5.0, 5.0) rect2 = QgsRectangle(2.0, 2.0, 7.0, 7.0) pnt1 = QgsPointXY(4.0, 4.0) pnt2 = QgsPointXY(6.0, 2.0) rect3 = rect1.intersect(rect2) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect1.contains(rect3))) assert rect1.contains(rect3), myMessage myMessage = ('Expected: %s\nGot: %s\n' % (True, rect2.contains(rect3))) assert rect2.contains(rect3), myMessage # test for point myMessage = ('Expected: %s\nGot: %s\n' % (True, rect1.contains(pnt1))) assert rect1.contains(pnt1), myMessage myMessage = ('Expected: %s\nGot: %s\n' % (True, rect2.contains(pnt1))) assert rect2.contains(pnt1), myMessage myMessage = ('Expected: %s\nGot: %s\n' % (True, rect3.contains(pnt1))) assert rect3.contains(pnt1), myMessage myMessage = ('Expected: %s\nGot: %s\n' % (False, rect1.contains(pnt2))) self.assertFalse(rect1.contains(pnt2), myMessage) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect2.contains(pnt2))) assert rect2.contains(pnt2), myMessage myMessage = ('Expected: %s\nGot: %s\n' % (False, rect3.contains(pnt2))) self.assertFalse(rect3.contains(pnt2), myMessage) myMessage = ('Expected: %s\nGot: %s\n' % (True, rect3.contains(pnt1))) self.assertTrue(rect3.contains(pnt1), myMessage)
def write_csv(self): layer = self.iface.activeLayer() if layer.type() != QgsMapLayer.VectorLayer: self.show_message( 'The active layer "{}" is NOT a vector layer containing plot data.\nPlease make a polygon layer active in the layermanager.' .format(layer.name())) return reply = QMessageBox.question( self.iface.mainWindow(), 'FieldExplorer NL', 'Save current active layer "{}" to FieldExplorer CSV?'.format( layer.name()), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.No: return layer_path = layer.dataProvider().dataSourceUri() (directory, file_name) = os.path.split(layer_path) file_name = file_name.split('|')[0] csv_name = os.path.splitext(file_name)[0] + '.csv' csv_file = os.path.join(directory, csv_name) # check if dir is writable if not os.access(directory, os.W_OK): # W_OK is for writing, R_OK for reading self.show_message( 'The data directory "{}"\nis not writable, will not be able to write a csv there.' .format(directory)) return # check if layer's crs = epsg:4326 if 'EPSG:4326' != layer.crs().authid(): self.show_message( 'The layer should have EPSG:4326 as crs, but is: "{}".\nPlease provide a layer in EPSG:4326 (lat lon coordinates).' .format(layer.crs().authid())) return # or coordinates within 2,50.0 : 8.0,55 (NL)) layer_extent = layer.extent() nl_extent = QgsRectangle(2, 50.0, 8.0, 55) if not nl_extent.contains(layer_extent): self.show_message( 'The data/layer extent:\n{}\nis not within The Netherlands.\nPlease provide data within\n{}.' .format(layer_extent.toString(), nl_extent.toString())) return features = layer.getFeatures() attributes = layer.fields().names() if not ('Plot-ID' in attributes and 'Comments' in attributes): self.show_message( 'The data should contain both an "Plot-ID" and a "Comments" attribute.\nAvailable attributes: {}' .format(attributes)) return # check for touches (could also be intersects) for feature in features: others = layer.getFeatures() for other in others: # self.log('Testing: \n{} with {}'.format(feature.attributes(), other.attributes())) if feature['Plot-ID'] == other['Plot-ID']: # self.log('IDEM: \n{} {}'.format(feature.attributes(),other.attributes())) pass elif feature.geometry().intersects(other.geometry()): self.show_message( 'These two features intersect each other: ' '\n{}\n{}. They should not share vertices and segments should not touch.' .format(feature.attributes(), other.attributes())) return features = layer.getFeatures() with open(csv_file, 'w', newline='') as f: self.log( 'Starting to write "{}" ({} in {}) to FieldExplorer NL CSV'. format(layer.name(), file_name, directory)) # QUOTE_NONNUMERIC, QUOTE_MINIMAL csv_writer = csv.writer(f, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) # NO quoting # header csv_writer.writerow( ('Plot-ID', 'A(LAT)', 'A(LONG)', 'B(LAT)', 'B(LONG)', 'C(LAT)', 'C(LONG)', 'D(LAT)', 'D(LONG)', 'Comments')) for feature in features: geom = feature.geometry() # force singletype (so both multi and single will work) if not geom.convertToSingleType(): self.show_message( 'Cannot convert feature {} to a single polygon.'. format(feature.attributes())) return # force clockwise direction! geom.forceRHR() coordinates = [] for vertex in geom.vertices(): coordinates.append(vertex) # check if just 5 if len(coordinates) > 5: self.show_message( 'The feature {}\ncontains too many vertices,\nthere should be just 4, but has: {}' .format(feature.attributes(), len(coordinates) - 1)) return if len(f'{feature["Plot-ID"]:}') > 50: self.show_message( 'The feature {}\nhas a Plot-ID of length {}\nPlease change this Plot-ID to a shorter one:\n"{}"' .format(feature.attributes(), len(feature['Plot-ID']), feature['Plot-ID'])) return # check for '\ / : * ? " < > |' forbidden_chars = ('\\', '/', ':', '*', '?', '"', '<', '>', '|') for c in forbidden_chars: if c in f'{feature["Plot-ID"]:}': self.show_message( 'The feature {}\ncontains the character "{}" which is forbidden in Plot-IDs\nPlease change this Plot-ID: "{}"' .format(feature.attributes(), c, feature['Plot-ID'])) return row = [] row.append(feature['Plot-ID']) for coord in coordinates[:-1]: row.append(coord.y()) row.append(coord.x()) if 'Comments' in attributes: row.append(feature['Comments']) csv_writer.writerow(row) # OK, done! self.iface.messageBar().pushMessage( 'Done!', 'Succesfully wrote FieldExplorer CSV file to:\n{}'.format( csv_file), level=Qgis.Info, duration=15)