/*************************************************************************** qgspostgresprovider.cpp - QGIS data provider for PostgreSQL/PostGIS layers ------------------- begin : 2004/01/07 copyright : (C) 2004 by Gary E.Sherman email : sherman at mrcc.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ /* $Id$ */ #include #include #include #include #include #include #include #include "../../src/qgis.h" #include "../../src/qgsfeature.h" #include "../../src/qgsfield.h" #include "../../src/qgsrect.h" #include "qgspostgresprovider.h" QgsPostgresProvider::QgsPostgresProvider(QString uri):dataSourceUri(uri) { // assume this is a valid layer until we determine otherwise valid = true; /* OPEN LOG FILE */ // make connection to the data source // for postgres, the connection information is passed as a space delimited // string: // host=192.168.1.5 dbname=test user=gsherman password=xxx table=tablename //--std::cout << "Data source uri is " << uri << std::endl; // strip the table name off tableName = uri.mid(uri.find("table=") + 6); QString connInfo = uri.left(uri.find("table=")); //--std::cout << "Table name is " << tableName << std::endl; //--std::cout << "Connection info is " << connInfo << std::endl; // calculate the schema if specified QString schema = ""; if (tableName.find(".") > -1) { schema = tableName.left(tableName.find(".")); } geometryColumn = tableName.mid(tableName.find(" (") + 2); geometryColumn.truncate(geometryColumn.length() - 1); tableName = tableName.mid(tableName.find(".") + 1, tableName.find(" (") - (tableName.find(".") + 1)); #ifdef QGISDEBUG std::cerr << "Geometry column is: " << geometryColumn << std::endl; std::cerr << "Schema is: " + schema << std::endl; std::cerr << "Table name is: " + tableName << std::endl; #endif //QString logFile = "./pg_provider_" + tableName + ".log"; //pLog.open((const char *)logFile); #ifdef QGISDEBUG std::cerr << "Opened log file for " << tableName << std::endl; #endif PGconn *pd = PQconnectdb((const char *) connInfo); // check the connection status if (PQstatus(pd) == CONNECTION_OK) { /* Check to see if we have GEOS support and if not, warn the user about the problems they will see :) */ #ifdef QGISDEBUG std::cerr << "Checking for GEOS support" << std::endl; #endif if(!hasGEOS(pd)){ QApplication::restoreOverrideCursor(); QMessageBox::warning(0, "No GEOS Support!", "Your PostGIS installation has no GEOS support.\nFeature selection and " "identification will not work properly.\nPlease install PostGIS with " "GEOS support (http://geos.refractions.net)"); QApplication::setOverrideCursor(Qt::waitCursor); } //--std::cout << "Connection to the database was successful\n"; // set the schema PQexec(pd,(const char *)QString("set search_path = '%1','public'").arg(schema)); // store the connection for future use connection = pd; // check the geometry column QString sql = "select f_geometry_column,type,srid from geometry_columns where f_table_name='" + tableName + "' and f_geometry_column = '" + geometryColumn + "' and f_table_schema = '" + schema + "'"; #ifdef QGISDEBUG std::cerr << "Getting geometry column: " + sql << std::endl; #endif PGresult *result = PQexec(pd, (const char *) sql); if (PQresultStatus(result) == PGRES_TUPLES_OK) { // this is a valid layer valid = true; //--std::cout << "geometry column query returned " << PQntuples(result) << std::endl; // store the srid //--std::cout << "column number of srid is " << PQfnumber(result, "srid") << std::endl; srid = PQgetvalue(result, 0, PQfnumber(result, "srid")); //--std::cout << "SRID is " << srid << std::endl; // need to store the PostgreSQL endian format used in binary cursors // since it appears that starting with // version 7.4, binary cursors return data in XDR whereas previous versions // return data in the endian of the server QString firstOid = "select oid from " + tableName + " limit 1"; PGresult * oidResult = PQexec(pd, firstOid); // get the int value from a "normal" select QString oidValue = PQgetvalue(oidResult,0,0); #ifdef QGISDEBUG std::cerr << "Creating binary cursor" << std::endl; #endif // get the same value using a binary cursor PQexec(pd,"begin work"); QString oidDeclare = QString("declare oidcursor binary cursor for select oid from %1 where oid = %2").arg(tableName).arg(oidValue); // set up the cursor PQexec(pd, (const char *)oidDeclare); QString fetch = "fetch forward 1 from oidcursor"; #ifdef QGISDEBUG std::cerr << "Fecthing a record and attempting to get check endian-ness" << std::endl; #endif PGresult *fResult = PQexec(pd, (const char *)fetch); if(PQntuples(fResult) > 0){ // get the oid value from the binary cursor int oid = *(int *)PQgetvalue(fResult,0,0); //--std::cout << "Got oid of " << oid << " from the binary cursor" << std::endl; //--std::cout << "First oid is " << oidValue << std::endl; // compare the two oid values to determine if we need to do an endian swap if(oid == oidValue.toInt()){ swapEndian = false; }else{ swapEndian = true; } // end the cursor transaction PQexec(pd, "end work"); #ifdef QGISDEBUG std::cerr << "Setting layer type" << std::endl; #endif // set the type // set the simple type for use with symbology operations QString fType = PQgetvalue(result, 0, PQfnumber(result, "type")); if (fType == "POINT" || fType == "MULTIPOINT") geomType = QGis::WKBPoint; else if (fType == "LINESTRING" || fType == "MULTILINESTRING") geomType = QGis::WKBLineString; else if (fType == "POLYGON" || fType == "MULTIPOLYGON") geomType = QGis::WKBPolygon; //--std::cout << "Feature type is " << geomType << std::endl; //--std::cout << "Feature type name is " << QGis::qgisFeatureTypes[geomType] << std::endl; // free the result PQclear(result); // get the extents sql = "select xmax(extent(" + geometryColumn + ")) as xmax," "xmin(extent(" + geometryColumn + ")) as xmin," "ymax(extent(" + geometryColumn + ")) as ymax," "ymin(extent(" + geometryColumn + ")) as ymin" " from " + tableName; #ifdef QGISDEBUG std::cerr << "Getting extents using schema.table: " + sql << std::endl; #endif result = PQexec(pd, (const char *) sql); layerExtent.setXmax(QString(PQgetvalue(result, 0, PQfnumber(result, "xmax"))).toDouble()); layerExtent.setXmin(QString(PQgetvalue(result, 0, PQfnumber(result, "xmin"))).toDouble()); layerExtent.setYmax(QString(PQgetvalue(result, 0, PQfnumber(result, "ymax"))).toDouble()); layerExtent.setYmin(QString(PQgetvalue(result, 0, PQfnumber(result, "ymin"))).toDouble()); QString xMsg; QTextOStream(&xMsg).precision(18); QTextOStream(&xMsg).width(18); QTextOStream(&xMsg) << "Set extents to: " << layerExtent. xMin() << ", " << layerExtent.yMin() << " " << layerExtent.xMax() << ", " << layerExtent.yMax(); #ifdef QGISDEBUG std::cerr << xMsg << std::endl; #endif // clear query result PQclear(result); // get total number of features sql = "select count(*) from " + tableName; result = PQexec(pd, (const char *) sql); numberFeatures = QString(PQgetvalue(result, 0, 0)).toLong(); //--std::cout << "Feature count is " << numberFeatures << std::endl; PQclear(result); // selectSQL stores the select sql statement. This has to include each attribute // plus the geometry column in binary form selectSQL = "select "; // Populate the field vector for this layer. The field vector contains // field name, type, length, and precision (if numeric) sql = "select * from " + tableName + " limit 1"; result = PQexec(pd, (const char *) sql); //--std::cout << "Field: Name, Type, Size, Modifier:" << std::endl; for (int i = 0; i < PQnfields(result); i++) { QString fieldName = PQfname(result, i); int fldtyp = PQftype(result, i); QString typOid = QString().setNum(fldtyp); int fieldModifier = PQfmod(result, i); QString sql = "select typelem from pg_type where typelem = " + typOid + " and typlen = -1"; // //--std::cout << sql << std::endl; PGresult *oidResult = PQexec(pd, (const char *) sql); // get the oid of the "real" type QString poid = PQgetvalue(oidResult, 0, PQfnumber(oidResult, "typelem")); PQclear(oidResult); sql = "select typname, typlen from pg_type where oid = " + poid; // //--std::cout << sql << std::endl; oidResult = PQexec(pd, (const char *) sql); QString fieldType = PQgetvalue(oidResult, 0, 0); QString fieldSize = PQgetvalue(oidResult, 0, 1); PQclear(oidResult); //--std::cout << "Field: " << fieldName << ", " << fieldType << " (" << fldtyp << "), " << fieldSize << ", " << // fieldModifier << std::endl; attributeFields.push_back(QgsField(fieldName, fieldType, fieldSize.toInt(), fieldModifier)); // add to the select sql statement if(i > 0){ selectSQL += ", "; } if(fieldType == "geometry"){ selectSQL += "asbinary(" + geometryColumn + ",'" + endianString() + "') as qgs_feature_geometry"; }else{ selectSQL += fieldName; } } // set the primary key getPrimaryKey(); selectSQL += " from " + tableName; //--std::cout << "selectSQL: " << (const char *)selectSQL << std::endl; PQclear(result); // get the total number of features in the layer sql = "select count(*) from " + tableName; result = PQexec(pd, (const char *) sql); numberFeatures = QString(PQgetvalue(result, 0, 0)).toLong(); #ifdef QGISDEBUG std::cerr << "Number of features: " << numberFeatures << std::endl; #endif }else{ numberFeatures = 0; valid = false; }//--std::cout << "Number of features in " << (const char *) tableName << ": " << numberFeatures << std::endl; } else { // the table is not a geometry table valid = false; #ifdef QGISDEBUG std::cerr << "Invalid Postgres layer" << std::endl; #endif } // reset tableName to include schema schemaTableName += schema + "." + tableName; ready = false; // not ready to read yet cuz the cursor hasn't been created } else { valid = false; //--std::cout << "Connection to database failed\n"; } //create a boolean vector and set every entry to false /* if (valid) { selected = new std::vector < bool > (ogrLayer->GetFeatureCount(), false); } else { selected = 0; } */ // tabledisplay=0; //draw the selected features in yellow // selectionColor.setRgb(255,255,0); } QgsPostgresProvider::~QgsPostgresProvider() { //pLog.flush(); } //TODO - we may not need this function - consider removing it from // the dataprovider.h interface /** * Get the first feature resutling from a select operation * @return QgsFeature */ //TODO - this function is a stub and always returns 0 QgsFeature *QgsPostgresProvider::getFirstFeature(bool fetchAttributes) { QgsFeature *f = 0; if (valid) { //--std::cout << "getting first feature\n"; f = new QgsFeature(); /* f->setGeometry(getGeometryPointer(feat)); if(fetchAttributes){ getFeatureAttributes(feat, f); } */ } return f; } bool QgsPostgresProvider::getNextFeature(QgsFeature &feature, bool fetchAttributes) { return true; } /** * Get the next feature resutling from a select operation * Return 0 if there are no features in the selection set * @return QgsFeature */ QgsFeature *QgsPostgresProvider::getNextFeature(bool fetchAttributes) { QgsFeature *f = 0; if (valid){ // If the query result is not valid, then select all features // before proceeding /* if(!ready) { // set up the QString declare = QString("declare qgisf binary cursor for select oid," "asbinary(%1,'%2') as qgs_feature_geometry from %3").arg(geometryColumn).arg(endianString()).arg(tableName); //--std::cout << "Selecting features using: " << declare << std::endl; // set up the cursor PQexec(connection,"begin work"); PQexec(connection, (const char *)declare); //--std::cout << "Error: " << PQerrorMessage(connection) << std::endl; ready = true; } */ QString fetch = "fetch forward 1 from qgisf"; queryResult = PQexec(connection, (const char *)fetch); ////--std::cout << "Error: " << PQerrorMessage(connection) << std::endl; //--std::cout << "Fetched " << PQntuples(queryResult) << "rows" << std::endl; if(PQntuples(queryResult) == 0){ PQexec(connection, "end work"); ready = false; return 0; } // //--std::cout <<"Raw value of the geometry field: " << PQgetvalue(queryResult,0,PQfnumber(queryResult,"qgs_feature_geometry")) << std::endl; //--std::cout << "Length of oid is " << PQgetlength(queryResult,0, PQfnumber(queryResult,"oid")) << std::endl; // get the value of the primary key based on type int oid = *(int *)PQgetvalue(queryResult,0,PQfnumber(queryResult,primaryKey)); // oid is in big endian int *noid; // if((endian() == NDR) && versionXDR){ if(swapEndian){ //--std::cout << "swapping endian for oid" << std::endl; // convert oid to opposite endian char *temp = new char[sizeof(oid)]; char *ptr = (char *)&oid + sizeof(oid) -1; int cnt = 0; while(cnt < sizeof(oid)){ temp[cnt] = *ptr--; cnt++; } noid = (int *)temp; }else{ noid = &oid; } // noid contains the oid to be used in fetching attributes if // fetchAttributes = true //--std::cout << "OID: " << *noid << std::endl; int returnedLength = PQgetlength(queryResult,0, PQfnumber(queryResult,"qgs_feature_geometry")); //--std::cout << "Returned length is " << returnedLength << std::endl; if(returnedLength > 0){ unsigned char *feature = new unsigned char[returnedLength + 1]; memset(feature, '\0', returnedLength + 1); memcpy(feature, PQgetvalue(queryResult,0,PQfnumber(queryResult,"qgs_feature_geometry")), returnedLength); int wkbType = *((int *) (feature + 1)); //--std::cout << "WKBtype is: " << wkbType << std::endl; f = new QgsFeature(*noid); f->setGeometry(feature, returnedLength + 1); if (fetchAttributes) { getFeatureAttributes(*noid, f); } }else{ //--std::cout <<"Couldn't get the feature geometry in binary form" << std::endl; } } else { //--std::cout << "Read attempt on an invalid postgresql data source\n"; } return f; } /** * Select features based on a bounding rectangle. Features can be retrieved * with calls to getFirstFeature and getNextFeature. * @param mbr QgsRect containing the extent to use in selecting features */ void QgsPostgresProvider::select(QgsRect * rect, bool useIntersect) { // spatial query to select features #ifdef QGISDEBUG std::cerr << "Selection rectangle is " << *rect << std::endl; std::cerr << "Selection polygon is " << rect->asPolygon() << std::endl; #endif QString declare = QString("declare qgisf binary cursor for select " + primaryKey + ",asbinary(%1,'%2') as qgs_feature_geometry from %3").arg(geometryColumn).arg(endianString()).arg(tableName); if(useIntersect){ declare += " where intersects(" + geometryColumn; declare += ", GeometryFromText('BOX3D(" + rect->stringRep(); declare += ")'::box3d,"; declare += srid; declare += "))"; }else{ declare += " where " + geometryColumn; declare += " && GeometryFromText('BOX3D(" + rect->stringRep(); declare += ")'::box3d,"; declare += srid; declare += ")"; } #ifdef QGISDEBUG std::cout << "Selecting features using: " << declare << std::endl; #endif // set up the cursor if(ready){ PQexec(connection, "end work"); } PQexec(connection,"begin work"); PQexec(connection, (const char *)declare); } /** * Set the data source specification. This may be a path or database * connection string * @uri data source specification */ void QgsPostgresProvider::setDataSourceUri(QString uri) { dataSourceUri = uri; } /** * Get the data source specification. This may be a path or database * connection string * @return data source specification */ QString QgsPostgresProvider::getDataSourceUri() { return dataSourceUri; } /** * Identify features within the search radius specified by rect * @param rect Bounding rectangle of search radius * @return std::vector containing QgsFeature objects that intersect rect */ std::vector& QgsPostgresProvider::identify(QgsRect * rect) { features.clear(); // select the features select(rect); return features; } /* unsigned char * QgsPostgresProvider::getGeometryPointer(OGRFeature *fet){ // OGRGeometry *geom = fet->GetGeometryRef(); unsigned char *gPtr=0; // get the wkb representation gPtr = new unsigned char[geom->WkbSize()]; geom->exportToWkb((OGRwkbByteOrder) endian(), gPtr); return gPtr; } */ int QgsPostgresProvider::endian() { char *chkEndian = new char[4]; memset(chkEndian, '\0', 4); chkEndian[0] = 0xE8; int *ce = (int *) chkEndian; int retVal; if (232 == *ce) retVal = NDR; else retVal = XDR; delete[]chkEndian; return retVal; } // TODO - make this function return the real extent_ QgsRect *QgsPostgresProvider::extent() { return &layerExtent; //extent_->MinX, extent_->MinY, extent_->MaxX, extent_->MaxY); } /** * Return the feature type */ int QgsPostgresProvider::geometryType() { return geomType; } /** * Return the feature type */ long QgsPostgresProvider::featureCount() { return numberFeatures; } /** * Return the number of fields */ int QgsPostgresProvider::fieldCount() { return attributeFields.size(); } /** * Fetch attributes for a selected feature */ void QgsPostgresProvider::getFeatureAttributes(int key, QgsFeature *f){ QString sql = QString("select * from %1 where %2 = %3").arg(tableName).arg(primaryKey).arg(key); PGresult *attr = PQexec(connection, (const char *)sql); for (int i = 0; i < fieldCount(); i++) { QString fld = PQfname(attr, i); // Dont add the WKT representation of the geometry column to the identify // results if(fld != geometryColumn){ // Add the attribute to the feature QString val = PQgetvalue(attr,0, i); f->addAttribute(fld, val); } } } std::vector& QgsPostgresProvider::fields() { return attributeFields; } void QgsPostgresProvider::reset() { // reset the cursor to the first record //--std::cout << "Resetting the cursor to the first record " << std::endl; QString declare = QString("declare qgisf binary cursor for select " + primaryKey + ",asbinary(%1,'%2') as qgs_feature_geometry from %3").arg(geometryColumn).arg(endianString()).arg(tableName); //--std::cout << "Selecting features using: " << declare << std::endl; #ifdef QGISDEBUG std::cerr << "Setting up binary cursor: " << declare << std::endl; #endif // set up the cursor PQexec(connection,"end work"); PQexec(connection,"begin work"); PQexec(connection, (const char *)declare); //--std::cout << "Error: " << PQerrorMessage(connection) << std::endl; ready = true; } /* QString QgsPostgresProvider::getFieldTypeName(PGconn * pd, int oid) { QString typOid = QString().setNum(oid); QString sql = "select typelem from pg_type where typelem = " + typOid + " and typlen = -1"; ////--std::cout << sql << std::endl; PGresult *result = PQexec(pd, (const char *) sql); // get the oid of the "real" type QString poid = PQgetvalue(result, 0, PQfnumber(result, "typelem")); PQclear(result); sql = "select typname, typlen from pg_type where oid = " + poid; // //--std::cout << sql << std::endl; result = PQexec(pd, (const char *) sql); QString typeName = PQgetvalue(result, 0, 0); QString typeLen = PQgetvalue(result, 0, 1); PQclear(result); typeName += "(" + typeLen + ")"; return typeName; } */ QString QgsPostgresProvider::endianString() { char *chkEndian = new char[4]; memset(chkEndian, '\0', 4); chkEndian[0] = 0xE8; int *ce = (int *) chkEndian; if (232 == *ce) return QString("NDR"); else return QString("XDR"); } QString QgsPostgresProvider::getPrimaryKey(){ QString sql = "select oid from pg_class where relname = '" + tableName + "'"; #ifdef QGISDEBUG std::cerr << "Getting primary key" << std::endl; std::cerr << sql << std::endl; #endif PGresult *pk = PQexec(connection,(const char *)sql); #ifdef QGISDEBUG std::cerr << "Got " << PQntuples(pk) << " rows " << std::endl; #endif // get the oid for the table QString oid = PQgetvalue(pk,0,0); // check to see if there is a primary key sql = "select indkey from pg_index where indrelid = " + oid + " and indisprimary = 't'"; #ifdef QGISDEBUG std::cerr << sql << std::endl; #endif PQclear(pk); pk = PQexec(connection,(const char *)sql); // if we got no tuples we ain't go no pk :) if(PQntuples(pk) == 0){ // no key - should we warn the user that performance will suffer #ifdef QGISDEBUG std::cerr << "Table has no primary key -- using oid to fetch records" << std::endl; #endif primaryKey = "oid"; }else{ // store the key column QString keyString = PQgetvalue(pk,0,0); QStringList columns = QStringList::split(" ", keyString); if(columns.count() > 1){ //TODO concatenated key -- can we use this? #ifdef QGISDEBUG std::cerr << "Table has a concatenated primary key" << std::endl; #endif } primaryKeyIndex = columns[0].toInt()-1; QgsField fld = attributeFields[primaryKeyIndex]; primaryKey = fld.name(); #ifdef QGISDEBUG std::cerr << "Primary key is " << primaryKey << std::endl;//);// +<< " at column " << primaryKeyIndex << std::endl; #endif // pLog.flush(); } PQclear(pk); return primaryKey; /* Process to determine the fields used in a primary key: test=# select oid from pg_class where relname = 'earthquakes'; oid ------- 24865 (1 row) test=# select indkey from pg_index where indrelid = 24865 and indisprimary = 't'; indkey -------- 1 5 (1 row) Primary key is composed of fields 1 and 5 */ } // Returns the minimum value of an attribute QString QgsPostgresProvider::minValue(int position){ // get the field name QgsField fld = attributeFields[position]; QString sql = QString("select min(%1) from %2").arg(fld.name()).arg(tableName); PGresult *rmin = PQexec(connection,(const char *)sql); QString minValue = PQgetvalue(rmin,0,0); PQclear(rmin); return minValue; } // Returns the maximum value of an attribute QString QgsPostgresProvider::maxValue(int position){ // get the field name QgsField fld = attributeFields[position]; QString sql = QString("select max(%1) from %2").arg(fld.name()).arg(tableName); PGresult *rmax = PQexec(connection,(const char *)sql); QString maxValue = PQgetvalue(rmax,0,0); PQclear(rmax); return maxValue; } bool QgsPostgresProvider::isValid(){ return valid; } /** * Check to see if GEOS is available */ bool QgsPostgresProvider::hasGEOS(PGconn *connection){ // make sure info is up to date for the current connection postgisVersion(connection); // get geos capability return geosAvailable; } /* Functions for determining available features in postGIS */ QString QgsPostgresProvider::postgisVersion(PGconn *connection){ PGresult *result = PQexec(connection, "select postgis_version()"); postgisVersionInfo = PQgetvalue(result,0,0); #ifdef QGISDEBUG std::cerr << "PostGIS version info: " << postgisVersionInfo << std::endl; #endif // assume no capabilities geosAvailable = false; gistAvailable = false; projAvailable = false; // parse out the capabilities and store them QStringList postgisParts = QStringList::split(" ", postgisVersionInfo); QStringList geos = postgisParts.grep("GEOS"); if(geos.size() == 1){ geosAvailable = (geos[0].find("=1") > -1); } QStringList gist = postgisParts.grep("STATS"); if(gist.size() == 1){ gistAvailable = (geos[0].find("=1") > -1); } QStringList proj = postgisParts.grep("PROJ"); if(proj.size() == 1){ projAvailable = (proj[0].find("=1") > -1); } return postgisVersionInfo; } /** * Class factory to return a pointer to a newly created * QgsPostgresProvider object */ extern "C" QgsPostgresProvider * classFactory(const char *uri) { return new QgsPostgresProvider(uri); } /** Required key function (used to map the plugin to a data store type) */ extern "C" QString providerKey(){ return QString("postgres"); } /** * Required description function */ extern "C" QString description(){ return QString("PostgreSQL/PostGIS data provider"); } /** * Required isProvider function. Used to determine if this shared library * is a data provider plugin */ extern "C" bool isProvider(){ return true; }