diff --git a/plugins/input/sqlserver/build.py b/plugins/input/sqlserver/build.py new file mode 100644 index 000000000..b72303a53 --- /dev/null +++ b/plugins/input/sqlserver/build.py @@ -0,0 +1,63 @@ +# +# This file is part of Mapnik (c++ mapping toolkit) +# +# Copyright (C) 2013 Artem Pavlenko +# +# Mapnik is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +# + +Import ('plugin_base') +Import ('env') + +PLUGIN_NAME = 'sqlserver' + +plugin_env = plugin_base.Clone() + +plugin_sources = Split( + """ + %(PLUGIN_NAME)s_datasource.cpp + %(PLUGIN_NAME)s_featureset.cpp + %(PLUGIN_NAME)s_geometry_parser.cpp + """ % locals() +) + +libraries = [ 'odbc' ] +libraries.append('boost_system%s' % env['BOOST_APPEND']) +libraries.append(env['ICU_LIB_NAME']) + +if env['PLUGIN_LINKING'] == 'shared': + libraries.append('mapnik') + + TARGET = plugin_env.SharedLibrary('../%s' % PLUGIN_NAME, + SHLIBPREFIX='', + SHLIBSUFFIX='.input', + source=plugin_sources, + LIBS=libraries, + LINKFLAGS=env['CUSTOM_LDFLAGS']) + + # if the plugin links to libmapnik ensure it is built first + Depends(TARGET, env.subst('../../../src/%s' % env['MAPNIK_LIB_NAME'])) + + if 'uninstall' not in COMMAND_LINE_TARGETS: + env.Install(env['MAPNIK_INPUT_PLUGINS_DEST'], TARGET) + env.Alias('install', env['MAPNIK_INPUT_PLUGINS_DEST']) + +plugin_obj = { + 'LIBS': libraries, + 'SOURCES': plugin_sources, +} + +Return('plugin_obj') diff --git a/plugins/input/sqlserver/sqlserver_datasource.cpp b/plugins/input/sqlserver/sqlserver_datasource.cpp new file mode 100644 index 000000000..fdd3a33b9 --- /dev/null +++ b/plugins/input/sqlserver/sqlserver_datasource.cpp @@ -0,0 +1,464 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 we-do-IT + * + *****************************************************************************/ + +#include "sqlserver_datasource.hpp" +#include "sqlserver_featureset.hpp" +#include "sqlserver_geometry_parser.hpp" + +// mapnik +#include +#include +#include +#include +#include +#include + +// boost +#include +#include +#include + +// stl +#include +#include +#include +#include +#include + +// sql server (odbc) +#include +#include + +using mapnik::datasource; +using mapnik::parameters; +using mapnik::query; +using mapnik::featureset_ptr; +using mapnik::layer_descriptor; +using mapnik::attribute_descriptor; +using mapnik::datasource_exception; +using mapnik::box2d; +using mapnik::coord2d; + +DATASOURCE_PLUGIN(sqlserver_datasource) + +// an exception class to wrap gathering the odbc error +sqlserver_datasource_exception::sqlserver_datasource_exception(std::string const& message) + : mapnik::datasource_exception("SQL Server Plugin: "+message) { +} + +sqlserver_datasource_exception::sqlserver_datasource_exception(std::string const& message, SQLSMALLINT HandleType, SQLHANDLE Handle) + : mapnik::datasource_exception("SQL Server Plugin: "+message+": "+sql_diagnostics(HandleType, Handle)) { +} + +sqlserver_datasource_exception::~sqlserver_datasource_exception() throw () { +} + +std::string sqlserver_datasource_exception::sql_diagnostics(SQLSMALLINT HandleType, SQLHANDLE Handle) { + // Get the status records. + std::ostringstream s; + SQLCHAR SqlState[6]; + SQLINTEGER NativeError; + SQLCHAR Msg[SQL_MAX_MESSAGE_LENGTH]; + SQLSMALLINT MsgLen; + SQLSMALLINT i = 1; + while (SQLGetDiagRec(HandleType, Handle, i, SqlState, &NativeError, Msg, sizeof(Msg), &MsgLen) == SQL_SUCCESS) { + s << "[" << SqlState << "] "; + s << Msg; + s << " (" << NativeError << ") "; + i++; + } + return s.str(); +} + +// datasource +sqlserver_datasource::sqlserver_datasource(parameters const& params) : + datasource (params), + type_(datasource::Vector), + fields_(*params.get("fields", "*")), + geometry_field_(*params.get("geometry_field", "")), + is_geometry_(true), + extent_initialized_(false), + desc_(*params.get("type"), *params.get("encoding", "utf-8")), + henv_(0), + hdbc_(0) +{ +#ifdef MAPNIK_STATS + mapnik::progress_timer __stats__(std::clog, "sqlserver_datasource::init"); +#endif + + // they must supply a table/view name or a subquery + if (params.get("table")) { + table_ = *params.get("table"); + } else { + throw sqlserver_datasource_exception("no parameter specified"); + } + + // they may supply an extent (and srid) to prevent querying for it + boost::optional ext = params.get("extent"); + if (ext) { + extent_initialized_ = extent_.from_string(*ext); + } + + boost::optional srid = params.get("srid"); + if (srid) { + srid_ = *srid; + } + + if (ext && !srid) { + throw sqlserver_datasource_exception("must specify parameter if parameter specified"); + } + + // the driver refers to an entry in odbcinst.ini http://www.unixodbc.org/odbcinst.html + std::string driver("ODBC Driver 11 for SQL Server"); // default for ms driver + if (params.get("driver")) { + driver = *params.get("driver"); + } + + // build the connection string + std::ostringstream s; + s << "Driver={" << driver << "};"; + if (params.get("server")) { + s << "Server=" << *params.get("server") << ";"; + } + if (params.get("database")) { + s << "Database=" << *params.get("database") << ";"; + } + if (params.get("user")) { + s << "Uid=" << *params.get("user") << ";"; + } + if (params.get("password")) { + s << "Pwd=" << *params.get("password") << ";"; + } + if (params.get("trusted")) { + s << "Trusted_Connection=" << *params.get("trusted") << ";"; + } + std::string InConnectionString = s.str(); + MAPNIK_LOG_DEBUG(sqlserver) << "sqlserver_datasource: connection string: " << InConnectionString; + + // everything returns one of these + SQLRETURN retcode; + + // allocate environment handle + retcode = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv_); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not allocate environment handle"); // no diagnostics available + } + + // set the ODBC version environment attribute + retcode = SQLSetEnvAttr(henv_, SQL_ATTR_ODBC_VERSION, (SQLPOINTER*)SQL_OV_ODBC3, 0); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not set ODBC version environment value", SQL_HANDLE_ENV, henv_); + } + + // allocate connection handle + retcode = SQLAllocHandle(SQL_HANDLE_DBC, henv_, &hdbc_); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not allocate connection handle", SQL_HANDLE_ENV, henv_); + } + + // set login timeout to 5 seconds + retcode = SQLSetConnectAttr(hdbc_, SQL_LOGIN_TIMEOUT, (SQLPOINTER)5, 0); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not set connection timeout", SQL_HANDLE_DBC, hdbc_); + } + + // connect to data source + SQLCHAR OutConnectionString[1024]; + SQLSMALLINT OutConnectionStringLength; + retcode = SQLDriverConnect(hdbc_, + NULL, + (SQLCHAR*)InConnectionString.c_str(), + InConnectionString.length(), + OutConnectionString, + 1024, + &OutConnectionStringLength, + SQL_DRIVER_NOPROMPT); + + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not connect", SQL_HANDLE_DBC, hdbc_); + } + MAPNIK_LOG_DEBUG(sqlserver) << "sqlserver_datasource: connected: " << OutConnectionString; + + // get columns description +#ifdef MAPNIK_STATS + mapnik::progress_timer __stats__(std::clog, "sqlserver_datasource::get_column_description"); +#endif + + // table parameter can be a table/view name or a subquery + std::ostringstream stmt; + if (table_.find_first_of(" \t") == std::string::npos) { + // no whitespace in table_; assume a table/view name + stmt << "SELECT TOP(1) " << fields_ << " FROM " << table_; + } else { + // whitespace in table_; assume a valid query + stmt << table_; + } + MAPNIK_LOG_DEBUG(sqlserver) << "sqlserver_datasource: " << stmt.str(); + + // allocate statement handle + SQLHANDLE hstmt=0; + retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc_, &hstmt); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not allocate statement", SQL_HANDLE_DBC, hdbc_); + } + + // prepare statement + retcode = SQLPrepare(hstmt, (SQLCHAR*)stmt.str().c_str(), SQL_NTS); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not prepare statement", SQL_HANDLE_STMT, hstmt); + } + + // find out how many columns in result set + SQLSMALLINT n=0; + retcode = SQLNumResultCols(hstmt, &n); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not get number of result columns", SQL_HANDLE_STMT, hstmt); + } + + // get name,type for each column + for (int i=1; i<=n; i++) { + SQLCHAR ColumnName[255]; // max is currently 128 in sql server + SQLSMALLINT NameLength; + SQLSMALLINT DataType; + SQLULEN ColumnSize; + SQLSMALLINT DecimalDigits; + SQLSMALLINT Nullable; + retcode = SQLDescribeCol(hstmt, i, ColumnName, sizeof(ColumnName), &NameLength, &DataType, &ColumnSize, &DecimalDigits, &Nullable); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not describe column", SQL_HANDLE_STMT, hstmt); + } + + SQLCHAR TypeName[255]; + SQLSMALLINT ReturnedLength; + char geometry[] = {'g','\0','e','\0','o','\0','m','\0','e','\0','t','\0','r','\0','y','\0','\0','\0'}; // geometry + char geography[] = {'g','\0','e','\0','o','\0','g','\0','r','\0','a','\0','p','\0','h','\0','y','\0','\0','\0'}; // geography + switch (DataType) { + case SQL_CHAR: + case SQL_VARCHAR: + case -9: // NVARCHAR + desc_.add_descriptor(attribute_descriptor((char *)ColumnName,mapnik::sqlserver::String)); + //MAPNIK_LOG_DEBUG(sqlserver) << "found string column: " << (char*)ColumnName; + break; + + case SQL_INTEGER: + case SQL_SMALLINT: + desc_.add_descriptor(attribute_descriptor((char *)ColumnName,mapnik::sqlserver::Integer)); + //MAPNIK_LOG_DEBUG(sqlserver) << "found integer column: " << (char*)ColumnName; + break; + + case SQL_NUMERIC: + case SQL_DECIMAL: + case SQL_FLOAT: + case SQL_REAL: + case SQL_DOUBLE: + desc_.add_descriptor(attribute_descriptor((char *)ColumnName,mapnik::sqlserver::Double)); + //MAPNIK_LOG_DEBUG(sqlserver) << "found double column: " << (char*)ColumnName; + break; + + case SQL_DATETIME: + case SQL_TYPE_DATE: + case SQL_TYPE_TIME: + case SQL_TYPE_TIMESTAMP: + desc_.add_descriptor(attribute_descriptor((char *)ColumnName,mapnik::sqlserver::String)); + //MAPNIK_LOG_DEBUG(sqlserver) << "found string column: " << (char*)ColumnName; + break; + + case SQL_SS_UDT: + // check if it is a geometry type + retcode = SQLColAttribute(hstmt, i, SQL_CA_SS_UDT_TYPE_NAME, &TypeName, sizeof(TypeName), &ReturnedLength, NULL); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not get column attribute", SQL_HANDLE_STMT, hstmt); + } + // on linux the type name is returned as a weird string with null bytes as every second character + if (strcmp((char *)TypeName, "geometry") == 0 || std::memcmp((char*)TypeName, geometry, ReturnedLength) == 0) { + //MAPNIK_LOG_DEBUG(sqlserver) << "found geometry column: " << (char*)ColumnName; + geometry_field_ = (const char *)ColumnName; + is_geometry_ = true; + desc_.add_descriptor(attribute_descriptor((char *)ColumnName,mapnik::sqlserver::Geometry)); + } + if (strcmp((char *)TypeName, "geography") == 0 || std::memcmp((char*)TypeName, geography, ReturnedLength) == 0) { + //MAPNIK_LOG_DEBUG(sqlserver) << "found geography column: " << (char*)ColumnName; + geometry_field_ = (const char *)ColumnName; + is_geometry_ = false; + desc_.add_descriptor(attribute_descriptor((char *)ColumnName,mapnik::sqlserver::Geography)); + } + break; + + default: + MAPNIK_LOG_WARN(sqlserver) << "sqlserver_datasource: unknown/unsupported datatype in column: " << ColumnName << " (" << DataType << ")"; + desc_.add_descriptor(attribute_descriptor((char *)ColumnName,mapnik::sqlserver::Unknown)); + break; + } + } + + // free handle + retcode = SQLFreeStmt(hstmt, SQL_CLOSE); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not free statement handle", SQL_HANDLE_STMT, hstmt); + } + + // final check + if (geometry_field_ == "") { + MAPNIK_LOG_WARN(sqlserver) << "sqlserver_datasource: no geometry column found or specified"; + } + +} + +sqlserver_datasource::~sqlserver_datasource() +{ + if (hdbc_) { + (void)SQLDisconnect(hdbc_); + (void)SQLFreeHandle(SQL_HANDLE_DBC, hdbc_); + hdbc_ = 0; + } + if (henv_) { + (void)SQLFreeHandle(SQL_HANDLE_ENV, henv_); + henv_ = 0; + } +} + +const char * sqlserver_datasource::name() +{ + return "sqlserver"; +} + +mapnik::datasource::datasource_t sqlserver_datasource::type() const +{ + return type_; +} + +box2d sqlserver_datasource::envelope() const +{ + if (extent_initialized_) return extent_; + + SQLRETURN retcode; + + // allocate statement handle + SQLHANDLE hstmt=0; + retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc_, &hstmt); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not allocate statement", SQL_HANDLE_DBC, hdbc_); + } + + // table parameter can be a table/view or a subquery + // iff a subquery, need to wrap in () + std::ostringstream stmt; + stmt << "SELECT geometry::EnvelopeAggregate(" << geometry_field_ << ") FROM "; + if (table_.find_first_of(" \t") == std::string::npos) { + // no whitespace in table; assume a table/view name + stmt << table_; + } else { + // whitespace in table; assume a subquery + stmt << "(" << table_ << ") T"; + } + MAPNIK_LOG_DEBUG(sqlserver) << "sqlserver_datasource: " << stmt.str(); + + // execute statement + retcode = SQLExecDirect(hstmt, (SQLCHAR*)stmt.str().c_str(), SQL_NTS); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not execute statement", SQL_HANDLE_STMT, hstmt); + } + + // fetch first result (will only be one row) + retcode = SQLFetch(hstmt); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not fetch result", SQL_HANDLE_STMT, hstmt); + } + + // get the row data + SQLUSMALLINT ColumnNum = 1; + SQLCHAR BinaryPtr[1024]; // envelope is a 5 point polygon; usually only 112 bytes + SQLLEN BinaryLenOrInd; + retcode = SQLGetData(hstmt, ColumnNum, SQL_C_BINARY, BinaryPtr, sizeof(BinaryPtr), &BinaryLenOrInd); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not get data", SQL_HANDLE_STMT, hstmt); + } + //MAPNIK_LOG_DEBUG(sqlserver) << "sqlserver_datasource: envelope returned " << BinaryLenOrInd << " bytes"; + + sqlserver_geometry_parser geometry_parser(is_geometry_ ? Geometry : Geography); + mapnik::geometry::geometry geom = geometry_parser.parse(BinaryPtr, BinaryLenOrInd); + extent_ = mapnik::geometry::envelope(geom); + extent_initialized_ = true; + srid_ = geometry_parser.get_srs_id(); // get the srid of the extents; assume that is same for whole table + + // free handle + retcode = SQLFreeStmt(hstmt, SQL_CLOSE); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not free statement handle", SQL_HANDLE_STMT, hstmt); + } + + return extent_; +} + +boost::optional sqlserver_datasource::get_geometry_type() const +{ + return boost::optional(); +} + +layer_descriptor sqlserver_datasource::get_descriptor() const +{ + return desc_; +} + +featureset_ptr sqlserver_datasource::features(query const& q) const +{ +#ifdef MAPNIK_STATS + mapnik::progress_timer __stats__(std::clog, "sqlserver_datasource::features"); +#endif + box2d const& box = q.get_bbox(); + return features_in_box(box); +} + +featureset_ptr sqlserver_datasource::features_at_point(coord2d const& pt, double tol) const +{ +#ifdef MAPNIK_STATS + mapnik::progress_timer __stats__(std::clog, "sqlserver_datasource::features_at_point"); +#endif + box2d box(pt.x - tol, pt.y - tol, pt.x + tol, pt.y + tol); + return features_in_box(box); +} + +featureset_ptr sqlserver_datasource::features_in_box(box2d const& box) const { + std::string sql; // = table_; //populate_tokens(table_, scale_denom, box, px_gw, px_gh); + if (table_.find_first_of(" \t") == std::string::npos) { + // no whitespace in table_; assume a table/view name + sql = "SELECT * from " + table_; + } else { + // whitespace in table_; assume a subquery + sql = table_ ; + } + + std::ostringstream spatial_sql; + spatial_sql << " WHERE " << geometry_field_; + spatial_sql << ".STIntersects(" ; + spatial_sql << std::setprecision(16); + spatial_sql << "geometry::STPolyFromText('POLYGON(("; + spatial_sql << box.minx() << " " << box.miny() << ", "; + spatial_sql << box.minx() << " " << box.maxy() << ", "; + spatial_sql << box.maxx() << " " << box.maxy() << ", "; + spatial_sql << box.maxx() << " " << box.miny() << ", "; + spatial_sql << box.minx() << " " << box.miny() << "))'," << srid_ <<")"; + spatial_sql << ") = 1"; + + if (boost::algorithm::ifind_first(sql, "WHERE")) { + // change existing where clause; prefix it with the spatial predicate + boost::algorithm::ireplace_first(sql, "WHERE", spatial_sql.str() + " AND "); + } else if (boost::algorithm::ifind_first(sql, table_)) { + // no where clause, so add the spatial predicate as the where clause + boost::algorithm::ireplace_first(sql, table_, table_ + " " + spatial_sql.str()); + } else { + MAPNIK_LOG_WARN(sqlserver) << "sqlserver_datasource: cannot determine where to add the spatial filter clause"; + } + + MAPNIK_LOG_DEBUG(sqlserver) << "sqlserver_datasource: " << sql; + + return std::make_shared(hdbc_, + sql, + desc_); +} + diff --git a/plugins/input/sqlserver/sqlserver_datasource.hpp b/plugins/input/sqlserver/sqlserver_datasource.hpp new file mode 100644 index 000000000..c76e97199 --- /dev/null +++ b/plugins/input/sqlserver/sqlserver_datasource.hpp @@ -0,0 +1,102 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 we-do-IT + * + *****************************************************************************/ + +#ifndef SQLSERVER_DATASOURCE_HPP +#define SQLSERVER_DATASOURCE_HPP + +// mapnik +#include +#include +#include +#include +#include +#include +#include +#include + +// boost +#include +#include + +// stl +#include +#include + +// sql server (via odbc) +#ifdef _WINDOWS +#include +#endif +#include "sql.h" + +// extended from mapnik/attribute_descriptor.hpp (added 8,9) +namespace mapnik { + namespace sqlserver { + enum eAttributeTypeEx { + Integer=1, + Float=2, + Double=3, + String=4, + Boolean=5, + Geometry=6, + Object=7, + Geography=8, + Unknown=9 + }; + } +} + +// SQL Server datasource for Mapnik +class sqlserver_datasource : public mapnik::datasource +{ +public: + sqlserver_datasource(mapnik::parameters const& params); + virtual ~sqlserver_datasource (); + static const char * name(); + mapnik::datasource::datasource_t type() const; + mapnik::featureset_ptr features(mapnik::query const& q) const; + mapnik::featureset_ptr features_at_point(mapnik::coord2d const& pt, double tol = 0) const; + mapnik::box2d envelope() const; + boost::optional get_geometry_type() const; + mapnik::layer_descriptor get_descriptor() const; + +private: + mapnik::datasource::datasource_t type_; + + std::string table_; + std::string fields_; + std::string geometry_field_; + bool is_geometry_; // true=geometry, false=geography + + mutable bool extent_initialized_; + mutable mapnik::box2d extent_; + mutable int srid_; + + mapnik::layer_descriptor desc_; + + SQLHENV henv_; + SQLHDBC hdbc_; + + mapnik::featureset_ptr features_in_box(mapnik::box2d const& box) const; +}; + +// if anything goes wrong in the sqlserver_datasource class, one of these +// exceptions will be thrown. usually the message will include details from +// the SQL Server error +class sqlserver_datasource_exception : public mapnik::datasource_exception +{ +public: + sqlserver_datasource_exception(std::string const& message); + sqlserver_datasource_exception(std::string const& message, SQLSMALLINT HandleType, SQLHANDLE Handle); + + virtual ~sqlserver_datasource_exception() throw(); + +private: + static std::string sql_diagnostics(SQLSMALLINT HandleType, SQLHANDLE Handle); +}; + +#endif // SQLSERVER_DATASOURCE_HPP diff --git a/plugins/input/sqlserver/sqlserver_featureset.cpp b/plugins/input/sqlserver/sqlserver_featureset.cpp new file mode 100644 index 000000000..7295e3154 --- /dev/null +++ b/plugins/input/sqlserver/sqlserver_featureset.cpp @@ -0,0 +1,179 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 we-do-IT + * + *****************************************************************************/ + +#include "sqlserver_featureset.hpp" +#include "sqlserver_datasource.hpp" +#include "sqlserver_geometry_parser.hpp" + +// mapnik +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// sql server (odbc) +#include +#include + +using mapnik::query; +using mapnik::box2d; +using mapnik::feature_ptr; +using mapnik::geometry_utils; +using mapnik::transcoder; +using mapnik::feature_factory; +using mapnik::attribute_descriptor; + +sqlserver_featureset::sqlserver_featureset(SQLHDBC hdbc, + std::string const& sqlstring, + mapnik::layer_descriptor const& desc + ) + : hstmt_(0), + desc_(desc), + tr_(new transcoder(desc.get_encoding())), + feature_id_(1) +{ + SQLRETURN retcode; + + // allocate statement handle + retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt_); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not allocate statement", SQL_HANDLE_DBC, hdbc); + } + + // execute statement + retcode = SQLExecDirect(hstmt_, (SQLCHAR*)sqlstring.c_str(), SQL_NTS); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not execute statement", SQL_HANDLE_STMT, hstmt_); + } + + std::vector::const_iterator itr = desc_.get_descriptors().begin(); + std::vector::const_iterator end = desc_.get_descriptors().end(); + ctx_ = std::make_shared(); + while (itr != end) { + ctx_->push(itr->get_name()); + ++itr; + } + + +} + +sqlserver_featureset::~sqlserver_featureset() { + if (hstmt_) { + (void)SQLFreeStmt(hstmt_, SQL_CLOSE); + hstmt_ = 0; + } +} + +feature_ptr sqlserver_featureset::next() +{ + SQLRETURN retcode; + + // fetch next result + retcode = SQLFetch(hstmt_); + if (retcode == SQL_NO_DATA) { + // normal end of recordset + return feature_ptr(); + } + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not fetch result", SQL_HANDLE_STMT, hstmt_); + } + + // create an empty feature with the next id + feature_ptr feature(feature_factory::create(ctx_, feature_id_)); + + // populate feature geometry and attributes from this row + std::vector::const_iterator itr = desc_.get_descriptors().begin(); + std::vector::const_iterator end = desc_.get_descriptors().end(); + SQLUSMALLINT ColumnNum=1; + while (itr != end) { + SQLCHAR sval[2048]; + long ival; + double dval; + SQLCHAR *BinaryPtr = NULL; // Allocate dynamically + SQLLEN BinaryLenOrInd; + SQLLEN LenOrInd; + switch (itr->get_type()) { + case mapnik::sqlserver::String: + retcode = SQLGetData(hstmt_, ColumnNum, SQL_C_CHAR, sval, sizeof(sval), &LenOrInd); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not get string data", SQL_HANDLE_STMT, hstmt_); + } + feature->put(itr->get_name(), (UnicodeString)tr_->transcode((char*)sval)); + break; + + case mapnik::sqlserver::Integer: + retcode = SQLGetData(hstmt_, ColumnNum, SQL_C_SLONG, &ival, sizeof(ival), &LenOrInd); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not get int data", SQL_HANDLE_STMT, hstmt_); + } + feature->put(itr->get_name(), static_cast(ival)); + break; + + case mapnik::sqlserver::Double: + retcode = SQLGetData(hstmt_, ColumnNum, SQL_C_DOUBLE, &dval, sizeof(dval), &LenOrInd); + if (!SQL_SUCCEEDED(retcode)) { + throw sqlserver_datasource_exception("could not get double data", SQL_HANDLE_STMT, hstmt_); + } + feature->put(itr->get_name(), dval); + break; + + case mapnik::sqlserver::Geometry: + case mapnik::sqlserver::Geography: { + // Call SQLGetData with a zero buffer size to determine the amount of data that's waiting. + SQLCHAR DummyBinaryPtr[10]; // cannot pass a NULL pointer, even though we pass length of zero + retcode = SQLGetData(hstmt_, ColumnNum, SQL_C_BINARY, DummyBinaryPtr, 0, &BinaryLenOrInd); + if (retcode != SQL_SUCCESS_WITH_INFO) { + throw sqlserver_datasource_exception("could not get geometry data - failed to get buffer length", SQL_HANDLE_STMT, hstmt_); + } + + // allocate a suitably sized buffer + BinaryPtr = new SQLCHAR[BinaryLenOrInd]; + + // get the geometry data + retcode = SQLGetData(hstmt_, ColumnNum, SQL_C_BINARY, BinaryPtr, BinaryLenOrInd, &BinaryLenOrInd); + if (!SQL_SUCCEEDED(retcode)) { + delete[] BinaryPtr; + BinaryPtr = NULL; + throw sqlserver_datasource_exception("could not get geometry data into buffer", SQL_HANDLE_STMT, hstmt_); + } + + // attempt to parse + try { + sqlserver_geometry_parser geometry_parser((itr->get_type() == mapnik::sqlserver::Geometry ? Geometry : Geography)); + mapnik::geometry::geometry geom = geometry_parser.parse(BinaryPtr, BinaryLenOrInd); + feature->set_geometry(std::move(geom)); + } catch (mapnik::datasource_exception e) { + // Cleanup and rethrow the caught exception + delete[] BinaryPtr; + BinaryPtr = NULL; + throw; + } + + // normal cleanup + delete[] BinaryPtr; + BinaryPtr = NULL; + break; + } + + default: + MAPNIK_LOG_WARN(sqlserver) << "sqlserver_datasource: unknown/unsupported datatype in column: " << itr->get_name() << " (" << itr->get_type() << ")"; + break; + } + ++ColumnNum; + ++itr; + } + ++feature_id_; + + return feature; +} + diff --git a/plugins/input/sqlserver/sqlserver_featureset.hpp b/plugins/input/sqlserver/sqlserver_featureset.hpp new file mode 100644 index 000000000..9a891e63b --- /dev/null +++ b/plugins/input/sqlserver/sqlserver_featureset.hpp @@ -0,0 +1,49 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2015 we-do-IT + * + *****************************************************************************/ + +#ifndef SQLSERVER_FEATURESET_HPP +#define SQLSERVER_FEATURESET_HPP + +// mapnik +#include +#include +#include +#include +#include + +// boost +#include +#include + +// sql server (via odbc) +#ifdef _WINDOWS +#include +#endif +#include "sql.h" + +#include +#include + +class sqlserver_featureset : public mapnik::Featureset +{ +public: + sqlserver_featureset(SQLHDBC hdbc, + std::string const& sqlstring, + mapnik::layer_descriptor const& desc); + virtual ~sqlserver_featureset(); + mapnik::feature_ptr next(); + +private: + SQLHANDLE hstmt_; + mapnik::layer_descriptor desc_; + boost::scoped_ptr tr_; + mapnik::value_integer feature_id_; + mapnik::context_ptr ctx_; +}; + +#endif // SQLSERVER_FEATURESET_HPP diff --git a/plugins/input/sqlserver/sqlserver_geometry_parser.cpp b/plugins/input/sqlserver/sqlserver_geometry_parser.cpp new file mode 100644 index 000000000..6d1c066ef --- /dev/null +++ b/plugins/input/sqlserver/sqlserver_geometry_parser.cpp @@ -0,0 +1,500 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * adapted from ogrmssqlgeometryparser.cpp, part of OGR + * original copyright notice follows + * + *****************************************************************************/ + + +/****************************************************************************** + * $Id: ogrmssqlgeometryparser.cpp 24918 2012-09-07 12:02:01Z tamas $ + * + * Project: MSSQL Spatial driver + * Purpose: Implements ogrmssqlgeometryparser class to parse native SqlGeometries. + * Author: Tamas Szekeres, szekerest at gmail.com + * + ****************************************************************************** + * Copyright (c) 2010, Tamas Szekeres + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#include "sqlserver_geometry_parser.hpp" +#include "sqlserver_datasource.hpp" + +#include + +class sqlserver_geometry_parser_exception : public sqlserver_datasource_exception +{ +public: + sqlserver_geometry_parser_exception(std::string const& message) + : sqlserver_datasource_exception("Geometry Parser: "+message) {} + + virtual ~sqlserver_geometry_parser_exception() throw() {} +}; + + +/* SqlGeometry serialization format + +Simple Point (SerializationProps & IsSinglePoint) + [SRID][0x01][SerializationProps][Point][z][m] + +Simple Line Segment (SerializationProps & IsSingleLineSegment) + [SRID][0x01][SerializationProps][Point1][Point2][z1][z2][m1][m2] + +Complex Geometries + [SRID][0x01][SerializationProps][NumPoints][Point1]..[PointN][z1]..[zN][m1]..[mN] + [NumFigures][Figure]..[Figure][NumShapes][Shape]..[Shape] + +SRID + Spatial Reference Id (4 bytes) + +SerializationProps (bitmask) 1 byte + 0x01 = HasZValues + 0x02 = HasMValues + 0x04 = IsValid + 0x08 = IsSinglePoint + 0x10 = IsSingleLineSegment + 0x20 = IsWholeGlobe + +Point (2-4)x8 bytes, size depends on SerializationProps & HasZValues & HasMValues + [x][y] - SqlGeometry + [latitude][longitude] - SqlGeography + +Figure + [FigureAttribute][PointOffset] + +FigureAttribute (1 byte) + 0x00 = Interior Ring + 0x01 = Stroke + 0x02 = Exterior Ring + +Shape + [ParentFigureOffset][FigureOffset][ShapeType] + +ShapeType (1 byte) + 0x00 = Unknown + 0x01 = Point + 0x02 = LineString + 0x03 = Polygon + 0x04 = MultiPoint + 0x05 = MultiLineString + 0x06 = MultiPolygon + 0x07 = GeometryCollection + +*/ + +/************************************************************************/ +/* Geometry parser macros */ +/************************************************************************/ + +#define SP_NONE 0 +#define SP_HASZVALUES 1 +#define SP_HASMVALUES 2 +#define SP_ISVALID 4 +#define SP_ISSINGLEPOINT 8 +#define SP_ISSINGLELINESEGMENT 0x10 +#define SP_ISWHOLEGLOBE 0x20 + +#define ST_UNKNOWN 0 +#define ST_POINT 1 +#define ST_LINESTRING 2 +#define ST_POLYGON 3 +#define ST_MULTIPOINT 4 +#define ST_MULTILINESTRING 5 +#define ST_MULTIPOLYGON 6 +#define ST_GEOMETRYCOLLECTION 7 + +#define ReadInt32(nPos) (*((unsigned int*)(pszData + (nPos)))) + +#define ReadByte(nPos) (pszData[nPos]) + +#define ReadDouble(nPos) (*((double*)(pszData + (nPos)))) + +#define ParentOffset(iShape) (ReadInt32(nShapePos + (iShape) * 9 )) +#define FigureOffset(iShape) (ReadInt32(nShapePos + (iShape) * 9 + 4)) +#define ShapeType(iShape) (ReadByte(nShapePos + (iShape) * 9 + 8)) + +#define NextFigureOffset(iShape) (iShape + 1 < nNumShapes? FigureOffset((iShape) +1) : nNumFigures) + +#define FigureAttribute(iFigure) (ReadByte(nFigurePos + (iFigure) * 5)) +#define PointOffset(iFigure) (ReadInt32(nFigurePos + (iFigure) * 5 + 1)) +#define NextPointOffset(iFigure) (iFigure + 1 < nNumFigures? PointOffset((iFigure) +1) : nNumPoints) + +#define ReadX(iPoint) (ReadDouble(nPointPos + 16 * (iPoint))) +#define ReadY(iPoint) (ReadDouble(nPointPos + 16 * (iPoint) + 8)) +#define ReadZ(iPoint) (ReadDouble(nPointPos + 16 * nNumPoints + 8 * (iPoint))) +#define ReadM(iPoint) (ReadDouble(nPointPos + 24 * nNumPoints + 8 * (iPoint))) + +/************************************************************************/ +/* sqlserver_geometry_parser() */ +/************************************************************************/ + +sqlserver_geometry_parser::sqlserver_geometry_parser(spatial_data_type columnType) +{ + colType = columnType; +} + +/************************************************************************/ +/* ReadPoint() */ +/************************************************************************/ + +mapnik::geometry::point sqlserver_geometry_parser::ReadPoint(int iShape) +{ + mapnik::geometry::point geom; + int iFigure = FigureOffset(iShape); + if ( iFigure < nNumFigures ) { + int iPoint = PointOffset(iFigure); + if ( iPoint < nNumPoints ) { + if (colType == Geography) { + geom.x = ReadY(iPoint); + geom.y = ReadX(iPoint); + } else { + geom.x = ReadX(iPoint); + geom.y = ReadY(iPoint); + } + } + } + return geom; +} + +/************************************************************************/ +/* ReadMultiPoint() */ +/************************************************************************/ + +mapnik::geometry::multi_point sqlserver_geometry_parser::ReadMultiPoint(int iShape) +{ + mapnik::geometry::multi_point geom; + + for (int i = iShape + 1; i < nNumShapes; i++) { + if (ParentOffset(i) == (unsigned int)iShape) { + if ( ShapeType(i) == ST_POINT ) { + geom.emplace_back(ReadPoint(i)); + } + } + } + + return geom; +} + +/************************************************************************/ +/* ReadLineString() */ +/************************************************************************/ + +mapnik::geometry::line_string sqlserver_geometry_parser::ReadLineString(int iShape) +{ + mapnik::geometry::line_string geom; + int iFigure = FigureOffset(iShape); + + int iPoint = PointOffset(iFigure); + int iNextPoint = NextPointOffset(iFigure); + while (iPoint < iNextPoint) { + if (colType == Geography) { + geom.emplace_back(ReadY(iPoint), ReadX(iPoint)); + } else { + geom.emplace_back(ReadX(iPoint), ReadY(iPoint)); + } + + ++iPoint; + } + + return geom; +} + +/************************************************************************/ +/* ReadMultiLineString() */ +/************************************************************************/ + +mapnik::geometry::multi_line_string sqlserver_geometry_parser::ReadMultiLineString(int iShape) +{ + mapnik::geometry::multi_line_string geom; + + for (int i = iShape + 1; i < nNumShapes; i++) { + if (ParentOffset(i) == (unsigned int)iShape) { + if ( ShapeType(i) == ST_LINESTRING ) { + geom.emplace_back(ReadLineString(i)); + } + } + } + + return geom; +} + +/************************************************************************/ +/* ReadPolygon() */ +/************************************************************************/ + +mapnik::geometry::polygon sqlserver_geometry_parser::ReadPolygon(int iShape) +{ + mapnik::geometry::polygon geom; + int iNextFigure = NextFigureOffset(iShape); + + for (int iFigure = FigureOffset(iShape); iFigure < iNextFigure; iFigure++) { + mapnik::geometry::linear_ring ring; + int iPoint = PointOffset(iFigure); + int iNextPoint = NextPointOffset(iFigure); + while (iPoint < iNextPoint) { + if (colType == Geography) { + ring.emplace_back(ReadY(iPoint), ReadX(iPoint)); + } else { + ring.emplace_back(ReadX(iPoint), ReadY(iPoint)); + } + + ++iPoint; + } + if (iFigure == 0) { + geom.set_exterior_ring(std::move(ring)); + } else { + geom.add_hole(std::move(ring)); + } + } + return geom; +} + +/************************************************************************/ +/* ReadMultiPolygon() */ +/************************************************************************/ + +mapnik::geometry::multi_polygon sqlserver_geometry_parser::ReadMultiPolygon(int iShape) +{ + mapnik::geometry::multi_polygon geom; + + for (int i = iShape + 1; i < nNumShapes; i++) { + if (ParentOffset(i) == (unsigned int)iShape) { + if ( ShapeType(i) == ST_POLYGON ) { + geom.emplace_back(ReadPolygon(i)); + } + } + } + + return geom; +} + +/************************************************************************/ +/* ReadGeometryCollection() */ +/************************************************************************/ + +mapnik::geometry::geometry_collection sqlserver_geometry_parser::ReadGeometryCollection(int iShape) +{ + mapnik::geometry::geometry_collection geom; + + for (int i = iShape + 1; i < nNumShapes; i++) { + mapnik::geometry::geometry shape = mapnik::geometry::geometry_empty(); + if (ParentOffset(i) == (unsigned int)iShape) { + switch (ShapeType(i)) + { + case ST_POINT: + shape = ReadPoint(i); + break; + case ST_LINESTRING: + shape = ReadLineString(i); + break; + case ST_POLYGON: + shape = ReadPolygon(i); + break; + case ST_MULTIPOINT: + shape = ReadMultiPoint(i); + break; + case ST_MULTILINESTRING: + shape = ReadMultiLineString(i); + break; + case ST_MULTIPOLYGON: + shape = ReadMultiPolygon(i); + break; + case ST_GEOMETRYCOLLECTION: + shape = ReadGeometryCollection(i); + break; + } + } + geom.push_back(shape); + } + + return geom; +} + + +/************************************************************************/ +/* parse_sql_geometry() */ +/************************************************************************/ + +mapnik::geometry::geometry sqlserver_geometry_parser::parse(unsigned char* pszInput, int nLen) +{ + if (nLen < 10) { + throw sqlserver_geometry_parser_exception("not enough data, nLen < 10"); + } + + pszData = pszInput; + + /* store the SRS id for further use */ + nSRSId = ReadInt32(0); + + if ( ReadByte(4) != 1 ) + { + throw sqlserver_geometry_parser_exception("corrupt data, ReadByte(4) != 1"); + } + + chProps = ReadByte(5); + + if ( chProps & SP_HASMVALUES ) + nPointSize = 32; + else if ( chProps & SP_HASZVALUES ) + nPointSize = 24; + else + nPointSize = 16; + + mapnik::geometry::geometry geom; + if ( chProps & SP_ISSINGLEPOINT ) + { + // single point geometry + nNumPoints = 1; + nPointPos = 6; + + if (nLen < 6 + nPointSize) + { + throw sqlserver_geometry_parser_exception("not enough data, nLen < 6 + nPointSize"); + } + + mapnik::geometry::point point; + + if (colType == Geography) + { + point.x = ReadY(0); + point.y = ReadX(0); + } + else + { + point.x = ReadX(0); + point.y = ReadY(0); + } + geom = point; + } + else if ( chProps & SP_ISSINGLELINESEGMENT ) + { + // single line segment with 2 points + nNumPoints = 2; + nPointPos = 6; + + if (nLen < 6 + 2 * nPointSize) + { + throw sqlserver_geometry_parser_exception("not enough data, nLen < 6 + 2 * nPointSize"); + } + + mapnik::geometry::line_string line; + + if (colType == Geography) + { + line.emplace_back(ReadY(0), ReadX(0)); + line.emplace_back(ReadY(1), ReadX(1)); + } + else + { + line.emplace_back(ReadX(0), ReadY(0)); + line.emplace_back(ReadX(1), ReadY(1)); + } + geom = line; + + } + else + { + // complex geometries + nNumPoints = ReadInt32(6); + + if ( nNumPoints <= 0 ) + { + throw sqlserver_geometry_parser_exception("negative number of points, nNumPoints <= 0"); + } + + // position of the point array + nPointPos = 10; + + // position of the figures + nFigurePos = nPointPos + nPointSize * nNumPoints + 4; + + if (nLen < nFigurePos) + { + throw sqlserver_geometry_parser_exception("not enough data, nLen < nFigurePos"); + } + + nNumFigures = ReadInt32(nFigurePos - 4); + + if ( nNumFigures <= 0 ) + { + throw sqlserver_geometry_parser_exception("negative number of figures, nNumFigures <= 0"); + } + + // position of the shapes + nShapePos = nFigurePos + 5 * nNumFigures + 4; + + if (nLen < nShapePos) + { + throw sqlserver_geometry_parser_exception("not enough data, nLen < nShapePos"); + } + + nNumShapes = ReadInt32(nShapePos - 4); + + if (nLen < nShapePos + 9 * nNumShapes) + { + throw sqlserver_geometry_parser_exception("not enough data, nLen < nShapePos + 9 * nNumShapes"); + } + + if ( nNumShapes <= 0 ) + { + throw sqlserver_geometry_parser_exception("negative number of shapes, nNumShapes <= 0"); + } + + // pick up the root shape + if ( ParentOffset(0) != 0xFFFFFFFF) + { + throw sqlserver_geometry_parser_exception("corrupt data, ParentOffset(0) != 0xFFFFFFFF"); + } + + // determine the shape type + switch (ShapeType(0)) + { + case ST_POINT: + geom = ReadPoint(0); + break; + case ST_LINESTRING: + geom = ReadLineString(0); + break; + case ST_POLYGON: + geom = ReadPolygon(0); + break; + case ST_MULTIPOINT: + geom = ReadMultiPoint(0); + break; + case ST_MULTILINESTRING: + geom = ReadMultiLineString(0); + break; + case ST_MULTIPOLYGON: + geom = ReadMultiPolygon(0); + break; + case ST_GEOMETRYCOLLECTION: + geom = ReadGeometryCollection(0); + break; + default: + throw sqlserver_geometry_parser_exception("unsupported geometry type"); + } + } + + return geom; +} + diff --git a/plugins/input/sqlserver/sqlserver_geometry_parser.hpp b/plugins/input/sqlserver/sqlserver_geometry_parser.hpp new file mode 100644 index 000000000..fb810ac7f --- /dev/null +++ b/plugins/input/sqlserver/sqlserver_geometry_parser.hpp @@ -0,0 +1,45 @@ +// mapnik +#include + +enum spatial_data_type { + Geometry, + Geography +}; + +class sqlserver_geometry_parser +{ +protected: + unsigned char* pszData; + /* serialization propeties */ + char chProps; + /* point array */ + int nPointSize; + int nPointPos; + int nNumPoints; + /* figure array */ + int nFigurePos; + int nNumFigures; + /* shape array */ + int nShapePos; + int nNumShapes; + int nSRSId; + /* geometry or geography */ + spatial_data_type colType; + +protected: + mapnik::geometry::point ReadPoint(int iShape); + mapnik::geometry::multi_point ReadMultiPoint(int iShape); + mapnik::geometry::line_string ReadLineString(int iShape); + mapnik::geometry::multi_line_string ReadMultiLineString(int iShape); + mapnik::geometry::polygon ReadPolygon(int iShape); + mapnik::geometry::multi_polygon ReadMultiPolygon(int iShape); + mapnik::geometry::geometry_collection ReadGeometryCollection(int iShape); + +public: + sqlserver_geometry_parser(spatial_data_type columnType); + + mapnik::geometry::geometry parse(unsigned char* pszInput, int nLen); + int get_srs_id() { return nSRSId; }; +}; + +