From b8ac9b19846febdcff34c26e5d82e90056d5523c Mon Sep 17 00:00:00 2001 From: Jean-Francois Doyon Date: Fri, 14 Apr 2006 03:45:46 +0000 Subject: [PATCH] OK, final fixes to the WMS stuff, cleaned up documentation, and so on. Things should be good to go for a first try by the wider community! --- INSTALL | 36 +++++++++- bindings/python/mapnik/ogcserver/WMS.py | 53 ++++++++++++++ bindings/python/mapnik/ogcserver/cgiserver.py | 69 ++++++++++++------- bindings/python/mapnik/ogcserver/common.py | 25 ++++--- .../python/mapnik/ogcserver/exceptions.py | 25 +++++-- bindings/python/mapnik/ogcserver/wms111.py | 7 +- bindings/python/mapnik/ogcserver/wms130.py | 6 +- docs/ogcserver/readme.txt | 26 +++++-- utils/ogcserver/ogcserver.conf | 6 +- 9 files changed, 193 insertions(+), 60 deletions(-) create mode 100644 bindings/python/mapnik/ogcserver/WMS.py diff --git a/INSTALL b/INSTALL index 42fba4ca7..87d80b467 100644 --- a/INSTALL +++ b/INSTALL @@ -22,7 +22,8 @@ First, here is a quick list of the software dependencies: - libtiff - libz - libfreetype2 -- (Optional) PostgreSQL libraries +- (Optional) PostgreSQL libraries (For PostGIS support) +- (Optional) PROJ.4 (More on this below) - Python 1.5.2 or greater to build Mapnik - (Optional) Python 2.2 or greater for the Python language bindings @@ -33,6 +34,7 @@ If your system does NOT have one of these installed, you will need to install th Also, a minimum of 256MB of RAM is recommended for the build process. + Building -------- @@ -101,6 +103,14 @@ PGSQL_LIBS: Search path for PostgreSQL library files ( /path/to/PGSQL_LIBS ) default: /usr/lib actual: /usr/lib +PROJ_INCLUDES: Search path for PROJ.4 include files ( /path/to/PROJ_INCLUDES ) + default: /usr/local/include + actual: /usr/local/include + +PROJ_LIBS: Search path for PROJ.4 include files ( /path/to/PROJ_LIBS ) + default: /usr/local/lib + actual: /usr/local/lib + PYTHON: Python executable ( /path/to/PYTHON ) default: /usr/bin/python actual: /usr/bin/python @@ -146,6 +156,30 @@ $PYTHON_PREFIX/lib/python$PYTHON_VERSION/site-packages/mapnik: Python bindings If you're using the default PREFIX, you will most likely need to be root to perform the install. +A note on projection support +---------------------------- + +At this time Mapnik's core C++ library and map rendering engine does NOT support on-the-fly cartographic +reprojections. + +Mapnik can however be configured to build the Python API to the PROJ.4 library. This provides projection +support through Python, and is used by the WMS ogcserver feature, since that server is written in Python. + +Here is an example on how to use it: + +>>> from mapnik import Projection +registered datasource : raster +registered datasource : shape +registered datasource : postgis +>>> p = Projection(['init=epsg:42304']) +>>> p.Inverse(12345.245,143225.56) +[-94.825927695613018, 50.290732340975467] +>>> + +The Projection() instance provides Inverse() and Forward() methods. For details on the possible parameters, +see the PROJ.4 documentation. + + Test ---- diff --git a/bindings/python/mapnik/ogcserver/WMS.py b/bindings/python/mapnik/ogcserver/WMS.py new file mode 100644 index 000000000..c673f2887 --- /dev/null +++ b/bindings/python/mapnik/ogcserver/WMS.py @@ -0,0 +1,53 @@ +# +# This file is part of Mapnik (c++ mapping toolkit) +# +# Copyright (C) 2006 Jean-Francois Doyon +# +# 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 +# +# $Id$ + +from common import Version +from exceptions import OGCException, ServerConfigurationError +from wms111 import ServiceHandler as ServiceHandler111 +from wms130 import ServiceHandler as ServiceHandler130 + +def ServiceHandlerFactory(conf, mapfactory, onlineresource, version): + + if not version: + version = Version('1.3.0') + else: + version = Version(version) + if version >= '1.3.0': + return ServiceHandler130(conf, mapfactory, onlineresource) + else: + return ServiceHandler111(conf, mapfactory, onlineresource) + +class BaseWMSFactory: + + def __init__(self): + self.layers = {} + self.styles = {} + + def register_layer(self, layer): + layername = layer.name() + if not layername: + raise ServerConfigurationError('There is an un-named layer.') + self.layers[layername] = layer + + def register_style(self, name, style): + if not name: + raise ServerConfigurationError('There is an un-named style.') + self.styles[name] = style \ No newline at end of file diff --git a/bindings/python/mapnik/ogcserver/cgiserver.py b/bindings/python/mapnik/ogcserver/cgiserver.py index 4dbe8eabe..63517cb48 100644 --- a/bindings/python/mapnik/ogcserver/cgiserver.py +++ b/bindings/python/mapnik/ogcserver/cgiserver.py @@ -25,10 +25,11 @@ environ['PYTHON_EGG_CACHE'] = gettempdir() import sys from jon import cgi -from exceptions import OGCException -from wms130 import ExceptionHandler -from lxml import etree as ElementTree +from exceptions import OGCException, ServerConfigurationError +from wms111 import ExceptionHandler as ExceptionHandler111 +from wms130 import ExceptionHandler as ExceptionHandler130 from ConfigParser import SafeConfigParser +from common import Version class Handler(cgi.DebugHandler): @@ -36,29 +37,36 @@ class Handler(cgi.DebugHandler): conf = SafeConfigParser() conf.readfp(open(self.configpath)) self.conf = conf - mapfactorymodule = __import__(conf.get('server', 'module')) - self.mapfactory = getattr(mapfactorymodule, 'WMSFactory')() + if not conf.has_option('server', 'module'): + raise ServerConfigurationError('The factory module is not defined in the configuration file.') + try: + mapfactorymodule = __import__(conf.get('server', 'module')) + except ImportError: + raise ServerConfigurationError('The factory module could not be loaded.') + if hasattr(mapfactorymodule, 'WMSFactory'): + self.mapfactory = getattr(mapfactorymodule, 'WMSFactory')() + else: + raise ServerConfigurationError('The factory module does not have a WMSFactory class.') + if conf.has_option('server', 'debug'): + self.debug = int(conf.get('server', 'debug')) + else: + self.debug = 0 def process(self, req): - exceptionhandler = ExceptionHandler - reqparams = {} - for key, value in req.params.items(): - reqparams[key.lower()] = value + reqparams = lowerparams(req.params) onlineresource = 'http://%s:%s%s?' % (req.environ['SERVER_NAME'], req.environ['SERVER_PORT'], req.environ['SCRIPT_NAME']) -# try: if not reqparams.has_key('request'): raise OGCException('Missing request parameter.') if reqparams['request'] == 'GetCapabilities' and not reqparams.has_key('service'): raise OGCException('Missing service parameter.') if reqparams['request'] in ['GetMap', 'GetFeatureInfo']: - reqparams['service'] = 'wms' - service = reqparams['service'].lower() + reqparams['service'] = 'WMS' try: - mapnikmodule = __import__('mapnik.ogcserver.' + service) + mapnikmodule = __import__('mapnik.ogcserver.' + reqparams['service']) except: - raise OGCException('Unsupported service "%s".' % service) - ServiceHandlerFactory = getattr(mapnikmodule.ogcserver, service).ServiceHandlerFactory - servicehandler, exceptionhandler = ServiceHandlerFactory(self.conf, self.mapfactory, onlineresource, reqparams.get('version', None)) + raise OGCException('Unsupported service "%s".' % reqparams['service']) + ServiceHandlerFactory = getattr(mapnikmodule.ogcserver, reqparams['service']).ServiceHandlerFactory + servicehandler = ServiceHandlerFactory(self.conf, self.mapfactory, onlineresource, reqparams.get('version', None)) if reqparams['request'] not in servicehandler.SERVICE_PARAMS.keys(): raise OGCException('Operation "%s" not supported.' % reqparams['request'], 'OperationNotSupported') ogcparams = servicehandler.processParameters(reqparams['request'], reqparams) @@ -67,14 +75,25 @@ class Handler(cgi.DebugHandler): except: raise OGCException('Operation "%s" not supported.' % reqparams['request'], 'OperationNotSupported') response = requesthandler(ogcparams) -# except: -# raise -# else: req.set_header('Content-Type', response.content_type) req.write(response.content) - """ - except OGCException: - eh = exceptionhandler() - req.set_header('Content-Type', eh.mimetype) - req.write(ElementTree.tostring(eh.getexcetree(sys.exc_info()[1]))) - """ \ No newline at end of file + + def traceback(self, req): + reqparams = lowerparams(req.params) + version = reqparams.get('version', None) + if not version: + version = Version('1.3.0') + else: + version = Version(version) + if version >= '1.3.0': + eh = ExceptionHandler130(self.debug) + else: + eh = ExceptionHandler111(self.debug) + req.set_header('Content-Type', eh.mimetype) + req.write(eh.getcontent()) + +def lowerparams(params): + reqparams = {} + for key, value in params.items(): + reqparams[key.lower()] = value + return reqparams \ No newline at end of file diff --git a/bindings/python/mapnik/ogcserver/common.py b/bindings/python/mapnik/ogcserver/common.py index 5c7b235a2..543ed6463 100644 --- a/bindings/python/mapnik/ogcserver/common.py +++ b/bindings/python/mapnik/ogcserver/common.py @@ -195,8 +195,6 @@ class CRSFactory: class WMSBaseServiceHandler(BaseServiceHandler): def GetMap(self, params): - if str(params['crs']) != str(self.crs): - raise OGCException('Unsupported CRS requested. Must be "%s" and not "%s".' % (self.crs, params['crs']), 'InvalidCRS') if params['bbox'][0] >= params['bbox'][2]: raise OGCException("BBOX values don't make sense. minx is greater than maxx.") if params['bbox'][1] >= params['bbox'][3]: @@ -204,18 +202,19 @@ class WMSBaseServiceHandler(BaseServiceHandler): m = Map(params['width'], params['height']) if params.has_key('transparent') and params['transparent'] == 'FALSE': m.background = params['bgcolor'] - maplayers = self.mapfactory.getlayers() - mapstyles = self.mapfactory.getstyles() + maplayers = self.mapfactory.layers + mapstyles = self.mapfactory.styles for layername in params['layers']: - for layer in maplayers: - if layer.name() == layername: - for stylename in layer.styles: - if stylename in mapstyles.keys(): - m.append_style(stylename, mapstyles[stylename]) - m.layers.append(layer) - if len(m.layers) != len(params['layers']): - badnames = [ layername for layername in params['layers'] if layername not in [ layer.name() for layer in m.layers ] ] - raise OGCException('The following layers are not defined by this server: %s.' % ','.join(badnames), 'LayerNotDefined') + try: + layer = maplayers[layername] + except KeyError: + raise OGCException('Layer not defined: %s.' % layername, 'LayerNotDefined') + for stylename in layer.styles: + if stylename in mapstyles.keys(): + m.append_style(stylename, mapstyles[stylename]) + else: + raise ServerConfigurationError('Layer "%s" refers to non-existent style "%s".' % (layername, stylename)) + m.layers.append(layer) m.zoom_to_box(Envelope(params['bbox'][0], params['bbox'][1], params['bbox'][2], params['bbox'][3])) im = Image(params['width'], params['height']) render(m, im) diff --git a/bindings/python/mapnik/ogcserver/exceptions.py b/bindings/python/mapnik/ogcserver/exceptions.py index 3b3566bee..877710af7 100644 --- a/bindings/python/mapnik/ogcserver/exceptions.py +++ b/bindings/python/mapnik/ogcserver/exceptions.py @@ -20,6 +20,10 @@ # $Id$ from copy import deepcopy +from lxml import etree as ElementTree +from StringIO import StringIO +from traceback import print_tb +from sys import exc_info class OGCException(Exception): pass @@ -29,11 +33,20 @@ class ServerConfigurationError(Exception): class BaseExceptionHandler: - def getexcetree(self, exc): + def __init__(self, debug): + self.debug = debug + + def getcontent(self): + excinfo = exc_info() ogcexcetree = deepcopy(self.xmltemplate) e = ogcexcetree.find(self.xpath) - if len(exc.args) > 0: - e.text = exc.args[0] - if len(exc.args) > 1: - e.set('code', exc.args[1]) - return ogcexcetree \ No newline at end of file + if self.debug: + fh = StringIO() + print_tb(excinfo[2], None, fh) + fh.seek(0) + e.text = '\n' + fh.read() + '\n' + str(excinfo[0]) + ': ' + ', '.join(excinfo[1].args) + '\n' + elif len(excinfo[1].args) > 0: + e.text = excinfo[1].args[0] + if isinstance(excinfo[1], OGCException) and len(excinfo[1].args) > 1: + e.set('code', excinfo[1].args[1]) + return ElementTree.tostring(ogcexcetree) \ No newline at end of file diff --git a/bindings/python/mapnik/ogcserver/wms111.py b/bindings/python/mapnik/ogcserver/wms111.py index 1d3c85fdb..156c35097 100644 --- a/bindings/python/mapnik/ogcserver/wms111.py +++ b/bindings/python/mapnik/ogcserver/wms111.py @@ -140,7 +140,7 @@ class ServiceHandler(WMSBaseServiceHandler): rootlayersrs = rootlayerelem.find('SRS') rootlayersrs.text = str(self.crs) - for layer in self.mapfactory.getlayers(): + for layer in self.mapfactory.layers.values(): layername = ElementTree.Element('Name') layername.text = layer.name() layertitle = ElementTree.Element('Title') @@ -172,6 +172,11 @@ class ServiceHandler(WMSBaseServiceHandler): response = Response('application/vnd.ogc.wms_xml', self.capabilities) return response + def GetMap(self, params): + if str(params['srs']) != str(self.crs): + raise OGCException('Unsupported SRS requested. Must be "%s" and not "%s".' % (self.crs, params['crs']), 'InvalidCRS') + return WMSBaseServiceHandler.GetMap(self, params) + class ExceptionHandler(BaseExceptionHandler): mimetype = "application/vnd.ogc.se_xml" diff --git a/bindings/python/mapnik/ogcserver/wms130.py b/bindings/python/mapnik/ogcserver/wms130.py index 7754fb57a..53e4769f9 100644 --- a/bindings/python/mapnik/ogcserver/wms130.py +++ b/bindings/python/mapnik/ogcserver/wms130.py @@ -126,7 +126,7 @@ class ServiceHandler(WMSBaseServiceHandler): servicee = capetree.find('{http://www.opengis.net/wms}Service') for item in self.CONF_SERVICE: if self.conf.has_option('service', item[0]): - value = self.conf.get('service', item[0]) + value = self.conf.get('service', item[0]).strip() try: item[2](value) except: @@ -147,7 +147,7 @@ class ServiceHandler(WMSBaseServiceHandler): rootlayercrs = rootlayerelem.find('{http://www.opengis.net/wms}CRS') rootlayercrs.text = str(self.crs) - for layer in self.mapfactory.getlayers(): + for layer in self.mapfactory.layers.values(): layername = ElementTree.Element('Name') layername.text = layer.name() layertitle = ElementTree.Element('Title') @@ -190,6 +190,8 @@ class ServiceHandler(WMSBaseServiceHandler): def GetMap(self, params): if params['width'] > int(self.conf.get('service', 'maxwidth')) or params['height'] > int(self.conf.get('service', 'maxheight')): raise OGCException('Requested map size exceeds limits set by this server.') + if str(params['crs']) != str(self.crs): + raise OGCException('Unsupported CRS requested. Must be "%s" and not "%s".' % (self.crs, params['crs']), 'InvalidCRS') return WMSBaseServiceHandler.GetMap(self, params) class ExceptionHandler(BaseExceptionHandler): diff --git a/docs/ogcserver/readme.txt b/docs/ogcserver/readme.txt index bb36592e0..483d116ca 100644 --- a/docs/ogcserver/readme.txt +++ b/docs/ogcserver/readme.txt @@ -89,15 +89,27 @@ API, look in demo/python, or in docs/epydocs. The server needs a python module, with code that looks like this: -from mapnik.ogcserver.wms import BaseWMSFactory +from mapnik.ogcserver.WMS import BaseWMSFactory -class MapFactory: +class WMSFactory(BaseWMSFactory): def __init(self): + BaseWMSFactory.__init__(self) + sty = Style() ... - - def getlayers(self): + self.register_style('stylename', sty) + + lyr = Layer(name='layername') ... - - def getstyles(self): - ... \ No newline at end of file + self.register_layer(lyr) + +The rules for writing this class are: + +- It MUST be called 'WMSFactory'. +- It MUST sub-class mapnik.ogcserver.WMS.BaseWMSFactory. +- The __init__ MUST call the base class'. +- Layers MUST be named with the 'name' parameter to the constructor. +- style and layer names are meant for machine readability, not human. Keep + them short and simple, without spaces or special characters. +- The layers must have at least one style associated with them (a default). +- No Map() object is used or needed here. \ No newline at end of file diff --git a/utils/ogcserver/ogcserver.conf b/utils/ogcserver/ogcserver.conf index 8008c0b4f..a32535b4d 100644 --- a/utils/ogcserver/ogcserver.conf +++ b/utils/ogcserver/ogcserver.conf @@ -36,8 +36,4 @@ epsg=4326 # supporting the service for example. This is NOT the online # resource pointing to the CGI. -onlineresource=http://www.mapnik.org/ - -[contact] -name= -email= +onlineresource=http://www.mapnik.org/ \ No newline at end of file