├── .gitignore ├── AUTHORS.txt ├── CHANGELOG.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── bin └── ogcserver ├── conf ├── fcgi_app.py └── map_factory.py ├── demo ├── map.xml ├── openlayers.html ├── world_merc.dbf ├── world_merc.index ├── world_merc.json ├── world_merc.prj ├── world_merc.shp ├── world_merc.shx └── world_merc_license.txt ├── docs ├── overview.txt └── readme.txt ├── ogcserver ├── WMS.py ├── __init__.py ├── cgiserver.py ├── common.py ├── configparser.py ├── default.conf ├── exceptions.py ├── modserver.py ├── wms111.py ├── wms130.py └── wsgi.py ├── setup.py └── tests ├── empty.dbf ├── empty.shp ├── map_factory.py ├── mapfile_background-color.xml ├── mapfile_encoding.xml ├── mapfile_styles.xml ├── ogcserver.conf ├── shape_encoding.xml ├── shape_iso8859-1_col.dbf ├── shape_iso8859-1_col.shp ├── shape_iso8859-1_col.shx ├── shape_iso8859-1_col.zip ├── testGetCapabilities.py ├── testGetFeatureinfo.py ├── testGetMap.py ├── testLayerStyles.py ├── testLoadMapFail.py ├── testLoadMapFromString.py └── testWsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *pyc 2 | .DS_Store 3 | build 4 | dist 5 | *egg-info 6 | MANIFEST 7 | *~ 8 | /.project 9 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Written by Jean-Francois Doyon. 2 | 3 | Contributions from: 4 | 5 | Manel Clos 6 | Tom MacWright 7 | Dane Springmeyer 8 | Carsten Klein -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # OGCServer Changelog 2 | 3 | ## 0.1.1 4 | 5 | Released ... 6 | 7 | (Packaged from ...) 8 | 9 | Summary: TODO 10 | 11 | - Switch from lxml to xml.etree 12 | - Add bind address and port options to ogcserver script 13 | - Improved setup.py thanks to https://github.com/plepe 14 | - Improved ogcserver script thanks to https://github.com/plepe 15 | - Improved tests suite 16 | - Added support for writing root layer LatLonBoundingBox in capabilities and EX_GeographicBoundingBox to root layer for WMS 1.3.0 thanks to Per Liedman 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Previously (2006-2009) licensed LGPL as part of Mapnik codebase. 2 | 3 | Standalone 'ogcserver' module, with approval from original author 4 | J.F. Doyon, is now BSD licenced, as below: 5 | 6 | 7 | Copyright (c) 2010, Jean-Francois Doyon 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are 12 | met: 13 | 14 | * Redistributions of source code must retain the above copyright 15 | notice, this list of conditions and the following disclaimer. 16 | * Redistributions in binary form must reproduce the above 17 | copyright notice, this list of conditions and the following 18 | disclaimer in the documentation and/or other materials provided 19 | with the distribution. 20 | * Neither the name of the author nor the names of other 21 | contributors may be used to endorse or promote products derived 22 | from this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 25 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 26 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 27 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 28 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 29 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 30 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 31 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 32 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 34 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include setup.py 3 | recursive-include ogcserver *.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Ogcserver 3 | 4 | Python WMS implementation using Mapnik. 5 | 6 | ## Depends 7 | 8 | Mapnik >= 0.7.0 (and python bindings) 9 | Pillow 10 | PasteScript 11 | WebOb 12 | 13 | You will need to install Mapnik separately. 14 | 15 | All the remaining dependencies should be installed cleanly with the command below. 16 | 17 | 18 | ## Install 19 | 20 | Run the following command inside this directory (the directory that also contains the 'setup.py' file): 21 | 22 | sudo python setup.py install 23 | 24 | 25 | ## Testing 26 | 27 | Run the local http server with the sample data: 28 | 29 | ogcserver demo/map.xml 30 | 31 | Viewing http://localhost:8000/ in a local browser should show a welcome message like 'Welcome to the OGCServer' 32 | 33 | Now you should be able to access a map tile with a basic WMS request like: 34 | 35 | http://localhost:8000/?LAYERS=__all__&STYLES=&FORMAT=image%2Fpng&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG%3A3857&BBOX=-20037508.34,-20037508.34,20037508.3384,20037508.3384&WIDTH=256&HEIGHT=256 36 | 37 | 38 | -------------------------------------------------------------------------------- /bin/ogcserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | import socket 6 | from os import path 7 | from pkg_resources import * 8 | import argparse 9 | 10 | parser = argparse.ArgumentParser(description='Runs the ogcserver as WMS server') 11 | 12 | parser.add_argument('mapfile', type=str, help=''' 13 | A XML mapnik stylesheet 14 | ''') 15 | 16 | parser.add_argument('-c', '--config', dest='configfile', help=''' 17 | Path to the config file. 18 | ''') 19 | parser.add_argument('-b', '--bind', dest='bind_address', help=''' 20 | Bind to address. 21 | ''') 22 | parser.add_argument('-p', '--port', dest='bind_port', type=int, help=''' 23 | Listen on port. 24 | ''') 25 | 26 | args = parser.parse_args() 27 | 28 | sys.path.insert(0,os.path.abspath('.')) 29 | 30 | from ogcserver.wsgi import WSGIApp 31 | import ogcserver 32 | 33 | configfile = args.configfile 34 | if not configfile: 35 | configfile = resource_filename(ogcserver.__name__, 'default.conf') 36 | 37 | application = WSGIApp(configfile,args.mapfile) 38 | 39 | if __name__ == '__main__': 40 | from wsgiref.simple_server import make_server 41 | #if os.uname()[0] == 'Darwin': 42 | # host = socket.getfqdn() # yourname.local 43 | #else: 44 | # host = '0.0.0.0' 45 | host = args.bind_address or '0.0.0.0' 46 | port = args.bind_port or 8000 47 | httpd = make_server(host, port, application) 48 | print "Listening at %s:%s...." % (host,port) 49 | httpd.serve_forever() 50 | -------------------------------------------------------------------------------- /conf/fcgi_app.py: -------------------------------------------------------------------------------- 1 | from ogcserver.cgiserver import Handler 2 | from jon import fcgi 3 | 4 | class OGCServerHandler(Handler): 5 | configpath = '/path/to/ogcserver.conf' 6 | 7 | fcgi.Server({fcgi.FCGI_RESPONDER: OGCServerHandler}).run() 8 | -------------------------------------------------------------------------------- /conf/map_factory.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ogcserver.WMS import BaseWMSFactory 3 | from mapnik import Style, Layer, Map, load_map 4 | 5 | class WMSFactory(BaseWMSFactory): 6 | def __init__(self): 7 | import sys 8 | base_path, tail = os.path.split(__file__) 9 | configpath = os.path.join(base_path, 'ogcserver.conf') 10 | file_path = os.path.join(base_path, 'mapfile.xml') 11 | BaseWMSFactory.__init__(self, configpath=configpath) 12 | self.loadXML(file_path) 13 | self.finalize() 14 | -------------------------------------------------------------------------------- /demo/map.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | style 13 | 14 | world_merc.shp 15 | iso-8859-1 16 | shape 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/openlayers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WMS Test 4 | 12 | 16 | 17 | 18 | 19 | 54 | 55 | 56 | 57 |

WMS Test

58 |
59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /demo/world_merc.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/demo/world_merc.dbf -------------------------------------------------------------------------------- /demo/world_merc.index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/demo/world_merc.index -------------------------------------------------------------------------------- /demo/world_merc.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/demo/world_merc.json -------------------------------------------------------------------------------- /demo/world_merc.prj: -------------------------------------------------------------------------------- 1 | PROJCS["Google Maps Global Mercator",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mercator_2SP"],PARAMETER["standard_parallel_1",0],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1]] -------------------------------------------------------------------------------- /demo/world_merc.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/demo/world_merc.shp -------------------------------------------------------------------------------- /demo/world_merc.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/demo/world_merc.shx -------------------------------------------------------------------------------- /demo/world_merc_license.txt: -------------------------------------------------------------------------------- 1 | world_merc 2 | ========== 3 | 4 | 'world_merc.shp' is a version of TM_WORLD_BORDERS_SIMPL-0.3.shp 5 | downloaded from http://thematicmapping.org/downloads/world_borders.php. 6 | 7 | Coodinates near 180 degress longitude were clipped to faciliate reprojection 8 | to Google mercator (EPSG:900913). 9 | 10 | Details from original readme are below: 11 | 12 | ------------- 13 | 14 | TM_WORLD_BORDERS-0.1.ZIP 15 | 16 | Provided by Bjorn Sandvik, thematicmapping.org 17 | 18 | Use this dataset with care, as several of the borders are disputed. 19 | 20 | The original shapefile (world_borders.zip, 3.2 MB) was downloaded from the Mapping Hacks website: 21 | http://www.mappinghacks.com/data/ 22 | 23 | The dataset was derived by Schuyler Erle from public domain sources. 24 | Sean Gilles did some clean up and made some enhancements. 25 | 26 | 27 | COLUMN TYPE DESCRIPTION 28 | 29 | Shape Polygon Country/area border as polygon(s) 30 | FIPS String(2) FIPS 10-4 Country Code 31 | ISO2 String(2) ISO 3166-1 Alpha-2 Country Code 32 | ISO3 String(3) ISO 3166-1 Alpha-3 Country Code 33 | UN Short Integer(3) ISO 3166-1 Numeric-3 Country Code 34 | NAME String(50) Name of country/area 35 | AREA Long Integer(7) Land area, FAO Statistics (2002) 36 | POP2005 Double(10,0) Population, World Polulation Prospects (2005) 37 | REGION Short Integer(3) Macro geographical (continental region), UN Statistics 38 | SUBREGION Short Integer(3) Geogrpahical sub-region, UN Statistics 39 | LON FLOAT (7,3) Longitude 40 | LAT FLOAT (6,3) Latitude 41 | 42 | 43 | CHANGELOG VERSION 0.3 - 30 July 2008 44 | 45 | - Corrected spelling mistake (United Arab Emirates) 46 | - Corrected population number for Japan 47 | - Adjusted long/lat values for India, Italy and United Kingdom 48 | 49 | 50 | CHANGELOG VERSION 0.2 - 1 April 2008 51 | 52 | - Made new ZIP archieves. No change in dataset. 53 | 54 | 55 | CHANGELOG VERSION 0.1 - 13 March 2008 56 | 57 | - Polygons representing each country were merged into one feature 58 | - ≈land Islands was extracted from Finland 59 | - Hong Kong was extracted from China 60 | - Holy See (Vatican City) was added 61 | - Gaza Strip and West Bank was merged into "Occupied Palestinean Territory" 62 | - Saint-Barthelemy was extracted from Netherlands Antilles 63 | - Saint-Martin (Frensh part) was extracted from Guadeloupe 64 | - Svalbard and Jan Mayen was merged into "Svalbard and Jan Mayen Islands" 65 | - Timor-Leste was extracted from Indonesia 66 | - Juan De Nova Island was merged with "French Southern & Antarctic Land" 67 | - Baker Island, Howland Island, Jarvis Island, Johnston Atoll, Midway Islands 68 | and Wake Island was merged into "United States Minor Outlying Islands" 69 | - Glorioso Islands, Parcel Islands, Spartly Islands was removed 70 | (almost uninhabited and missing ISO-3611-1 code) 71 | 72 | - Added ISO-3166-1 codes (alpha-2, alpha-3, numeric-3). Source: 73 | https://www.cia.gov/library/publications/the-world-factbook/appendix/appendix-d.html 74 | http://unstats.un.org/unsd/methods/m49/m49alpha.htm 75 | http://www.fysh.org/~katie/development/geography.txt 76 | - AREA column has been replaced with data from UNdata: 77 | Land area, 1000 hectares, 2002, FAO Statistics 78 | - POPULATION column (POP2005) has been replaced with data from UNdata: 79 | Population, 2005, Medium variant, World Population Prospects: The 2006 Revision 80 | - Added region and sub-region codes from UN Statistics Division. Source: 81 | http://unstats.un.org/unsd/methods/m49/m49regin.htm 82 | - Added LAT, LONG values for each country 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/overview.txt: -------------------------------------------------------------------------------- 1 | = OGC Server = 2 | 3 | == Overview == 4 | This page dicusses all things related to supporting [http://www.opengeospatial.org/ OpenGeospatial Consortium (OGC)] server related specifications in Mapnik. 5 | 6 | As of 0.3.0, Mapnik provides an beta [http://www.opengeospatial.org/standards/wms Web Mapping Service (WMS)] compliant server, written in Python. 7 | 8 | == Features == 9 | Supports both versions [http://portal.opengeospatial.org/files/?artifact_id=1081&version=1&format=pdf 1.1.1] and [http://portal.opengeospatial.org/modules/admin/license_agreement.php?suppressHeaders=0&access_license_id=3&target=http://portal.opengeospatial.org/files/index.php?artifact_id=14416 1.3.0] of the specification. 10 | 11 | Supports all operations: 12 | 13 | * !GetCapabilities 14 | * !GetMap 15 | * !GetFeatureInfo (As of 0.4.0) 16 | 17 | Supports defining styles and layers in python and by loading an xml mapfile (as of 0.6.0) 18 | 19 | It can be configured to be run within a webserver as either: 20 | * CGI 21 | * FastCGI 22 | * WSGI 23 | * mod_python (as of 0.6.0) 24 | 25 | Or can be run as a local process using a [http://docs.python.org/library/wsgiref.html wsgiref localserver] 26 | 27 | '''See below for Sample configurations''' 28 | 29 | == Dependencies == 30 | * Python 31 | * Mapnik installed with Python Bindings 32 | * Proj4 epsg file with all needed customs projections added (ie. google mercator) 33 | * [http://www.pythonware.com/products/pil/ PIL (Python Imaging Library)] 34 | * For running as CGI or FastCGI: 35 | * [http://jonpy.sourceforge.net/ jonpy] 36 | 37 | == Installation == 38 | * The server code is automatically installed in the Python site-packages folder along with the Mapnik Python bindings. 39 | * To test installation try importing the ogcserver module within a python interpreter: 40 | 41 | >>> from mapnik import ogcserver 42 | >>> # no error means proper installation 43 | 44 | * To test that your Proj4 'epsg' file can be located choose and EPSG code your server will be exposing, say EPSG 4326 (WGS 84) or EPSG 900913 (Google Mercator), try instantiating from the Mapnik python bindings: 45 | 46 | >>> from mapnik import Projection 47 | >>> Projection('+init=epsg:4326') 48 | Projection('+init=epsg:4326') 49 | >>> Projection('+init=epsg:900913') 50 | Traceback (most recent call last): 51 | File "", line 1, in 52 | RuntimeError: failed to initialize projection with:+init=epsg:900913 53 | 54 | * In this example 4236 was found, but 900913 needs to be manually added to the 'epsg' file usually located at /usr/share/proj/epsg. On Mac OS X if you installed the PROJ framework from [http://www.kyngchaos.com/software/frameworks KyngChaos Wiki] the location will be at /Library/Frameworks/PROJ.framework/resources/proj. 55 | 56 | <900913> +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs <> 57 | 58 | == Setup == 59 | See the OgcServerSvn (Web-based view of the document available in the mapnik source code [http://trac.mapnik.org/browser/trunk/docs/ogcserver/readme.txt here]) 60 | 61 | === Sample Configurations === 62 | 63 | Usually this file is named 'wms.py': 64 | 65 | import sys 66 | from mapnik.ogcserver.wsgi import WSGIApp 67 | sys.path.append('/path/to/map_factory/') 68 | 69 | application = WSGIApp('/path/to/ogcserver.conf') 70 | 71 | * To run the WSGI as a standalone server do: 72 | 73 | # add these line to the bottom of your wsgi-based 'wms.py' 74 | 75 | if __name__ == '__main__': 76 | from wsgiref.simple_server import make_server 77 | httpd = make_server('localhost', 8000, application) 78 | print "Listening on port 8000...." 79 | httpd.serve_forever() 80 | * And then do: 81 | 82 | $ python wms.py 83 | Listening on port 8000.... # go to http://localhost:8000 in your browser 84 | 85 | '''CGI/FastCGI''' 86 | 87 | import sys 88 | from mapnik.ogcserver.cgiserver import Handler 89 | sys.path.append('/path/to/map_factory/') 90 | from jon import fcgi 91 | 92 | class OGCServerHandler(Handler): 93 | configpath = '/path/to/ogcserver.conf' 94 | 95 | fcgi.Server({fcgi.FCGI_RESPONDER: OGCServerHandler}).run() 96 | 97 | '''Mod Python''' 98 | 99 | Note that the mod_python environment keeps Python code in memory, so after making changes to worldMapServer.py or other files, an Apache reload or restart may be required. 100 | 101 | ''wms.py'' 102 | 103 | import sys 104 | from mapnik.ogcserver.modserver import ModHandler 105 | sys.path.append('/path/to/map_factory/') 106 | 107 | handler = ModHandler('/path/to/ogcserver.conf') 108 | 109 | ''Apache Configuration'' 110 | 111 | 112 | PythonPath "['/home/dane/projects/ogcserver/'] + sys.path" 113 | AddHandler mod_python .py 114 | PythonHandler wms 115 | 116 | 117 | === Plans for the future === 118 | 119 | * Test with various WMS clients 120 | * Add support for text/xml and text/html to !GetFeatureInfo (only supports text/plain in 0.4.0) 121 | * Investigate [http://wiki.osgeo.org/wiki/WMS_Tiling_Client_Recommendation WMS-C] integration 122 | 123 | === History === 124 | 125 | For a bit of history on the server development see: http://lists.berlios.de/pipermail/mapnik-devel/2006-April/000011.html -------------------------------------------------------------------------------- /docs/readme.txt: -------------------------------------------------------------------------------- 1 | # $Id: readme.txt 1186 2009-06-29 00:11:14Z dane $ 2 | 3 | Mapnik OGC Server 4 | ----------------- 5 | 6 | 7 | Introduction 8 | ------------ 9 | 10 | Mapnik provides a server package to allow the publishing of maps 11 | through the open and standard WMS interface published by the Open Geospatial 12 | Consortium (OGC). It is in implemented in Python, around the core Mapnik C++ 13 | library. 14 | 15 | This is the very first implementation of a WMS for Mapnik. Although initial 16 | testing seems to suggest it works well, there may be bugs, and it lacks some 17 | useful features. Comments, contributions, and requests for help should all be 18 | directed to the Mapnik mailing list. 19 | 20 | 21 | Features 22 | -------- 23 | 24 | - WMS 1.1.1 and 1.3.0 25 | - CGI/FastCGI, WSGI, mod_python 26 | - Supports all 3 requests: GetCapabilities, GetMap and GetFeatureInfo 27 | - JPEG/PNG output 28 | - XML/INIMAGE/BLANK error handling 29 | - Multiple named styles support 30 | - Reprojection support 31 | - Supported layer metadata: title, abstract 32 | - Ability to request all layers with LAYERS=__all__ 33 | 34 | 35 | Caveats 36 | ---------------- 37 | - GetFeatureInfo supports text/plain output only 38 | - PNG256(8-bit PNG not yet supported) 39 | - CGI/FastCGI interface needs to be able to write to tempfile.gettempdir() (most likely "/tmp") 40 | - Need to be further evaluated for thread safety 41 | 42 | 43 | Dependencies 44 | ------------ 45 | 46 | Please properly install the following before proceeding further: 47 | 48 | - Mapnik python bindings (which will also install the `ogcserver` module code) 49 | - PIL (http://www.pythonware.com/products/pil) 50 | 51 | For the CGI/FastCGI interface also install: 52 | 53 | - jonpy (http://jonpy.sourceforge.net/) 54 | 55 | 56 | Installation 57 | ------------ 58 | 59 | - The OGC Server uses the Mapnik interface to the Proj.4 library for projection support 60 | and depends on integer EPSG codes. Confirm that you have installed Proj.4 with 61 | all necessary data files (http://trac.osgeo.org/proj/wiki/FAQ) and have added any custom 62 | projections to the 'epsg' file usually located at '/usr/local/share/proj/epsg'. 63 | 64 | - Test that the server code is available and installed properly by importing it within a 65 | python interpreter:: 66 | 67 | >>> import ogcserver 68 | >>> # no error means proper installation 69 | 70 | - There is a sample python script called "wms.py" in the utils/ogcserver folder of the 71 | Mapnik source code that will work for both CGI and FastCGI operations. Where to place it 72 | will depend on your server choice and configuration and is beyond this documentation. 73 | For information on FastCGI go to http://www.fastcgi.com/. 74 | 75 | 76 | Configuring the server 77 | ---------------------- 78 | 79 | - You will need to create two simple python scripts: 80 | 81 | 1) The web-accessible python script ('wms.py') which will import the 82 | ogcserver module code and associate itself with the 'ogcserver.conf' 83 | configuration file. The code of this script will depend upon whether 84 | you deploy the server as cgi/fastcgi/wsgi/mod_python. See the Mapnik 85 | Community Wiki for examples: http://trac.mapnik.org/wiki/OgcServer and 86 | see the cgi sample in the /utils/ogcserver folder. 87 | 88 | 2) A 'map_factory' script which loads your layers and styles. Samples of this 89 | script can be found below. 90 | 91 | 92 | - Next you need to edit the ogcserver.conf file to: 93 | 94 | 1) Point to the 'map_factory' script by using the "module" parameter 95 | 96 | 2) Fill out further settings for the server. 97 | 98 | Edit the configuration file to your liking, the comments within the file will 99 | help you further. Be sure to, at the very minimum, edit the "module" 100 | parameter. The server will not work without setting it properly first. 101 | 102 | 103 | Defining Layers and Styles 104 | -------------------------- 105 | 106 | The ogcserver obviously needs layers to publish and styles for how to display those layers. 107 | 108 | You create your layers and styles in the 'map_factory' script. 109 | 110 | For now this can be done by either loading an XML mapfile inside that script using the 111 | 'loadXML()' function or by writing your layers and styles in python code, or both. 112 | 113 | If you load your layers and styles using an existing XML mapfile the 'map_factory' module 114 | should look like:: 115 | 116 | from ogcserver.WMS import BaseWMSFactory 117 | 118 | class WMSFactory(BaseWMSFactory): 119 | def __init__(self): 120 | BaseWMSFactory.__init__(self) 121 | self.loadXML('/full/path/to/mapfile.xml') 122 | self.finalize() 123 | 124 | Or if you want to define your layers and styles in pure python you might 125 | have a 'map_factory' more like:: 126 | 127 | from ogcserver.WMS import BaseWMSFactory 128 | from mapnik import * 129 | 130 | SHAPEFILE = '/path/to/world_borders.shp' 131 | PROJ4_STRING = '+init=epsg:4326' 132 | 133 | class WMSFactory(BaseWMSFactory): 134 | def __init__(self): 135 | BaseWMSFactory.__init__(self) 136 | sty,rl = Style(),Rule() 137 | poly = PolygonSymbolizer(Color('#f2eff9')) 138 | line = LineSymbolizer(Color('steelblue'),.1) 139 | rl.symbols.extend([poly,line]) 140 | sty.rules.append(rl) 141 | self.register_style('world_style',sty) 142 | lyr = Layer('world',PROJ4_STRING) 143 | lyr.datasource = Shapefile(file=SHAPEFILE) 144 | lyr.title = 'World Borders' 145 | lyr.abstract = 'Country Borders of the World' 146 | self.register_layer(lyr,'world_style',('world_style',)) 147 | self.finalize() 148 | 149 | The rules for writing this class are: 150 | 151 | - It MUST be called 'WMSFactory'. 152 | - It MUST sub-class mapnik.ogcserver.WMS.BaseWMSFactory. 153 | - The __init__ MUST call the base class. 154 | - Layers MUST be named with the first parameter to the constructor. 155 | - Layers MUST define an EPSG projection in the second parameter of the 156 | constructor. This implies that the underlying data must be in an EPSG 157 | projection already. 158 | - Style and layer names are meant for machine readability, not human. Keep 159 | them short and simple, without spaces or special characters. 160 | - For human readable info, set the title and abstract properties on the layer 161 | object. 162 | - DO NOT register styles using layer.styles.append(), instead, provide style 163 | information to the register_layer() call:: 164 | 165 | register_layer(layerobject, defaultstylename, (tuple of alternative style names,)) 166 | 167 | - No Map() object is used or needed here. 168 | - Be sure to call self.finalize() once you have registered everything! This will 169 | validate everything and let you know if there are any problems. 170 | - For a layer to be queryable via GetFeatureInfo, simply set the 'queryable' 171 | property to True:: 172 | 173 | lyr.queryable = True 174 | 175 | 176 | Paster applications 177 | ------------------- 178 | You may want to integrate your ogcserver services in a WSGI pipeline configured through PasteDeploy. 179 | You have already in this package some factories availables: 180 | 181 | -:factory: ogcserver#wms_factory (ogcserver.wsgi.ogcserver_wms_factory 182 | 183 | -:ogcserver_config: ogcserver.conf configuration file (see conf/ogcserver.conf for a sample one) 184 | -:server_module: module to get WMSFactory from 185 | -:debug: (opt) debug flag (true/false) 186 | -:fonts: (opt) system fonts dir 187 | -:maxage: (opt) proxy max age (seconds) 188 | -:example: :: 189 | 190 | [myapp] 191 | use=egg:ogcserver#mapfile 192 | mapfile=/path/to/mapfile 193 | ogcserver_config=/path/to/ogcserver.conf 194 | 195 | :factory: ogcserver#mapfile (ogcserver.wsgi.ogcserver_map_factory 196 | 197 | -:ogcserver_config: ogcserver.conf configuration file (see conf/ogcserver.conf for a sample one) 198 | -:mapfile: map file (see conf/ogcserver.conf for a sample one) 199 | -:debug: (opt) debug flag (true/false) 200 | -:fonts: (opt) system fonts dir 201 | -:maxage: (opt) proxy max age (seconds) 202 | -:example: :: 203 | 204 | [myapp] 205 | use=egg:ogcserver#wms_factory 206 | server_module=my.nice.appmaker.module 207 | ogcserver_config=/path/to/ogcserver.conf 208 | 209 | To Do 210 | ----- 211 | 212 | - Investigate moving to xml.etree.cElementTree from xml.etree. 213 | - Add some internal "caching" for performance improvements. 214 | - Switch to using C/C++ libs for image generation, instead of PIL (also 215 | requires core changes). PIL requirement will remain for INIMAGE/BLANK 216 | error handling. 217 | -------------------------------------------------------------------------------- /ogcserver/WMS.py: -------------------------------------------------------------------------------- 1 | """Interface for registering map styles and layers for availability in WMS Requests.""" 2 | 3 | import re 4 | import sys 5 | import ConfigParser 6 | from mapnik import Style, Map, load_map, load_map_from_string, Envelope, Coord 7 | 8 | from ogcserver import common 9 | from ogcserver.wms111 import ServiceHandler as ServiceHandler111 10 | from ogcserver.wms130 import ServiceHandler as ServiceHandler130 11 | from ogcserver.exceptions import OGCException, ServerConfigurationError 12 | 13 | def ServiceHandlerFactory(conf, mapfactory, onlineresource, version): 14 | 15 | if not version: 16 | version = common.Version() 17 | else: 18 | version = common.Version(version) 19 | if version >= '1.3.0': 20 | return ServiceHandler130(conf, mapfactory, onlineresource) 21 | else: 22 | return ServiceHandler111(conf, mapfactory, onlineresource) 23 | 24 | def extract_named_rules(s_obj): 25 | s = Style() 26 | s.names = [] 27 | if isinstance(s_obj,Style): 28 | for rule in s_obj.rules: 29 | if rule.name: 30 | s.rules.append(rule) 31 | if not rule.name in s.names: 32 | s.names.append(rule.name) 33 | elif isinstance(s_obj,list): 34 | for sty in s_obj: 35 | for rule in sty.rules: 36 | if rule.name: 37 | s.rules.append(rule) 38 | if not rule.name in s.names: 39 | s.names.append(rule.name) 40 | if len(s.rules): 41 | return s 42 | 43 | class BaseWMSFactory: 44 | def __init__(self, configpath=None): 45 | self.layers = {} 46 | self.ordered_layers = [] 47 | self.styles = {} 48 | self.aggregatestyles = {} 49 | self.map_attributes = {} 50 | self.map_scale = 1 51 | self.meta_styles = {} 52 | self.meta_layers = {} 53 | self.configpath = configpath 54 | self.latlonbb = None 55 | 56 | def loadXML(self, xmlfile=None, strict=False, xmlstring='', basepath=''): 57 | config = ConfigParser.SafeConfigParser() 58 | map_wms_srs = None 59 | if self.configpath: 60 | config.readfp(open(self.configpath)) 61 | 62 | if config.has_option('map', 'wms_srs'): 63 | map_wms_srs = config.get('map', 'wms_srs') 64 | 65 | tmp_map = Map(0,0) 66 | if xmlfile: 67 | load_map(tmp_map, xmlfile, strict) 68 | elif xmlstring: 69 | load_map_from_string(tmp_map, xmlstring, strict, basepath) 70 | else: 71 | raise ServerConfigurationError("Mapnik configuration XML is not specified - 'xmlfile' and 'xmlstring' variables are empty.\ 72 | Please set one of this variables to load mapnik map object.") 73 | # get the map scale 74 | if tmp_map.parameters: 75 | if tmp_map.parameters['scale']: 76 | self.map_scale = float(tmp_map.parameters['scale']) 77 | # parse map level attributes 78 | if tmp_map.background: 79 | self.map_attributes['bgcolor'] = tmp_map.background 80 | if tmp_map.buffer_size: 81 | self.map_attributes['buffer_size'] = tmp_map.buffer_size 82 | for lyr in tmp_map.layers: 83 | layer_section = 'layer_%s' % lyr.name 84 | layer_wms_srs = None 85 | if config.has_option(layer_section, 'wms_srs'): 86 | layer_wms_srs = config.get(layer_section, 'wms_srs') 87 | else: 88 | layer_wms_srs = map_wms_srs 89 | 90 | if config.has_option(layer_section, 'title'): 91 | lyr.title = config.get(layer_section, 'title') 92 | else: 93 | lyr.title = '' 94 | 95 | if config.has_option(layer_section, 'abstract'): 96 | lyr.abstract = config.get(layer_section, 'abstract') 97 | else: 98 | lyr.abstract = '' 99 | 100 | style_count = len(lyr.styles) 101 | if style_count == 0: 102 | raise ServerConfigurationError("Cannot register Layer '%s' without a style" % lyr.name) 103 | elif style_count == 1: 104 | style_obj = tmp_map.find_style(lyr.styles[0]) 105 | style_name = lyr.styles[0] 106 | 107 | meta_s = extract_named_rules(style_obj) 108 | if meta_s: 109 | self.meta_styles['%s_meta' % lyr.name] = meta_s 110 | if hasattr(lyr,'abstract'): 111 | name_ = lyr.abstract 112 | else: 113 | name_ = lyr.name 114 | meta_layer_name = '%s:%s' % (name_,'-'.join(meta_s.names)) 115 | meta_layer_name = meta_layer_name.replace(' ','_') 116 | self.meta_styles[meta_layer_name] = meta_s 117 | meta_lyr = common.copy_layer(lyr) 118 | meta_lyr.meta_style = meta_layer_name 119 | meta_lyr.name = meta_layer_name 120 | meta_lyr.wmsextrastyles = () 121 | meta_lyr.defaultstyle = meta_layer_name 122 | meta_lyr.wms_srs = layer_wms_srs 123 | self.ordered_layers.append(meta_lyr) 124 | self.meta_layers[meta_layer_name] = meta_lyr 125 | print meta_layer_name 126 | 127 | if style_name not in self.aggregatestyles.keys() and style_name not in self.styles.keys(): 128 | self.register_style(style_name, style_obj) 129 | 130 | # must copy layer here otherwise we'll segfault 131 | lyr_ = common.copy_layer(lyr) 132 | lyr_.wms_srs = layer_wms_srs 133 | self.register_layer(lyr_, style_name, extrastyles=(style_name,)) 134 | 135 | elif style_count > 1: 136 | for style_name in lyr.styles: 137 | style_obj = tmp_map.find_style(style_name) 138 | 139 | meta_s = extract_named_rules(style_obj) 140 | if meta_s: 141 | self.meta_styles['%s_meta' % lyr.name] = meta_s 142 | if hasattr(lyr,'abstract'): 143 | name_ = lyr.abstract 144 | else: 145 | name_ = lyr.name 146 | meta_layer_name = '%s:%s' % (name_,'-'.join(meta_s.names)) 147 | meta_layer_name = meta_layer_name.replace(' ','_') 148 | self.meta_styles[meta_layer_name] = meta_s 149 | meta_lyr = common.copy_layer(lyr) 150 | meta_lyr.meta_style = meta_layer_name 151 | print meta_layer_name 152 | meta_lyr.name = meta_layer_name 153 | meta_lyr.wmsextrastyles = () 154 | meta_lyr.defaultstyle = meta_layer_name 155 | meta_lyr.wms_srs = layer_wms_srs 156 | self.ordered_layers.append(meta_lyr) 157 | self.meta_layers[meta_layer_name] = meta_lyr 158 | 159 | if style_name not in self.aggregatestyles.keys() and style_name not in self.styles.keys(): 160 | self.register_style(style_name, style_obj) 161 | aggregates = tuple([sty for sty in lyr.styles]) 162 | aggregates_name = '%s_aggregates' % lyr.name 163 | self.register_aggregate_style(aggregates_name,aggregates) 164 | # must copy layer here otherwise we'll segfault 165 | lyr_ = common.copy_layer(lyr) 166 | lyr_.wms_srs = layer_wms_srs 167 | self.register_layer(lyr_, aggregates_name, extrastyles=aggregates) 168 | if 'default' in aggregates: 169 | sys.stderr.write("Warning: Multi-style layer '%s' contains a regular style named 'default'. \ 170 | This style will effectively be hidden by the 'all styles' default style for multi-style layers.\n" % lyr_.name) 171 | 172 | def register_layer(self, layer, defaultstyle, extrastyles=()): 173 | layername = layer.name 174 | if not layername: 175 | raise ServerConfigurationError('Attempted to register an unnamed layer.') 176 | if not layer.wms_srs and not re.match('^\+init=epsg:\d+$', layer.srs) and not re.match('^\+proj=.*$', layer.srs): 177 | raise ServerConfigurationError('Attempted to register a layer without an epsg projection defined.') 178 | if defaultstyle not in self.styles.keys() + self.aggregatestyles.keys(): 179 | raise ServerConfigurationError('Attempted to register a layer with an non-existent default style.') 180 | layer.wmsdefaultstyle = defaultstyle 181 | if isinstance(extrastyles, tuple): 182 | for stylename in extrastyles: 183 | if type(stylename) == type(''): 184 | if stylename not in self.styles.keys() + self.aggregatestyles.keys(): 185 | raise ServerConfigurationError('Attempted to register a layer with an non-existent extra style.') 186 | else: 187 | ServerConfigurationError('Attempted to register a layer with an invalid extra style name.') 188 | layer.wmsextrastyles = extrastyles 189 | else: 190 | raise ServerConfigurationError('Layer "%s" was passed an invalid list of extra styles. List must be a tuple of strings.' % layername) 191 | layerproj = common.Projection(layer.srs) 192 | env = layer.envelope() 193 | llp = layerproj.inverse(Coord(env.minx, env.miny)) 194 | urp = layerproj.inverse(Coord(env.maxx, env.maxy)) 195 | if self.latlonbb is None: 196 | self.latlonbb = Envelope(llp, urp) 197 | else: 198 | self.latlonbb.expand_to_include(Envelope(llp, urp)) 199 | self.ordered_layers.append(layer) 200 | self.layers[layername] = layer 201 | 202 | def register_style(self, name, style): 203 | if not name: 204 | raise ServerConfigurationError('Attempted to register a style without providing a name.') 205 | if name in self.aggregatestyles.keys() or name in self.styles.keys(): 206 | raise ServerConfigurationError("Attempted to register a style with a name already in use: '%s'" % name) 207 | if not isinstance(style, Style): 208 | raise ServerConfigurationError('Bad style object passed to register_style() for style "%s".' % name) 209 | self.styles[name] = style 210 | 211 | def register_aggregate_style(self, name, stylenames): 212 | if not name: 213 | raise ServerConfigurationError('Attempted to register an aggregate style without providing a name.') 214 | if name in self.aggregatestyles.keys() or name in self.styles.keys(): 215 | raise ServerConfigurationError('Attempted to register an aggregate style with a name already in use.') 216 | self.aggregatestyles[name] = [] 217 | for stylename in stylenames: 218 | if stylename not in self.styles.keys(): 219 | raise ServerConfigurationError('Attempted to register an aggregate style containing a style that does not exist.') 220 | self.aggregatestyles[name].append(stylename) 221 | 222 | def finalize(self): 223 | if len(self.layers) == 0: 224 | raise ServerConfigurationError('No layers defined!') 225 | if len(self.styles) == 0: 226 | raise ServerConfigurationError('No styles defined!') 227 | for layer in self.layers.values(): 228 | for style in list(layer.styles) + list(layer.wmsextrastyles): 229 | if style not in self.styles.keys() + self.aggregatestyles.keys(): 230 | raise ServerConfigurationError('Layer "%s" refers to undefined style "%s".' % (layer.name, style)) 231 | -------------------------------------------------------------------------------- /ogcserver/__init__.py: -------------------------------------------------------------------------------- 1 | """Mapnik OGC WMS Server.""" 2 | -------------------------------------------------------------------------------- /ogcserver/cgiserver.py: -------------------------------------------------------------------------------- 1 | """CGI/FastCGI handler for Mapnik OGC WMS Server. 2 | 3 | Requires 'jon' module. 4 | 5 | """ 6 | 7 | from os import environ 8 | from tempfile import gettempdir 9 | environ['PYTHON_EGG_CACHE'] = gettempdir() 10 | 11 | import sys 12 | from jon import cgi 13 | 14 | from ogcserver.common import Version 15 | from ogcserver.configparser import SafeConfigParser 16 | from ogcserver.wms111 import ExceptionHandler as ExceptionHandler111 17 | from ogcserver.wms130 import ExceptionHandler as ExceptionHandler130 18 | from ogcserver.exceptions import OGCException, ServerConfigurationError 19 | 20 | class Handler(cgi.DebugHandler): 21 | 22 | def __init__(self, home_html=None): 23 | conf = SafeConfigParser() 24 | conf.readfp(open(self.configpath)) 25 | # TODO - be able to supply in config as well 26 | self.home_html = home_html 27 | self.conf = conf 28 | if not conf.has_option_with_value('server', 'module'): 29 | raise ServerConfigurationError('The factory module is not defined in the configuration file.') 30 | try: 31 | mapfactorymodule = __import__(conf.get('server', 'module')) 32 | except ImportError: 33 | raise ServerConfigurationError('The factory module could not be loaded.') 34 | if hasattr(mapfactorymodule, 'WMSFactory'): 35 | self.mapfactory = getattr(mapfactorymodule, 'WMSFactory')() 36 | else: 37 | raise ServerConfigurationError('The factory module does not have a WMSFactory class.') 38 | if conf.has_option('server', 'debug'): 39 | self.debug = int(conf.get('server', 'debug')) 40 | else: 41 | self.debug = 0 42 | 43 | def process(self, req): 44 | base = False 45 | if not req.params: 46 | base = True 47 | 48 | reqparams = lowerparams(req.params) 49 | 50 | if self.conf.has_option_with_value('service', 'baseurl'): 51 | onlineresource = '%s' % self.conf.get('service', 'baseurl') 52 | else: 53 | # if there is no baseurl in the config file try to guess a valid one 54 | onlineresource = 'http://%s%s?' % (req.environ['HTTP_HOST'], req.environ['SCRIPT_NAME']) 55 | 56 | try: 57 | if not reqparams.has_key('request'): 58 | raise OGCException('Missing request parameter.') 59 | request = reqparams['request'] 60 | del reqparams['request'] 61 | if request == 'GetCapabilities' and not reqparams.has_key('service'): 62 | raise OGCException('Missing service parameter.') 63 | if request in ['GetMap', 'GetFeatureInfo']: 64 | service = 'WMS' 65 | else: 66 | service = reqparams['service'] 67 | if reqparams.has_key('service'): 68 | del reqparams['service'] 69 | try: 70 | ogcserver = __import__('ogcserver.' + service) 71 | except: 72 | raise OGCException('Unsupported service "%s".' % service) 73 | ServiceHandlerFactory = getattr(ogcserver, service).ServiceHandlerFactory 74 | servicehandler = ServiceHandlerFactory(self.conf, self.mapfactory, onlineresource, reqparams.get('version', None)) 75 | if reqparams.has_key('version'): 76 | del reqparams['version'] 77 | if request not in servicehandler.SERVICE_PARAMS.keys(): 78 | raise OGCException('Operation "%s" not supported.' % request, 'OperationNotSupported') 79 | ogcparams = servicehandler.processParameters(request, reqparams) 80 | try: 81 | requesthandler = getattr(servicehandler, request) 82 | except: 83 | raise OGCException('Operation "%s" not supported.' % request, 'OperationNotSupported') 84 | 85 | # stick the user agent in the request params 86 | # so that we can add ugly hacks for specific buggy clients 87 | ogcparams['HTTP_USER_AGENT'] = req.environ['HTTP_USER_AGENT'] 88 | 89 | response = requesthandler(ogcparams) 90 | except: 91 | version = reqparams.get('version', None) 92 | if not version: 93 | version = Version() 94 | else: 95 | version = Version(version) 96 | if version >= '1.3.0': 97 | eh = ExceptionHandler130(self.debug,base,self.home_html) 98 | else: 99 | eh = ExceptionHandler111(self.debug,base,self.home_html) 100 | response = eh.getresponse(reqparams) 101 | 102 | req.set_header('Content-Type', response.content_type) 103 | req.set_header('Content-Length', str(len(response.content))) 104 | req.write(response.content) 105 | 106 | def traceback(self, req): 107 | reqparams = lowerparams(req.params) 108 | version = reqparams.get('version', None) 109 | if not version: 110 | version = Version() 111 | else: 112 | version = Version(version) 113 | if version >= '1.3.0': 114 | eh = ExceptionHandler130(self.debug) 115 | else: 116 | eh = ExceptionHandler111(self.debug) 117 | response = eh.getresponse(reqparams) 118 | req.set_header('Content-Type', response.content_type) 119 | req.set_header('Content-Length', str(len(response.content))) 120 | req.write(response.content) 121 | 122 | def lowerparams(params): 123 | reqparams = {} 124 | for key, value in params.items(): 125 | reqparams[key.lower()] = value 126 | return reqparams -------------------------------------------------------------------------------- /ogcserver/common.py: -------------------------------------------------------------------------------- 1 | """Core OGCServer classes and functions.""" 2 | 3 | import re 4 | import sys 5 | import copy 6 | from sys import exc_info 7 | from StringIO import StringIO 8 | from xml.etree import ElementTree 9 | from traceback import format_exception, format_exception_only 10 | 11 | from mapnik import Map, Color, Envelope, render, Image, Layer, Style, Projection as MapnikProjection, Coord, mapnik_version 12 | 13 | try: 14 | from PIL.Image import new 15 | from PIL.ImageDraw import Draw 16 | HAS_PIL = True 17 | except ImportError: 18 | sys.stderr.write('Warning: PIL.Image not found: image based error messages will not be supported\n') 19 | HAS_PIL = False 20 | 21 | from ogcserver.exceptions import OGCException, ServerConfigurationError 22 | 23 | 24 | 25 | # from elementtree import ElementTree 26 | # ElementTree._namespace_map.update({'http://www.opengis.net/wms': 'wms', 27 | # 'http://www.opengis.net/ogc': 'ogc', 28 | # 'http://www.w3.org/1999/xlink': 'xlink', 29 | # 'http://www.w3.org/2001/XMLSchema-instance': 'xsi' 30 | # }) 31 | 32 | # TODO - need support for jpeg quality, and proper conversion into PIL formats 33 | PIL_TYPE_MAPPING = {'image/jpeg': 'jpeg', 'image/png': 'png', 'image/png8': 'png256'} 34 | 35 | class ParameterDefinition: 36 | 37 | def __init__(self, mandatory, cast, default=None, allowedvalues=None, fallback=False): 38 | """ An OGC request parameter definition. Used to describe a 39 | parameter's characteristics. 40 | 41 | @param mandatory: Is this parameter required by the request? 42 | @type mandatory: Boolean. 43 | 44 | @param default: Default value to use if one is not provided 45 | and the parameter is optional. 46 | @type default: None or any valid value. 47 | 48 | @param allowedvalues: A list of allowed values for the parameter. 49 | If a value is provided that is not in this 50 | list, an error is raised. 51 | @type allowedvalues: A python tuple of values. 52 | 53 | @param fallback: Whether the value of the parameter should fall 54 | back to the default should an illegal value be 55 | provided. 56 | @type fallback: Boolean. 57 | 58 | @return: A L{ParameterDefinition} instance. 59 | """ 60 | if mandatory not in [True, False]: 61 | raise ServerConfigurationError("Bad value for 'mandatory' parameter, must be True or False.") 62 | self.mandatory = mandatory 63 | if not callable(cast): 64 | raise ServerConfigurationError('Cast parameter definition must be callable.') 65 | self.cast = cast 66 | self.default = default 67 | if allowedvalues and type(allowedvalues) != type(()): 68 | raise ServerConfigurationError("Bad value for 'allowedvalues' parameter, must be a tuple.") 69 | self.allowedvalues = allowedvalues 70 | if fallback not in [True, False]: 71 | raise ServerConfigurationError("Bad value for 'fallback' parameter, must be True or False.") 72 | self.fallback = fallback 73 | 74 | class BaseServiceHandler: 75 | 76 | CONF_CONTACT_PERSON_PRIMARY = [ 77 | ['contactperson', 'ContactPerson', str], 78 | ['contactorganization', 'ContactOrganization', str] 79 | ] 80 | 81 | CONF_CONTACT_ADDRESS = [ 82 | ['addresstype', 'AddressType', str], 83 | ['address', 'Address', str], 84 | ['city', 'City', str], 85 | ['stateorprovince', 'StateOrProvince', str], 86 | ['postcode', 'PostCode', str], 87 | ['country', 'Country', str] 88 | ] 89 | 90 | CONF_CONTACT = [ 91 | ['contactposition', 'ContactPosition', str], 92 | ['contactvoicetelephone', 'ContactVoiceTelephone', str], 93 | ['contactelectronicmailaddress', 'ContactElectronicMailAddress', str] 94 | ] 95 | 96 | def processParameters(self, requestname, params): 97 | finalparams = {} 98 | for paramname, paramdef in self.SERVICE_PARAMS[requestname].items(): 99 | if paramname not in params.keys() and paramdef.mandatory: 100 | raise OGCException('Mandatory parameter "%s" missing from request.' % paramname) 101 | elif paramname in params.keys(): 102 | try: 103 | params[paramname] = paramdef.cast(params[paramname]) 104 | except OGCException: 105 | raise 106 | except: 107 | raise OGCException('Invalid value "%s" for parameter "%s".' % (params[paramname], paramname)) 108 | if paramdef.allowedvalues and params[paramname] not in paramdef.allowedvalues: 109 | if not paramdef.fallback: 110 | raise OGCException('Parameter "%s" has an illegal value.' % paramname) 111 | else: 112 | finalparams[paramname] = paramdef.default 113 | else: 114 | finalparams[paramname] = params[paramname] 115 | elif not paramdef.mandatory and paramdef.default: 116 | finalparams[paramname] = paramdef.default 117 | return finalparams 118 | 119 | def processServiceCapabilities(self, capetree): 120 | if len(self.conf.items('service')) > 0: 121 | servicee = capetree.find('Service') 122 | if servicee == None: 123 | servicee = capetree.find('{http://www.opengis.net/wms}Service') 124 | for item in self.CONF_SERVICE: 125 | if self.conf.has_option_with_value('service', item[0]): 126 | value = self.conf.get('service', item[0]).strip() 127 | try: 128 | item[2](value) 129 | except: 130 | raise ServerConfigurationError('Configuration parameter [%s]->%s has an invalid value: %s.' % ('service', item[0], value)) 131 | if item[0] == 'onlineresource': 132 | element = ElementTree.Element('%s' % item[1]) 133 | servicee.append(element) 134 | element.set('{http://www.w3.org/1999/xlink}href', value) 135 | element.set('{http://www.w3.org/1999/xlink}type', 'simple') 136 | elif item[0] == 'keywordlist': 137 | element = ElementTree.Element('%s' % item[1]) 138 | servicee.append(element) 139 | keywords = value.split(',') 140 | keywords = map(str.strip, keywords) 141 | for keyword in keywords: 142 | kelement = ElementTree.Element('Keyword') 143 | kelement.text = keyword 144 | element.append(kelement) 145 | else: 146 | element = ElementTree.Element('%s' % item[1]) 147 | element.text = to_unicode(value) 148 | servicee.append(element) 149 | if len(self.conf.items_with_value('contact')) > 0: 150 | element = ElementTree.Element('ContactInformation') 151 | servicee.append(element) 152 | for item in self.CONF_CONTACT: 153 | if self.conf.has_option_with_value('contact', item[0]): 154 | value = self.conf.get('contact', item[0]).strip() 155 | try: 156 | item[2](value) 157 | except: 158 | raise ServerConfigurationError('Configuration parameter [%s]->%s has an invalid value: %s.' % ('service', item[0], value)) 159 | celement = ElementTree.Element('%s' % item[1]) 160 | celement.text = value 161 | element.append(celement) 162 | for item in self.CONF_CONTACT_PERSON_PRIMARY + self.CONF_CONTACT_ADDRESS: 163 | if item in self.CONF_CONTACT_PERSON_PRIMARY: 164 | tagname = 'ContactPersonPrimary' 165 | else: 166 | tagname = 'ContactAddress' 167 | if self.conf.has_option_with_value('contact', item[0]): 168 | if element.find(tagname) == None: 169 | subelement = ElementTree.Element(tagname) 170 | element.append(subelement) 171 | value = self.conf.get('contact', item[0]).strip() 172 | try: 173 | item[2](value) 174 | except: 175 | raise ServerConfigurationError('Configuration parameter [%s]->%s has an invalid value: %s.' % ('service', item[0], value)) 176 | celement = ElementTree.Element('%s' % item[1]) 177 | celement.text = value 178 | subelement.append(celement) 179 | 180 | 181 | class Response: 182 | 183 | def __init__(self, content_type, content, status_code=200): 184 | self.content_type = content_type 185 | self.content = content 186 | self.status_code = status_code 187 | 188 | 189 | class Version: 190 | 191 | def __init__(self, version = "1.1.1"): 192 | version = version.split('.') 193 | if len(version) != 3: 194 | raise OGCException('Badly formatted version number.') 195 | try: 196 | version = map(int, version) 197 | except: 198 | raise OGCException('Badly formatted version number.') 199 | self.version = version 200 | 201 | def __repr__(self): 202 | return '%s.%s.%s' % (self.version[0], self.version[1], self.version[2]) 203 | 204 | def __cmp__(self, other): 205 | if isinstance(other, str): 206 | other = Version(other) 207 | if self.version[0] < other.version[0]: 208 | return -1 209 | elif self.version[0] > other.version[0]: 210 | return 1 211 | else: 212 | if self.version[1] < other.version[1]: 213 | return -1 214 | elif self.version[1] > other.version[1]: 215 | return 1 216 | else: 217 | if self.version[2] < other.version[2]: 218 | return -1 219 | elif self.version[2] > other.version[2]: 220 | return 1 221 | else: 222 | return 0 223 | 224 | class ListFactory: 225 | 226 | def __init__(self, cast): 227 | self.cast = cast 228 | 229 | def __call__(self, string): 230 | seq = string.split(',') 231 | return map(self.cast, seq) 232 | 233 | def ColorFactory(colorstring): 234 | if re.match('^0x[a-fA-F0-9]{6}$', colorstring): 235 | return Color(eval('0x' + colorstring[2:4]), eval('0x' + colorstring[4:6]), eval('0x' + colorstring[6:8])) 236 | else: 237 | try: 238 | return Color(colorstring) 239 | except: 240 | raise OGCException('Invalid color value. Must be of format "0xFFFFFF", or any format acceptable my mapnik.Color()') 241 | 242 | class CRS: 243 | 244 | def __init__(self, namespace, code): 245 | self.namespace = namespace.lower() 246 | self.code = int(code) 247 | self.proj = None 248 | 249 | def __repr__(self): 250 | return '%s:%s' % (self.namespace, self.code) 251 | 252 | def __eq__(self, other): 253 | if str(other) == str(self): 254 | return True 255 | return False 256 | 257 | def inverse(self, x, y): 258 | if not self.proj: 259 | self.proj = Projection('+init=%s:%s' % (self.namespace, self.code)) 260 | return self.proj.inverse(Coord(x, y)) 261 | 262 | def forward(self, x, y): 263 | if not self.proj: 264 | self.proj = Projection('+init=%s:%s' % (self.namespace, self.code)) 265 | return self.proj.forward(Coord(x, y)) 266 | 267 | class CRSFactory: 268 | 269 | def __init__(self, allowednamespaces): 270 | self.allowednamespaces = allowednamespaces 271 | 272 | def __call__(self, crsstring): 273 | if not re.match('^[A-Z]{3,5}:\d+$', crsstring): 274 | raise OGCException('Invalid format for the CRS parameter: %s' % crsstring, 'InvalidCRS') 275 | crsparts = crsstring.split(':') 276 | if crsparts[0] in self.allowednamespaces: 277 | return CRS(crsparts[0], crsparts[1]) 278 | else: 279 | raise OGCException('Invalid CRS Namespace: %s' % crsparts[0], 'InvalidCRS') 280 | 281 | def copy_layer(obj): 282 | lyr = Layer(obj.name) 283 | if hasattr(obj, 'title'): 284 | lyr.title = obj.title 285 | else: 286 | lyr.title = '' 287 | if hasattr(obj, 'abstract'): 288 | lyr.abstract = obj.abstract 289 | else: 290 | lyr.abstract = '' 291 | # only if mapnik version supports it 292 | # http://trac.mapnik.org/ticket/503 293 | if hasattr(obj, 'tolerance'): 294 | lyr.tolerance = obj.tolerance 295 | if hasattr(obj, 'toleranceunits'): 296 | lyr.toleranceunits = obj.toleranceunits 297 | lyr.srs = obj.srs 298 | if hasattr(obj, 'minzoom'): 299 | lyr.minzoom = obj.minzoom 300 | if hasattr(obj, 'maxzoom'): 301 | lyr.maxzoom = obj.maxzoom 302 | lyr.active = obj.active 303 | lyr.queryable = obj.queryable 304 | lyr.clear_label_cache = obj.clear_label_cache 305 | lyr.datasource = obj.datasource 306 | if hasattr(obj,'wmsdefaultstyle'): 307 | lyr.wmsdefaultstyle = obj.wmsdefaultstyle 308 | if hasattr(obj,'wmsextrastyles'): 309 | lyr.wmsextrastyles = obj.wmsextrastyles 310 | if hasattr(obj,'meta_style'): 311 | lyr.meta_style = obj.meta_style 312 | if hasattr(lyr, 'wms_srs'): 313 | lyr.wms_srs = obj.wms_srs 314 | return lyr 315 | 316 | class WMSBaseServiceHandler(BaseServiceHandler): 317 | 318 | def GetMap(self, params): 319 | m = self._buildMap(params) 320 | im = Image(params['width'], params['height']) 321 | map_scale = self.mapfactory.map_scale if self.mapfactory is not None else 1 322 | render(m, im, map_scale) 323 | format = PIL_TYPE_MAPPING[params['format']] 324 | if mapnik_version() >= 200300: 325 | # Mapnik 2.3 uses png8 as default, use png32 for backwards compatibility 326 | if format == 'png': 327 | format = 'png32' 328 | return Response(params['format'].replace('8',''), im.tostring(format)) 329 | 330 | def GetFeatureInfo(self, params, querymethodname='query_point'): 331 | m = self._buildMap(params) 332 | if params['info_format'] == 'text/plain': 333 | writer = TextFeatureInfo() 334 | elif params['info_format'] == 'text/xml': 335 | writer = XMLFeatureInfo() 336 | if params['query_layers'] and params['query_layers'][0] == '__all__': 337 | for layerindex, layer in enumerate(m.layers): 338 | featureset = getattr(m, querymethodname)(layerindex, params['i'], params['j']) 339 | features = featureset.features 340 | if features: 341 | writer.addlayer(layer.name) 342 | for feat in features: 343 | writer.addfeature() 344 | if mapnik_version() >= 800: 345 | for prop,value in feat.attributes.iteritems(): 346 | writer.addattribute(prop, value) 347 | else: 348 | for prop in feat.properties: 349 | writer.addattribute(prop[0], prop[1]) 350 | else: 351 | for layerindex, layername in enumerate(params['query_layers']): 352 | if layername in params['layers']: 353 | # TODO - pretty sure this is bogus, we can't pull from m.layers by the layerindex of the 354 | # 'query_layers' subset, need to pull from: 355 | # self.mapfactory.layers[layername] 356 | if m.layers[layerindex].queryable: 357 | featureset = getattr(m, querymethodname)(layerindex, params['i'], params['j']) 358 | features = featureset.features 359 | if features: 360 | writer.addlayer(m.layers[layerindex].name) 361 | for feat in features: 362 | writer.addfeature() 363 | if mapnik_version() >= 800: 364 | for prop,value in feat.attributes.iteritems(): 365 | writer.addattribute(prop, value) 366 | else: 367 | for prop in feat.properties: 368 | writer.addattribute(prop[0], prop[1]) 369 | else: 370 | raise OGCException('Requested query layer "%s" is not marked queryable.' % layername, 'LayerNotQueryable') 371 | else: 372 | raise OGCException('Requested query layer "%s" not in the LAYERS parameter.' % layername) 373 | return Response(params['info_format'], str(writer)) 374 | 375 | def _buildMap(self, params): 376 | if str(params['crs']) not in self.allowedepsgcodes: 377 | raise OGCException('Unsupported CRS "%s" requested.' % str(params['crs']).upper(), 'InvalidCRS') 378 | if params['bbox'][0] >= params['bbox'][2]: 379 | raise OGCException("BBOX values don't make sense. minx is greater than maxx.") 380 | if params['bbox'][1] >= params['bbox'][3]: 381 | raise OGCException("BBOX values don't make sense. miny is greater than maxy.") 382 | 383 | # relax this for now to allow for a set of specific layers (meta layers even) 384 | # to be used without known their styles or putting the right # of commas... 385 | 386 | #if params.has_key('styles') and len(params['styles']) != len(params['layers']): 387 | # raise OGCException('STYLES length does not match LAYERS length.') 388 | m = Map(params['width'], params['height'], '+init=%s' % params['crs']) 389 | 390 | transparent = params.get('transparent', '').lower() == 'true' 391 | 392 | # disable transparent on incompatible formats 393 | if transparent and params.get('format', '') == 'image/jpeg': 394 | transparent = False 395 | 396 | if transparent: 397 | # transparent has highest priority 398 | pass 399 | elif params.has_key('bgcolor'): 400 | # if not transparent use bgcolor in url 401 | m.background = params['bgcolor'] 402 | else: 403 | # if not bgcolor in url use map background 404 | if mapnik_version() >= 200000: 405 | bgcolor = self.mapfactory.map_attributes.get('bgcolor', None) 406 | else: 407 | bgcolor = self.mapfactory.map_attributes.get('background-color', None) 408 | 409 | if bgcolor: 410 | m.background = bgcolor 411 | else: 412 | # if not map background defined use white color 413 | m.background = Color(255, 255, 255, 255) 414 | 415 | 416 | if params.has_key('buffer_size'): 417 | if params['buffer_size']: 418 | m.buffer_size = params['buffer_size'] 419 | else: 420 | buffer_ = self.mapfactory.map_attributes.get('buffer_size') 421 | if buffer_: 422 | m.buffer_size = self.mapfactory.map_attributes['buffer_size'] 423 | 424 | # haiti spec tmp hack! show meta layers without having 425 | # to request huge string to avoid some client truncating it! 426 | if params['layers'] and params['layers'][0] in ('osm_haiti_overlay','osm_haiti_overlay_900913'): 427 | for layer_obj in self.mapfactory.ordered_layers: 428 | layer = copy_layer(layer_obj) 429 | if not hasattr(layer,'meta_style'): 430 | pass 431 | else: 432 | layer.styles.append(layer.meta_style) 433 | m.append_style(layer.meta_style, self.mapfactory.meta_styles[layer.meta_style]) 434 | m.layers.append(layer) 435 | # a non WMS spec way of requesting all layers 436 | # uses orderedlayers that preserves original ordering in XML mapfile 437 | elif params['layers'] and params['layers'][0] == '__all__': 438 | for layer_obj in self.mapfactory.ordered_layers: 439 | # if we don't copy the layer here we get 440 | # duplicate layers added to the map because the 441 | # layer is kept around and the styles "pile up"... 442 | layer = copy_layer(layer_obj) 443 | if hasattr(layer,'meta_style'): 444 | continue 445 | reqstyle = layer.wmsdefaultstyle 446 | if reqstyle in self.mapfactory.aggregatestyles.keys(): 447 | for stylename in self.mapfactory.aggregatestyles[reqstyle]: 448 | layer.styles.append(stylename) 449 | else: 450 | layer.styles.append(reqstyle) 451 | for stylename in layer.styles: 452 | if stylename in self.mapfactory.styles.keys(): 453 | m.append_style(stylename, self.mapfactory.styles[stylename]) 454 | m.layers.append(layer) 455 | else: 456 | for layerindex, layername in enumerate(params['layers']): 457 | if layername in self.mapfactory.meta_layers: 458 | layer = copy_layer(self.mapfactory.meta_layers[layername]) 459 | layer.styles.append(layername) 460 | m.append_style(layername, self.mapfactory.meta_styles[layername]) 461 | else: 462 | try: 463 | # uses unordered dict of layers 464 | # order based on params['layers'] request which 465 | # should be originally informed by order of GetCaps response 466 | layer = copy_layer(self.mapfactory.layers[layername]) 467 | except KeyError: 468 | raise OGCException('Layer "%s" not defined.' % layername, 'LayerNotDefined') 469 | try: 470 | reqstyle = params['styles'][layerindex] 471 | except IndexError: 472 | reqstyle = '' 473 | if len(layer.wmsextrastyles) > 1 and reqstyle == 'default': 474 | reqstyle = '' 475 | if reqstyle and reqstyle not in layer.wmsextrastyles: 476 | raise OGCException('Invalid style "%s" requested for layer "%s".' % (reqstyle, layername), 'StyleNotDefined') 477 | if not reqstyle: 478 | reqstyle = layer.wmsdefaultstyle 479 | if reqstyle in self.mapfactory.aggregatestyles.keys(): 480 | for stylename in self.mapfactory.aggregatestyles[reqstyle]: 481 | layer.styles.append(stylename) 482 | else: 483 | layer.styles.append(reqstyle) 484 | 485 | for stylename in layer.styles: 486 | if stylename in self.mapfactory.styles.keys(): 487 | m.append_style(stylename, self.mapfactory.styles[stylename]) 488 | else: 489 | raise ServerConfigurationError('Layer "%s" refers to non-existent style "%s".' % (layername, stylename)) 490 | 491 | m.layers.append(layer) 492 | m.zoom_to_box(Envelope(params['bbox'][0], params['bbox'][1], params['bbox'][2], params['bbox'][3])) 493 | return m 494 | 495 | class BaseExceptionHandler: 496 | 497 | def __init__(self, debug,base=False,home_html=None): 498 | self.debug = debug 499 | self.base = base 500 | self.home_html = home_html 501 | 502 | def getresponse(self, params): 503 | code = '' 504 | message = '\n' 505 | if self.base and not params: 506 | if self.home_html: 507 | message = open(self.home_html,'r').read() 508 | else: 509 | message = ''' 510 |

Welcome to the OGCServer

511 |

Ready to accept map requests...

512 |

More info

513 | ''' 514 | return self.htmlhandler('', message) 515 | excinfo = exc_info() 516 | if self.debug: 517 | messagelist = format_exception(excinfo[0], excinfo[1], excinfo[2]) 518 | else: 519 | messagelist = format_exception_only(excinfo[0], excinfo[1]) 520 | message += ''.join(messagelist) 521 | if isinstance(excinfo[1], OGCException) and len(excinfo[1].args) > 1: 522 | code = excinfo[1].args[1] 523 | exceptions = params.get('exceptions', None) 524 | if self.debug: 525 | return self.htmlhandler(code, message) 526 | if not exceptions or not self.handlers.has_key(exceptions): 527 | exceptions = self.defaulthandler 528 | return self.handlers[exceptions](self, code, message, params) 529 | 530 | def htmlhandler(self,code,message): 531 | if code: 532 | resp_text = '

OGCServer Error:

%s
\n

Traceback:

%s
\n' % (message, code) 533 | else: 534 | resp_text = message 535 | return Response('text/html', resp_text, status_code=404) 536 | 537 | def xmlhandler(self, code, message, params): 538 | ogcexcetree = copy.deepcopy(self.xmltemplate) 539 | e = ogcexcetree.find(self.xpath) 540 | e.text = message 541 | if code: 542 | e.set('code', code) 543 | return Response(self.xmlmimetype, ElementTree.tostring(ogcexcetree, pretty_print=True), status_code=404) 544 | 545 | def inimagehandler(self, code, message, params): 546 | im = new('RGBA', (int(params['width']), int(params['height']))) 547 | im.putalpha(new('1', (int(params['width']), int(params['height'])))) 548 | draw = Draw(im) 549 | for count, line in enumerate(message.strip().split('\n')): 550 | draw.text((12,15*(count+1)), line, fill='#000000') 551 | fh = StringIO() 552 | format = PIL_TYPE_MAPPING[params['format']].replace('256','') 553 | im.save(fh, format) 554 | fh.seek(0) 555 | return Response(params['format'].replace('8',''), fh.read(), status_code=404) 556 | 557 | def blankhandler(self, code, message, params): 558 | bgcolor = params.get('bgcolor', '#FFFFFF') 559 | bgcolor = bgcolor.replace('0x', '#') 560 | transparent = params.get('transparent', 'FALSE') 561 | if transparent in ('TRUE','true','True'): 562 | im = new('RGBA', (int(params['width']), int(params['height']))) 563 | im.putalpha(new('1', (int(params['width']), int(params['height'])))) 564 | else: 565 | im = new('RGBA', (int(params['width']), int(params['height'])), bgcolor) 566 | fh = StringIO() 567 | format = PIL_TYPE_MAPPING[params['format']].replace('256','') 568 | im.save(fh, format) 569 | fh.seek(0) 570 | return Response(params['format'].replace('8',''), fh.read(), status_code=404) 571 | 572 | class Projection(MapnikProjection): 573 | 574 | def epsgstring(self): 575 | return self.params().split('=')[1].upper() 576 | 577 | class TextFeatureInfo: 578 | 579 | def __init__(self): 580 | self.buffer = '' 581 | 582 | def addlayer(self, name): 583 | self.buffer += '\n[%s]\n' % name 584 | 585 | def addfeature(self): 586 | pass#self.buffer += '\n' 587 | 588 | def addattribute(self, name, value): 589 | if type(name) is str: 590 | try: 591 | name = to_unicode(name) 592 | except: 593 | # https://github.com/mapnik/mapnik/pull/1837 594 | # try the default encoding just in case source is a shape 595 | name = to_unicode(name.decode('latin1').encode('utf-8')) 596 | if not value: 597 | value = '' 598 | value = unicode(value) 599 | self.buffer += '%s=%s\n' % (name, value) 600 | 601 | def __str__(self): 602 | return self.buffer.encode('utf-8') 603 | 604 | class XMLFeatureInfo: 605 | 606 | basexml = """ 607 | 608 | 609 | """ 610 | 611 | def __init__(self): 612 | self.rootelement = ElementTree.fromstring(self.basexml) 613 | 614 | def addlayer(self, name): 615 | layer = ElementTree.Element('layer') 616 | layer.set('name', name) 617 | self.rootelement.append(layer) 618 | self.currentlayer = layer 619 | 620 | def addfeature(self): 621 | feature = ElementTree.Element('feature') 622 | self.currentlayer.append(feature) 623 | self.currentfeature = feature 624 | 625 | def addattribute(self, name, value): 626 | attribute = ElementTree.Element('attribute') 627 | attname = ElementTree.Element('name') 628 | if type(name) is str: 629 | try: 630 | name = to_unicode(name) 631 | except: 632 | # https://github.com/mapnik/mapnik/pull/1837 633 | # try the default encoding just in case source is a shape 634 | name = to_unicode(name.decode('latin1').encode('utf-8')) 635 | if not value: 636 | value = '' 637 | attname.text = name 638 | attvalue = ElementTree.Element('value') 639 | attvalue.text = unicode(value) 640 | attribute.append(attname) 641 | attribute.append(attvalue) 642 | self.currentfeature.append(attribute) 643 | 644 | def __str__(self): 645 | return '\n' + ElementTree.tostring(self.rootelement, encoding='utf-8') 646 | 647 | def to_unicode(obj, encoding='utf-8'): 648 | if isinstance(obj, basestring): 649 | if not isinstance(obj, unicode): 650 | obj = unicode(obj, encoding) 651 | return obj 652 | -------------------------------------------------------------------------------- /ogcserver/configparser.py: -------------------------------------------------------------------------------- 1 | """ Change SafeConfigParser behavior to treat options without values as 2 | non-existent. 3 | """ 4 | 5 | from ConfigParser import SafeConfigParser as OrigSafeConfigParser 6 | 7 | class SafeConfigParser(OrigSafeConfigParser): 8 | 9 | def items_with_value(self, section): 10 | finallist = [] 11 | items = self.items(section) 12 | for item in items: 13 | if item[1] != '': 14 | finallist.append(item) 15 | return finallist 16 | 17 | def has_option_with_value(self, section, option): 18 | if self.has_option(section, option): 19 | if self.get(section, option) == '': 20 | return False 21 | else: 22 | return False 23 | return True -------------------------------------------------------------------------------- /ogcserver/default.conf: -------------------------------------------------------------------------------- 1 | # server: This section contains software related configuration parameters. 2 | 3 | [server] 4 | 5 | # module: The module containing the MapFactory class. See the readme for 6 | # details. 7 | # This would be the name of the map_factory file (without extension .py) 8 | 9 | module=CHANGEME 10 | 11 | # service: This section contains service level metadata. 12 | 13 | [service] 14 | 15 | # title: The title of the server. 16 | 17 | title=Mapnik OGC Server 18 | 19 | # abstract: An abstract describing the server. 20 | 21 | abstract=This abstract describes the server and its contents. 22 | 23 | # maxwidth, maxheight: The maximum size that a map will be supplied at. 24 | # Exceeding it will raise an error in the client. 25 | 26 | maxheight=1024 27 | maxwidth=1024 28 | 29 | # allowedepsgcodes: The comma separated list of epsg codes we want the server 30 | # to support and advertise as supported in GetCapabilities. 31 | 32 | allowedepsgcodes=4326,3857 33 | 34 | # onlineresource: A service level URL most likely pointing to the web site 35 | # supporting the service for example. This is NOT the online 36 | # resource pointing to the CGI. 37 | 38 | onlineresource=http://www.mapnik.org/ 39 | 40 | # baseurl: the base url for the Capability section, used to allow reverse proxy 41 | # mode or alised servers. If not specified will be determined from the 42 | # server name and script path 43 | 44 | #baseurl=http://www.mapnik.org:8000/wms/ 45 | 46 | # fees: An explanation of the fee structure for the usage of your service, 47 | # if any. Use the reserved keyword "none" if not applicable. 48 | 49 | fees= 50 | 51 | # keywords: A comma separated list of key words. 52 | 53 | keywordlist= 54 | 55 | # accessconstraints: Plain language description of any constraints that might 56 | # apply to the usage of your service, such as hours of 57 | # operation. 58 | 59 | accessconstraints= 60 | 61 | # maxage: The content of the HTTP Cache-Control header - 62 | # the maximum age of the content in a cache, measured 63 | # in seconds. One week is 604800 seconds, the default is 64 | # 1 day. 65 | 66 | maxage=86400 67 | 68 | # contact: Contact information. Provides information to service users on who 69 | # to contact for help on or details about the service. 70 | 71 | [contact] 72 | 73 | contactperson= 74 | contactorganization= 75 | contactposition= 76 | 77 | addresstype= 78 | address= 79 | city= 80 | stateorprovince= 81 | postcode= 82 | country= 83 | 84 | contactvoicetelephone= 85 | contactelectronicmailaddress= 86 | 87 | [map] 88 | # wms_srs: Default SRS for all layers, it replaces the srs defined in the XML 89 | # It can also be overriden in each layer 90 | 91 | # wms_name: The name for the top layer, will default to __all__ if empty 92 | wms_name = __all__ 93 | 94 | # wms_title: The title for the top layer, defaults to 'OGCServer WMS Server' 95 | wms_name = OGCServer WMS Server 96 | 97 | # wms_abstract: The abstract for the top layer, defaults to 'OGCServer WMS Server' 98 | wms_abstract = OGCServer WMS Server 99 | 100 | # [layer_] Create a section to modify Layer properties 101 | # is the name attribute in the XML 102 | # wms_srs = EPSG:4326 Set Layer SRS overriding Layers XML srs and wms_srs defined in the [map] section 103 | # title = Layer Title 104 | # abstract = Layer description 105 | -------------------------------------------------------------------------------- /ogcserver/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom OGCServer Exceptions""" 2 | 3 | class OGCException(Exception): 4 | pass 5 | 6 | class ServerConfigurationError(Exception): 7 | pass -------------------------------------------------------------------------------- /ogcserver/modserver.py: -------------------------------------------------------------------------------- 1 | """Mod_python handler for Mapnik OGC WMS Server.""" 2 | 3 | import sys 4 | from mod_python import apache, util 5 | 6 | from ogcserver.common import Version 7 | from ogcserver.configparser import SafeConfigParser 8 | from ogcserver.wms111 import ExceptionHandler as ExceptionHandler111 9 | from ogcserver.wms130 import ExceptionHandler as ExceptionHandler130 10 | from ogcserver.exceptions import OGCException, ServerConfigurationError 11 | 12 | 13 | class ModHandler(object): 14 | def __init__(self, configpath): 15 | conf = SafeConfigParser() 16 | conf.readfp(open(configpath)) 17 | self.conf = conf 18 | if not conf.has_option_with_value('server', 'module'): 19 | raise ServerConfigurationError('The factory module is not defined in the configuration file.') 20 | try: 21 | mapfactorymodule = __import__(conf.get('server', 'module')) 22 | except ImportError: 23 | raise ServerConfigurationError('The factory module could not be loaded.') 24 | if hasattr(mapfactorymodule, 'WMSFactory'): 25 | self.mapfactory = getattr(mapfactorymodule, 'WMSFactory')() 26 | else: 27 | raise ServerConfigurationError('The factory module does not have a WMSFactory class.') 28 | if conf.has_option('server', 'debug'): 29 | self.debug = int(conf.get('server', 'debug')) 30 | else: 31 | self.debug = 0 32 | if self.conf.has_option_with_value('server', 'maxage'): 33 | self.max_age = 'max-age=%d' % self.conf.get('server', 'maxage') 34 | else: 35 | self.max_age = None 36 | 37 | def __call__(self, apacheReq): 38 | try: 39 | reqparams = util.FieldStorage(apacheReq,keep_blank_values=1) 40 | if not reqparams: 41 | eh = ExceptionHandler130(self.debug) 42 | response = eh.getresponse(reqparams) 43 | apacheReq.content_type = response.content_type 44 | else: 45 | reqparams = lowerparams(reqparams) 46 | port = apacheReq.connection.local_addr[1] 47 | onlineresource = 'http://%s:%s%s?' % (apacheReq.hostname, port, apacheReq.subprocess_env['SCRIPT_NAME']) 48 | if not reqparams.has_key('request'): 49 | raise OGCException('Missing Request parameter.') 50 | request = reqparams['request'] 51 | del reqparams['request'] 52 | if request == 'GetCapabilities' and not reqparams.has_key('service'): 53 | raise OGCException('Missing service parameter.') 54 | if request in ['GetMap', 'GetFeatureInfo']: 55 | service = 'WMS' 56 | else: 57 | service = reqparams['service'] 58 | if reqparams.has_key('service'): 59 | del reqparams['service'] 60 | try: 61 | ogcserver = __import__('ogcserver.' + service) 62 | except: 63 | raise OGCException('Unsupported service "%s".' % service) 64 | ServiceHandlerFactory = getattr(ogcserver, service).ServiceHandlerFactory 65 | servicehandler = ServiceHandlerFactory(self.conf, self.mapfactory, onlineresource, reqparams.get('version', None)) 66 | if reqparams.has_key('version'): 67 | del reqparams['version'] 68 | if request not in servicehandler.SERVICE_PARAMS.keys(): 69 | raise OGCException('Operation "%s" not supported.' % request, 'OperationNotSupported') 70 | 71 | # Get parameters and pass to WMSFactory in custom "setup" method 72 | ogcparams = servicehandler.processParameters(request, reqparams) 73 | try: 74 | requesthandler = getattr(servicehandler, request) 75 | except: 76 | raise OGCException('Operation "%s" not supported.' % request, 'OperationNotSupported') 77 | 78 | response = requesthandler(ogcparams) 79 | apacheReq.content_type = response.content_type 80 | apacheReq.status = apache.HTTP_OK 81 | except Exception, E: 82 | return self.traceback(apacheReq,E) 83 | 84 | if self.max_age: 85 | apacheReq.headers_out.add('Cache-Control', max_age) 86 | apacheReq.headers_out.add('Content-Length', str(len(response.content))) 87 | apacheReq.send_http_header() 88 | apacheReq.write(response.content) 89 | return apache.OK 90 | 91 | def traceback(self, apacheReq,E): 92 | reqparams = lowerparams(util.FieldStorage(apacheReq)) 93 | version = reqparams.get('version', None) 94 | if not version: 95 | version = Version() 96 | else: 97 | version = Version(version) 98 | if version >= '1.3.0': 99 | eh = ExceptionHandler130(self.debug) 100 | else: 101 | eh = ExceptionHandler111(self.debug) 102 | response = eh.getresponse(reqparams) 103 | apacheReq.content_type = response.content_type 104 | apacheReq.headers_out.add('Content-Length', str(len(response.content))) 105 | apacheReq.send_http_header() 106 | apacheReq.write(response.content) 107 | return apache.OK 108 | 109 | def lowerparams(params): 110 | reqparams = {} 111 | for key, value in params.items(): 112 | reqparams[key.lower()] = value 113 | return reqparams 114 | -------------------------------------------------------------------------------- /ogcserver/wms111.py: -------------------------------------------------------------------------------- 1 | """WMS 1.1.1 compliant GetCapabilities, GetMap, GetFeatureInfo, and Exceptions interface.""" 2 | 3 | from mapnik import Coord 4 | 5 | from xml.etree import ElementTree 6 | ElementTree.register_namespace('', "http://www.opengis.net/wms") 7 | ElementTree.register_namespace('xlink', "http://www.w3.org/1999/xlink") 8 | 9 | from ogcserver.common import ParameterDefinition, Response, Version, ListFactory, \ 10 | ColorFactory, CRSFactory, WMSBaseServiceHandler, CRS, \ 11 | BaseExceptionHandler, Projection, to_unicode 12 | from ogcserver.exceptions import OGCException, ServerConfigurationError 13 | 14 | 15 | class ServiceHandler(WMSBaseServiceHandler): 16 | 17 | SERVICE_PARAMS = { 18 | 'GetCapabilities': { 19 | 'updatesequence': ParameterDefinition(False, str) 20 | }, 21 | 'GetMap': { 22 | 'layers': ParameterDefinition(True, ListFactory(str)), 23 | 'styles': ParameterDefinition(True, ListFactory(str)), 24 | 'srs': ParameterDefinition(True, CRSFactory(['EPSG'])), 25 | 'bbox': ParameterDefinition(True, ListFactory(float)), 26 | 'width': ParameterDefinition(True, int), 27 | 'height': ParameterDefinition(True, int), 28 | 'format': ParameterDefinition(True, str, allowedvalues=('image/png','image/png8', 'image/jpeg')), 29 | 'transparent': ParameterDefinition(False, str, 'FALSE', ('TRUE', 'FALSE','true','True','false','False')), 30 | 'bgcolor': ParameterDefinition(False, ColorFactory, None), 31 | 'exceptions': ParameterDefinition(False, str, 'application/vnd.ogc.se_xml', ('application/vnd.ogc.se_xml', 'application/vnd.ogc.se_inimage', 'application/vnd.ogc.se_blank','text/html'),True) 32 | }, 33 | 'GetFeatureInfo': { 34 | 'layers': ParameterDefinition(True, ListFactory(str)), 35 | 'styles': ParameterDefinition(False, ListFactory(str)), 36 | 'srs': ParameterDefinition(True, CRSFactory(['EPSG'])), 37 | 'bbox': ParameterDefinition(True, ListFactory(float)), 38 | 'width': ParameterDefinition(True, int), 39 | 'height': ParameterDefinition(True, int), 40 | 'format': ParameterDefinition(False, str, allowedvalues=('image/png', 'image/jpeg')), 41 | 'transparent': ParameterDefinition(False, str, 'FALSE', ('TRUE', 'FALSE','true','True','false','False')), 42 | 'bgcolor': ParameterDefinition(False, ColorFactory, ColorFactory('0xFFFFFF')), 43 | 'exceptions': ParameterDefinition(False, str, 'application/vnd.ogc.se_xml', ('application/vnd.ogc.se_xml', 'application/vnd.ogc.se_inimage', 'application/vnd.ogc.se_blank','text/html'),True), 44 | 'query_layers': ParameterDefinition(True, ListFactory(str)), 45 | 'info_format': ParameterDefinition(True, str, allowedvalues=('text/plain', 'text/xml')), 46 | 'feature_count': ParameterDefinition(False, int, 1), 47 | 'x': ParameterDefinition(True, int), 48 | 'y': ParameterDefinition(True, int) 49 | } 50 | } 51 | 52 | CONF_SERVICE = [ 53 | ['title', 'Title', str], 54 | ['abstract', 'Abstract', str], 55 | ['onlineresource', 'OnlineResource', str], 56 | ['fees', 'Fees', str], 57 | ['accessconstraints', 'AccessConstraints', str], 58 | ['keywordlist', 'KeywordList', str] 59 | ] 60 | 61 | capabilitiesxmltemplate = """ 62 | 63 | 64 | 65 | OGC:WMS 66 | 67 | 68 | 69 | 70 | application/vnd.ogc.wms_xml 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | image/png 81 | image/png8 82 | image/jpeg 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | text/plain 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | application/vnd.ogc.se_xml 104 | application/vnd.ogc.se_inimage 105 | application/vnd.ogc.se_blank 106 | text/html 107 | 108 | 109 | 110 | 111 | 112 | """ 113 | 114 | def __init__(self, conf, mapfactory, opsonlineresource): 115 | self.conf = conf 116 | self.mapfactory = mapfactory 117 | self.opsonlineresource = opsonlineresource 118 | if self.conf.has_option('service', 'allowedepsgcodes'): 119 | self.allowedepsgcodes = map(lambda code: 'epsg:%s' % code, self.conf.get('service', 'allowedepsgcodes').split(',')) 120 | else: 121 | raise ServerConfigurationError('Allowed EPSG codes not properly configured.') 122 | self.capabilities = None 123 | 124 | def GetCapabilities(self, params): 125 | if not self.capabilities: 126 | capetree = ElementTree.fromstring(self.capabilitiesxmltemplate) 127 | 128 | elements = capetree.findall('Capability//OnlineResource') 129 | for element in elements: 130 | element.set('xlink:href', self.opsonlineresource) 131 | 132 | self.processServiceCapabilities(capetree) 133 | 134 | rootlayerelem = capetree.find('Capability/Layer') 135 | 136 | rootlayername = ElementTree.Element('Name') 137 | if self.conf.has_option('map', 'wms_name'): 138 | rootlayername.text = to_unicode(self.conf.get('map', 'wms_name')) 139 | else: 140 | rootlayername.text = '__all__' 141 | rootlayerelem.append(rootlayername) 142 | 143 | rootlayertitle = ElementTree.Element('Title') 144 | if self.conf.has_option('map', 'wms_title'): 145 | rootlayertitle.text = to_unicode(self.conf.get('map', 'wms_title')) 146 | else: 147 | rootlayertitle.text = 'OGCServer WMS Server' 148 | rootlayerelem.append(rootlayertitle) 149 | 150 | rootlayerabstract = ElementTree.Element('Abstract') 151 | if self.conf.has_option('map', 'wms_abstract'): 152 | rootlayerabstract.text = to_unicode(self.conf.get('map', 'wms_abstract')) 153 | else: 154 | rootlayerabstract.text = 'OGCServer WMS Server' 155 | rootlayerelem.append(rootlayerabstract) 156 | 157 | latlonbb = ElementTree.Element('LatLonBoundingBox') 158 | latlonbb.set('minx', str(self.mapfactory.latlonbb.minx)) 159 | latlonbb.set('miny', str(self.mapfactory.latlonbb.miny)) 160 | latlonbb.set('maxx', str(self.mapfactory.latlonbb.maxx)) 161 | latlonbb.set('maxy', str(self.mapfactory.latlonbb.maxy)) 162 | rootlayerelem.append(latlonbb) 163 | 164 | for epsgcode in self.allowedepsgcodes: 165 | rootlayercrs = ElementTree.Element('SRS') 166 | rootlayercrs.text = epsgcode.upper() 167 | rootlayerelem.append(rootlayercrs) 168 | 169 | for epsgcode in self.allowedepsgcodes: 170 | rootbbox = ElementTree.Element('BoundingBox') 171 | rootbbox.set('SRS', epsgcode.upper()) 172 | proj = Projection('+init='+epsgcode) 173 | bb = self.mapfactory.latlonbb 174 | minCoord = Coord(bb.minx, bb.miny).forward(proj) 175 | maxCoord = Coord(bb.maxx, bb.maxy).forward(proj) 176 | rootbbox.set('minx', str(minCoord.x)) 177 | rootbbox.set('miny', str(minCoord.y)) 178 | rootbbox.set('maxx', str(maxCoord.x)) 179 | rootbbox.set('maxy', str(maxCoord.y)) 180 | rootlayerelem.append(rootbbox) 181 | 182 | for layer in self.mapfactory.ordered_layers: 183 | layerproj = Projection(layer.srs) 184 | layername = ElementTree.Element('Name') 185 | layername.text = to_unicode(layer.name) 186 | env = layer.envelope() 187 | llp = layerproj.inverse(Coord(env.minx, env.miny)) 188 | urp = layerproj.inverse(Coord(env.maxx, env.maxy)) 189 | latlonbb = ElementTree.Element('LatLonBoundingBox') 190 | latlonbb.set('minx', str(llp.x)) 191 | latlonbb.set('miny', str(llp.y)) 192 | latlonbb.set('maxx', str(urp.x)) 193 | latlonbb.set('maxy', str(urp.y)) 194 | layerbbox = ElementTree.Element('BoundingBox') 195 | if layer.wms_srs: 196 | layerbbox.set('SRS', layer.wms_srs) 197 | else: 198 | layerbbox.set('SRS', layerproj.epsgstring()) 199 | layerbbox.set('minx', str(env.minx)) 200 | layerbbox.set('miny', str(env.miny)) 201 | layerbbox.set('maxx', str(env.maxx)) 202 | layerbbox.set('maxy', str(env.maxy)) 203 | layere = ElementTree.Element('Layer') 204 | layere.append(layername) 205 | layertitle = ElementTree.Element('Title') 206 | if hasattr(layer,'title'): 207 | layertitle.text = to_unicode(layer.title) 208 | if layertitle.text == '': 209 | layertitle.text = to_unicode(layer.name) 210 | else: 211 | layertitle.text = to_unicode(layer.name) 212 | layere.append(layertitle) 213 | layerabstract = ElementTree.Element('Abstract') 214 | if hasattr(layer,'abstract'): 215 | layerabstract.text = to_unicode(layer.abstract) 216 | else: 217 | layerabstract.text = 'no abstract' 218 | layere.append(layerabstract) 219 | if layer.queryable: 220 | layere.set('queryable', '1') 221 | layere.append(latlonbb) 222 | layere.append(layerbbox) 223 | style_count = len(layer.wmsextrastyles) 224 | if style_count > 0: 225 | extrastyles = layer.wmsextrastyles 226 | if style_count > 1: 227 | extrastyles = ['default'] + [x for x in extrastyles if x != 'default'] 228 | for extrastyle in extrastyles: 229 | style = ElementTree.Element('Style') 230 | stylename = ElementTree.Element('Name') 231 | stylename.text = to_unicode(extrastyle) 232 | styletitle = ElementTree.Element('Title') 233 | styletitle.text = to_unicode(extrastyle) 234 | style.append(stylename) 235 | style.append(styletitle) 236 | if style_count > 1 and extrastyle == 'default': 237 | styleabstract = ElementTree.Element('Abstract') 238 | styleabstract.text = to_unicode('This layer\'s default style that combines all its other named styles.') 239 | style.append(styleabstract) 240 | layere.append(style) 241 | rootlayerelem.append(layere) 242 | self.capabilities = ElementTree.tostring(capetree,encoding='UTF-8') 243 | response = Response('application/vnd.ogc.wms_xml', self.capabilities) 244 | return response 245 | 246 | def GetMap(self, params): 247 | params['crs'] = params['srs'] 248 | return WMSBaseServiceHandler.GetMap(self, params) 249 | 250 | def GetFeatureInfo(self, params): 251 | params['crs'] = params['srs'] 252 | params['i'] = params['x'] 253 | params['j'] = params['y'] 254 | return WMSBaseServiceHandler.GetFeatureInfo(self, params, 'query_map_point') 255 | 256 | class ExceptionHandler(BaseExceptionHandler): 257 | 258 | xmlmimetype = "application/vnd.ogc.se_xml" 259 | 260 | xmltemplate = ElementTree.fromstring(""" 261 | 262 | 263 | 264 | 265 | """) 266 | 267 | xpath = 'ServiceException' 268 | 269 | handlers = {'application/vnd.ogc.se_xml': BaseExceptionHandler.xmlhandler, 270 | 'application/vnd.ogc.se_inimage': BaseExceptionHandler.inimagehandler, 271 | 'application/vnd.ogc.se_blank': BaseExceptionHandler.blankhandler, 272 | 'text/html': BaseExceptionHandler.htmlhandler} 273 | 274 | defaulthandler = 'application/vnd.ogc.se_xml' 275 | -------------------------------------------------------------------------------- /ogcserver/wms130.py: -------------------------------------------------------------------------------- 1 | """WMS 1.3.0 compliant GetCapabilities, GetMap, GetFeatureInfo, and Exceptions interface.""" 2 | 3 | from mapnik import Coord 4 | 5 | from xml.etree import ElementTree 6 | ElementTree.register_namespace('', "http://www.opengis.net/wms") 7 | ElementTree.register_namespace('xlink', "http://www.w3.org/1999/xlink") 8 | 9 | from ogcserver.common import ParameterDefinition, Response, Version, ListFactory, \ 10 | ColorFactory, CRSFactory, CRS, WMSBaseServiceHandler, \ 11 | BaseExceptionHandler, Projection, Envelope, to_unicode 12 | from ogcserver.exceptions import OGCException, ServerConfigurationError 13 | 14 | class ServiceHandler(WMSBaseServiceHandler): 15 | 16 | SERVICE_PARAMS = { 17 | 'GetCapabilities': { 18 | 'format': ParameterDefinition(False, str, 'text/xml', ('text/xml',)), 19 | 'updatesequence': ParameterDefinition(False, str) 20 | }, 21 | 'GetMap': { 22 | 'layers': ParameterDefinition(True, ListFactory(str)), 23 | 'styles': ParameterDefinition(True, ListFactory(str)), 24 | 'crs': ParameterDefinition(True, CRSFactory(['EPSG'])), 25 | 'bbox': ParameterDefinition(True, ListFactory(float)), 26 | 'width': ParameterDefinition(True, int), 27 | 'height': ParameterDefinition(True, int), 28 | 'format': ParameterDefinition(True, str, allowedvalues=('image/png','image/png8', 'image/jpeg')), 29 | 'transparent': ParameterDefinition(False, str, 'FALSE', ('TRUE', 'FALSE','true','True','false','False')), 30 | 'bgcolor': ParameterDefinition(False, ColorFactory, None), 31 | 'exceptions': ParameterDefinition(False, str, 'XML', ('XML', 'INIMAGE', 'BLANK','HTML'),True), 32 | }, 33 | 'GetFeatureInfo': { 34 | 'layers': ParameterDefinition(True, ListFactory(str)), 35 | 'styles': ParameterDefinition(False, ListFactory(str)), 36 | 'crs': ParameterDefinition(True, CRSFactory(['EPSG'])), 37 | 'bbox': ParameterDefinition(True, ListFactory(float)), 38 | 'width': ParameterDefinition(True, int), 39 | 'height': ParameterDefinition(True, int), 40 | 'format': ParameterDefinition(False, str, allowedvalues=('image/png', 'image/jpeg')), 41 | 'transparent': ParameterDefinition(False, str, 'FALSE', ('TRUE', 'FALSE','true','True','false','False')), 42 | 'bgcolor': ParameterDefinition(False, ColorFactory, ColorFactory('0xFFFFFF')), 43 | 'exceptions': ParameterDefinition(False, str, 'XML', ('XML', 'INIMAGE', 'BLANK','HTML'),True), 44 | 'query_layers': ParameterDefinition(True, ListFactory(str)), 45 | 'info_format': ParameterDefinition(True, str, allowedvalues=('text/plain', 'text/xml')), 46 | 'feature_count': ParameterDefinition(False, int, 1), 47 | 'i': ParameterDefinition(False, float), 48 | 'j': ParameterDefinition(False, float), 49 | 'y': ParameterDefinition(False, float), 50 | 'x': ParameterDefinition(False, float) 51 | } 52 | } 53 | 54 | CONF_SERVICE = [ 55 | ['title', 'Title', str], 56 | ['abstract', 'Abstract', str], 57 | ['onlineresource', 'OnlineResource', str], 58 | ['fees', 'Fees', str], 59 | ['accessconstraints', 'AccessConstraints', str], 60 | ['layerlimit', 'LayerLimit', int], 61 | ['maxwidth', 'MaxWidth', int], 62 | ['maxheight', 'MaxHeight', int], 63 | ['keywordlist', 'KeywordList', str] 64 | ] 65 | 66 | capabilitiesxmltemplate = """ 67 | 71 | 72 | WMS 73 | 74 | 75 | 76 | 77 | text/xml 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | image/png 88 | image/png8 89 | image/jpeg 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | text/plain 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | XML 111 | INIMAGE 112 | BLANK 113 | HTML 114 | 115 | 116 | 117 | 118 | 119 | """ 120 | 121 | def __init__(self, conf, mapfactory, opsonlineresource): 122 | self.conf = conf 123 | self.mapfactory = mapfactory 124 | self.opsonlineresource = opsonlineresource 125 | if self.conf.has_option('service', 'allowedepsgcodes'): 126 | self.allowedepsgcodes = map(lambda code: 'epsg:%s' % code, self.conf.get('service', 'allowedepsgcodes').split(',')) 127 | else: 128 | raise ServerConfigurationError('Allowed EPSG codes not properly configured.') 129 | self.capabilities = None 130 | 131 | def GetCapabilities(self, params): 132 | if not self.capabilities: 133 | capetree = ElementTree.fromstring(self.capabilitiesxmltemplate) 134 | 135 | elements = capetree.findall('{http://www.opengis.net/wms}Capability//{http://www.opengis.net/wms}OnlineResource') 136 | for element in elements: 137 | element.set('xlink:href', self.opsonlineresource) 138 | 139 | self.processServiceCapabilities(capetree) 140 | 141 | rootlayerelem = capetree.find('{http://www.opengis.net/wms}Capability/{http://www.opengis.net/wms}Layer') 142 | 143 | rootlayername = ElementTree.Element('{http://www.opengis.net/wms}Name') 144 | if self.conf.has_option('map', 'wms_name'): 145 | rootlayername.text = to_unicode(self.conf.get('map', 'wms_name')) 146 | else: 147 | rootlayername.text = '__all__' 148 | rootlayerelem.append(rootlayername) 149 | 150 | rootlayertitle = ElementTree.Element('{http://www.opengis.net/wms}Title') 151 | if self.conf.has_option('map', 'wms_title'): 152 | rootlayertitle.text = to_unicode(self.conf.get('map', 'wms_title')) 153 | else: 154 | rootlayertitle.text = 'OGCServer WMS Server' 155 | rootlayerelem.append(rootlayertitle) 156 | 157 | rootlayerabstract = ElementTree.Element('{http://www.opengis.net/wms}Abstract') 158 | if self.conf.has_option('map', 'wms_abstract'): 159 | rootlayerabstract.text = to_unicode(self.conf.get('map', 'wms_abstract')) 160 | else: 161 | rootlayerabstract.text = 'OGCServer WMS Server' 162 | rootlayerelem.append(rootlayerabstract) 163 | 164 | layerexgbb = ElementTree.Element('{http://www.opengis.net/wms}EX_GeographicBoundingBox') 165 | exgbb_wbl = ElementTree.Element('{http://www.opengis.net/wms}westBoundLongitude') 166 | exgbb_wbl.text = str(self.mapfactory.latlonbb.minx) 167 | layerexgbb.append(exgbb_wbl) 168 | exgbb_ebl = ElementTree.Element('{http://www.opengis.net/wms}eastBoundLongitude') 169 | exgbb_ebl.text = str(self.mapfactory.latlonbb.maxx) 170 | layerexgbb.append(exgbb_ebl) 171 | exgbb_sbl = ElementTree.Element('{http://www.opengis.net/wms}southBoundLatitude') 172 | exgbb_sbl.text = str(self.mapfactory.latlonbb.miny) 173 | layerexgbb.append(exgbb_sbl) 174 | exgbb_nbl = ElementTree.Element('{http://www.opengis.net/wms}northBoundLatitude') 175 | exgbb_nbl.text = str(self.mapfactory.latlonbb.maxy) 176 | layerexgbb.append(exgbb_nbl) 177 | rootlayerelem.append(layerexgbb) 178 | 179 | for epsgcode in self.allowedepsgcodes: 180 | rootlayercrs = ElementTree.Element('{http://www.opengis.net/wms}CRS') 181 | rootlayercrs.text = epsgcode.upper() 182 | rootlayerelem.append(rootlayercrs) 183 | 184 | for layer in self.mapfactory.ordered_layers: 185 | layerproj = Projection(layer.srs) 186 | layername = ElementTree.Element('{http://www.opengis.net/wms}Name') 187 | layername.text = to_unicode(layer.name) 188 | env = layer.envelope() 189 | layerexgbb = ElementTree.Element('{http://www.opengis.net/wms}EX_GeographicBoundingBox') 190 | ll = layerproj.inverse(Coord(env.minx, env.miny)) 191 | ur = layerproj.inverse(Coord(env.maxx, env.maxy)) 192 | exgbb_wbl = ElementTree.Element('{http://www.opengis.net/wms}westBoundLongitude') 193 | exgbb_wbl.text = str(ll.x) 194 | layerexgbb.append(exgbb_wbl) 195 | exgbb_ebl = ElementTree.Element('{http://www.opengis.net/wms}eastBoundLongitude') 196 | exgbb_ebl.text = str(ur.x) 197 | layerexgbb.append(exgbb_ebl) 198 | exgbb_sbl = ElementTree.Element('{http://www.opengis.net/wms}southBoundLatitude') 199 | exgbb_sbl.text = str(ll.y) 200 | layerexgbb.append(exgbb_sbl) 201 | exgbb_nbl = ElementTree.Element('{http://www.opengis.net/wms}northBoundLatitude') 202 | exgbb_nbl.text = str(ur.y) 203 | layerexgbb.append(exgbb_nbl) 204 | layerbbox = ElementTree.Element('{http://www.opengis.net/wms}BoundingBox') 205 | if layer.wms_srs: 206 | layerbbox.set('CRS', layer.wms_srs) 207 | else: 208 | layerbbox.set('CRS', layerproj.epsgstring()) 209 | layerbbox.set('minx', str(env.minx)) 210 | layerbbox.set('miny', str(env.miny)) 211 | layerbbox.set('maxx', str(env.maxx)) 212 | layerbbox.set('maxy', str(env.maxy)) 213 | layere = ElementTree.Element('{http://www.opengis.net/wms}Layer') 214 | layere.append(layername) 215 | layertitle = ElementTree.Element('{http://www.opengis.net/wms}Title') 216 | if hasattr(layer,'title'): 217 | layertitle.text = to_unicode(layer.title) 218 | if layertitle.text == '': 219 | layertitle.text = to_unicode(layer.name) 220 | else: 221 | layertitle.text = to_unicode(layer.name) 222 | layere.append(layertitle) 223 | layerabstract = ElementTree.Element('{http://www.opengis.net/wms}Abstract') 224 | if hasattr(layer,'abstract'): 225 | layerabstract.text = to_unicode(layer.abstract) 226 | else: 227 | layerabstract.text = 'no abstract' 228 | layere.append(layerabstract) 229 | if layer.queryable: 230 | layere.set('queryable', '1') 231 | layere.append(layerexgbb) 232 | layere.append(layerbbox) 233 | style_count = len(layer.wmsextrastyles) 234 | if style_count > 0: 235 | extrastyles = layer.wmsextrastyles 236 | if style_count > 1: 237 | extrastyles = ['default'] + [x for x in extrastyles if x != 'default'] 238 | for extrastyle in extrastyles: 239 | style = ElementTree.Element('{http://www.opengis.net/wms}Style') 240 | stylename = ElementTree.Element('{http://www.opengis.net/wms}Name') 241 | stylename.text = to_unicode(extrastyle) 242 | styletitle = ElementTree.Element('{http://www.opengis.net/wms}Title') 243 | styletitle.text = to_unicode(extrastyle) 244 | style.append(stylename) 245 | style.append(styletitle) 246 | if style_count > 1 and extrastyle == 'default': 247 | styleabstract = ElementTree.Element('{http://www.opengis.net/wms}Abstract') 248 | styleabstract.text = to_unicode('This layer\'s default style that combines all its other named styles.') 249 | style.append(styleabstract) 250 | layere.append(style) 251 | rootlayerelem.append(layere) 252 | self.capabilities = ElementTree.tostring(capetree,encoding='UTF-8') 253 | response = Response('text/xml', self.capabilities) 254 | return response 255 | 256 | def GetMap(self, params): 257 | if params['width'] > int(self.conf.get('service', 'maxwidth')) or params['height'] > int(self.conf.get('service', 'maxheight')): 258 | raise OGCException('Requested map size exceeds limits set by this server.') 259 | return WMSBaseServiceHandler.GetMap(self, params) 260 | 261 | def GetFeatureInfo(self, params): 262 | # support for QGIS 1.3.0 GetFeatInfo... 263 | if not params.get('i') and not params.get('j'): 264 | params['i'] = params.get('x',params.get('X')) 265 | params['j'] = params.get('y',params.get('Y')) 266 | # support 1.1.1 request that end up using 1.3.0 impl 267 | # because the version is not included in GetMap 268 | # ArcGIS 9.2 for example makes 1.1.1 GetCaps request 269 | # but leaves version out of GetMap 270 | if not params.get('crs') and params.get('srs'): 271 | params['crs'] = params.get('srs') 272 | return WMSBaseServiceHandler.GetFeatureInfo(self, params, 'query_map_point') 273 | 274 | def _buildMap(self, params): 275 | """ Override _buildMap method to handle reverse axis ordering in WMS 1.3.0. 276 | 277 | More info: http://mapserver.org/development/rfc/ms-rfc-30.html 278 | http://trac.osgeo.org/mapserver/changeset/10459 279 | 280 | 'when using epsg code >=4000 and <5000 will be assumed to have a reversed axes.' 281 | 282 | """ 283 | # Call superclass method 284 | m = WMSBaseServiceHandler._buildMap(self, params) 285 | # for range of epsg codes reverse axis as per 1.3.0 spec 286 | if params['crs'].code >= 4000 and params['crs'].code < 5000: 287 | bbox = params['bbox'] 288 | # MapInfo Pro 10 does not "know" this is the way and gets messed up 289 | if not 'mapinfo' in params['HTTP_USER_AGENT'].lower(): 290 | m.zoom_to_box(Envelope(bbox[1], bbox[0], bbox[3], bbox[2])) 291 | return m 292 | 293 | class ExceptionHandler(BaseExceptionHandler): 294 | 295 | xmlmimetype = "text/xml" 296 | 297 | xmltemplate = ElementTree.fromstring(""" 298 | 302 | 303 | 304 | """) 305 | 306 | xpath = '{http://www.opengis.net/ogc}ServiceException' 307 | 308 | handlers = {'XML': BaseExceptionHandler.xmlhandler, 309 | 'INIMAGE': BaseExceptionHandler.inimagehandler, 310 | 'BLANK': BaseExceptionHandler.blankhandler, 311 | 'HTML': BaseExceptionHandler.htmlhandler} 312 | 313 | defaulthandler = 'XML' 314 | 315 | -------------------------------------------------------------------------------- /ogcserver/wsgi.py: -------------------------------------------------------------------------------- 1 | """WSGI application wrapper for Mapnik OGC WMS Server.""" 2 | 3 | try: 4 | from urlparse import parse_qs 5 | except ImportError: 6 | from cgi import parse_qs 7 | 8 | import logging 9 | import imp 10 | 11 | from cStringIO import StringIO 12 | 13 | import mapnik 14 | 15 | from ogcserver.common import Version 16 | from ogcserver.WMS import BaseWMSFactory 17 | from ogcserver.configparser import SafeConfigParser 18 | from ogcserver.wms111 import ExceptionHandler as ExceptionHandler111 19 | from ogcserver.wms130 import ExceptionHandler as ExceptionHandler130 20 | from ogcserver.exceptions import OGCException, ServerConfigurationError 21 | 22 | WSGI_STATUS = { 23 | 200: '200 OK', 24 | 404: '404 NOT FOUND', 25 | 500: '500 SERVER ERROR', 26 | } 27 | 28 | def do_import(module): 29 | """ 30 | Makes setuptools namespaces work 31 | """ 32 | moduleobj = None 33 | exec 'import %s' % module 34 | exec 'moduleobj=%s' % module 35 | return moduleobj 36 | 37 | class WSGIApp: 38 | 39 | def __init__(self, configpath, mapfile=None,fonts=None,home_html=None): 40 | conf = SafeConfigParser() 41 | conf.readfp(open(configpath)) 42 | # TODO - be able to supply in config as well 43 | self.home_html = home_html 44 | self.conf = conf 45 | if fonts: 46 | mapnik.register_fonts(fonts) 47 | if mapfile: 48 | wms_factory = BaseWMSFactory(configpath) 49 | # TODO - add support for Cascadenik MML 50 | wms_factory.loadXML(mapfile) 51 | wms_factory.finalize() 52 | self.mapfactory = wms_factory 53 | else: 54 | if not conf.has_option_with_value('server', 'module'): 55 | raise ServerConfigurationError('The factory module is not defined in the configuration file.') 56 | try: 57 | mapfactorymodule = do_import(conf.get('server', 'module')) 58 | except ImportError: 59 | raise ServerConfigurationError('The factory module could not be loaded.') 60 | if hasattr(mapfactorymodule, 'WMSFactory'): 61 | self.mapfactory = getattr(mapfactorymodule, 'WMSFactory')() 62 | else: 63 | raise ServerConfigurationError('The factory module does not have a WMSFactory class.') 64 | if conf.has_option('server', 'debug'): 65 | self.debug = int(conf.get('server', 'debug')) 66 | else: 67 | self.debug = 0 68 | if self.conf.has_option_with_value('server', 'maxage'): 69 | self.max_age = 'max-age=%d' % self.conf.get('server', 'maxage') 70 | else: 71 | self.max_age = None 72 | 73 | def __call__(self, environ, start_response): 74 | reqparams = {} 75 | base = True 76 | for key, value in parse_qs(environ['QUERY_STRING'], True).items(): 77 | reqparams[key.lower()] = value[0] 78 | base = False 79 | 80 | if self.conf.has_option_with_value('service', 'baseurl'): 81 | onlineresource = '%s' % self.conf.get('service', 'baseurl') 82 | else: 83 | # if there is no baseurl in the config file try to guess a valid one 84 | onlineresource = 'http://%s%s%s?' % (environ['HTTP_HOST'], environ['SCRIPT_NAME'], environ['PATH_INFO']) 85 | 86 | try: 87 | if not reqparams.has_key('request'): 88 | raise OGCException('Missing request parameter.') 89 | request = reqparams['request'] 90 | del reqparams['request'] 91 | if request == 'GetCapabilities' and not reqparams.has_key('service'): 92 | raise OGCException('Missing service parameter.') 93 | if request in ['GetMap', 'GetFeatureInfo']: 94 | service = 'WMS' 95 | else: 96 | try: 97 | service = reqparams['service'] 98 | except: 99 | service = 'WMS' 100 | request = 'GetCapabilities' 101 | if reqparams.has_key('service'): 102 | del reqparams['service'] 103 | try: 104 | ogcserver = do_import('ogcserver') 105 | except: 106 | raise OGCException('Unsupported service "%s".' % service) 107 | ServiceHandlerFactory = getattr(ogcserver, service).ServiceHandlerFactory 108 | servicehandler = ServiceHandlerFactory(self.conf, self.mapfactory, onlineresource, reqparams.get('version', None)) 109 | if reqparams.has_key('version'): 110 | del reqparams['version'] 111 | if request not in servicehandler.SERVICE_PARAMS.keys(): 112 | raise OGCException('Operation "%s" not supported.' % request, 'OperationNotSupported') 113 | ogcparams = servicehandler.processParameters(request, reqparams) 114 | try: 115 | requesthandler = getattr(servicehandler, request) 116 | except: 117 | raise OGCException('Operation "%s" not supported.' % request, 'OperationNotSupported') 118 | 119 | # stick the user agent in the request params 120 | # so that we can add ugly hacks for specific buggy clients 121 | ogcparams['HTTP_USER_AGENT'] = environ.get('HTTP_USER_AGENT', '') 122 | 123 | response = requesthandler(ogcparams) 124 | except: 125 | version = reqparams.get('version', None) 126 | if not version: 127 | version = Version() 128 | else: 129 | version = Version(version) 130 | if version >= '1.3.0': 131 | eh = ExceptionHandler130(self.debug,base,self.home_html) 132 | else: 133 | eh = ExceptionHandler111(self.debug,base,self.home_html) 134 | response = eh.getresponse(reqparams) 135 | response_headers = [('Content-Type', response.content_type),('Content-Length', str(len(response.content)))] 136 | if self.max_age: 137 | response_headers.append(('Cache-Control', self.max_age)) 138 | status = WSGI_STATUS.get(response.status_code, '500 SERVER ERROR') 139 | start_response(status, response_headers) 140 | yield response.content 141 | 142 | 143 | # PasteDeploy factories [kiorky kiorky@cryptelium.net] 144 | 145 | class BasePasteWSGIApp(WSGIApp): 146 | def __init__(self, 147 | configpath, 148 | fonts=None, 149 | home_html=None, 150 | **kwargs 151 | ): 152 | conf = SafeConfigParser() 153 | conf.readfp(open(configpath)) 154 | # TODO - be able to supply in config as well 155 | self.home_html = home_html 156 | self.conf = conf 157 | if fonts: 158 | mapnik.register_fonts(fonts) 159 | if 'debug' in kwargs: 160 | self.debug = bool(kwargs['debug']) 161 | else: 162 | self.debug = False 163 | if self.debug: 164 | self.debug=1 165 | else: 166 | self.debug=0 167 | if 'maxage' in kwargs: 168 | self.max_age = 'max-age=%d' % kwargs.get('maxage') 169 | else: 170 | self.max_age = None 171 | 172 | class MapFilePasteWSGIApp(BasePasteWSGIApp): 173 | def __init__(self, 174 | configpath, 175 | mapfile, 176 | fonts=None, 177 | home_html=None, 178 | **kwargs 179 | ): 180 | BasePasteWSGIApp.__init__(self, 181 | configpath, 182 | font=fonts, home_html=home_html, **kwargs) 183 | wms_factory = BaseWMSFactory(configpath) 184 | wms_factory.loadXML(mapfile) 185 | wms_factory.finalize() 186 | self.mapfactory = wms_factory 187 | 188 | class WMSFactoryPasteWSGIApp(BasePasteWSGIApp): 189 | def __init__(self, 190 | configpath, 191 | server_module, 192 | fonts=None, 193 | home_html=None, 194 | **kwargs 195 | ): 196 | BasePasteWSGIApp.__init__(self, 197 | configpath, 198 | font=fonts, home_html=home_html, **kwargs) 199 | try: 200 | mapfactorymodule = do_import(server_module) 201 | except ImportError: 202 | raise ServerConfigurationError('The factory module could not be loaded.') 203 | if hasattr(mapfactorymodule, 'WMSFactory'): 204 | self.mapfactory = getattr(mapfactorymodule, 'WMSFactory')(configpath) 205 | else: 206 | raise ServerConfigurationError('The factory module does not have a WMSFactory class.') 207 | 208 | def ogcserver_base_factory(base, global_config, **local_config): 209 | """ 210 | A paste.httpfactory to wrap an ogcserver WSGI based application. 211 | """ 212 | log = logging.getLogger('ogcserver.wsgi') 213 | wconf = global_config.copy() 214 | wconf.update(**local_config) 215 | debug = False 216 | if global_config.get('debug', 'False').lower() == 'true': 217 | debug = True 218 | configpath = wconf['ogcserver_config'] 219 | server_module = wconf.get('mapfile', None) 220 | fonts = wconf.get('fonts', None) 221 | home_html = wconf.get('home_html', None) 222 | app = None 223 | if base == MapFilePasteWSGIApp: 224 | mapfile = wconf['mapfile'] 225 | app = base(configpath, 226 | mapfile, 227 | fonts=fonts, 228 | home_html=home_html, 229 | debug=False) 230 | elif base == WMSFactoryPasteWSGIApp: 231 | server_module = wconf['server_module'] 232 | app = base(configpath, 233 | server_module, 234 | fonts=fonts, 235 | home_html=home_html, 236 | debug=False) 237 | def ogcserver_app(environ, start_response): 238 | from webob import Request 239 | req = Request(environ) 240 | try: 241 | resp = req.get_response(app) 242 | return resp(environ, start_response) 243 | except Exception, e: 244 | if not debug: 245 | log.error('%r: %s', e, e) 246 | log.error('%r', environ) 247 | from webob import exc 248 | return exc.HTTPServerError(str(e))(environ, start_response) 249 | else: 250 | raise 251 | return ogcserver_app 252 | 253 | def ogcserver_map_factory(global_config, **local_config): 254 | return ogcserver_base_factory(MapFilePasteWSGIApp, 255 | global_config, 256 | **local_config) 257 | 258 | def ogcserver_wms_factory(global_config, **local_config): 259 | return ogcserver_base_factory(WMSFactoryPasteWSGIApp, 260 | global_config, 261 | **local_config) 262 | 263 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup 5 | HAS_SETUPTOOLS = True 6 | except ImportError: 7 | from distutils.core import setup 8 | HAS_SETUPTOOLS = False 9 | 10 | options = dict(name='ogcserver', 11 | version='0.1.1', 12 | description="A OGC WMS for Mapnik", 13 | #long_description="TODO", 14 | author='Jean-Francois Doyon', 15 | maintainer='Dane Springmeyer', 16 | maintainer_email='dane@dbsgeo.com', 17 | requires=['mapnik (>=0.7.0)'], 18 | provides=['ogcserver'], 19 | keywords='mapnik,wms,gis,geospatial', 20 | url='https://github.com/mapnik/OGCServer', 21 | packages=['ogcserver'], 22 | scripts=['bin/ogcserver'], 23 | package_data={ 24 | 'ogcserver':['default.conf'], 25 | }, 26 | classifiers=[ 27 | 'Development Status :: 4 - Beta', 28 | 'Environment :: Web Environment', 29 | 'License :: OSI Approved :: BSD License', 30 | 'Intended Audience :: Developers', 31 | 'Intended Audience :: Science/Research', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python', 34 | 'Topic :: Scientific/Engineering :: GIS', 35 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 36 | 'Topic :: Utilities'], 37 | ) 38 | 39 | if HAS_SETUPTOOLS: 40 | options.update(dict(entry_points={ 41 | 'paste.app_factory': ['mapfile=ogcserver.wsgi:ogcserver_map_factory', 42 | 'wms_factory=ogcserver.wsgi:ogcserver_wms_factory', 43 | ], 44 | }, 45 | install_requires = ['setuptools', 'PasteScript', 'WebOb', 'Pillow'] 46 | )) 47 | 48 | setup(**options) 49 | 50 | if not HAS_SETUPTOOLS: 51 | warning = '\n***Warning*** ogcserver also requires' 52 | missing = False 53 | try: 54 | import PIL 55 | # todo import Image ? 56 | except: 57 | try: 58 | from PIL import Image 59 | except: 60 | missing = True 61 | warning +=' Pillow (easy_install Pillow)' 62 | if missing: 63 | import sys 64 | sys.stderr.write('%s\n' % warning) 65 | 66 | -------------------------------------------------------------------------------- /tests/empty.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/tests/empty.dbf -------------------------------------------------------------------------------- /tests/empty.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/tests/empty.shp -------------------------------------------------------------------------------- /tests/map_factory.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ogcserver.WMS import BaseWMSFactory 3 | from mapnik import Style, Layer, Map, load_map 4 | 5 | class WMSFactory(BaseWMSFactory): 6 | def __init__(self): 7 | BaseWMSFactory.__init__(self) 8 | base_path, tail = os.path.split(__file__) 9 | file_path = os.path.join(base_path, 'mapfile_encoding.xml') 10 | self.loadXML(file_path) 11 | self.finalize() 12 | -------------------------------------------------------------------------------- /tests/mapfile_background-color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | test 11 | 12 | 13 | shape 14 | empty 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/mapfile_encoding.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | Càsaïñ 11 | 12 | 13 | shape 14 | empty 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/mapfile_styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | 17 | 21 | 22 | 23 | simple-style 24 | 25 | 26 | shape 27 | empty 28 | 29 | 30 | 31 | 32 | simple-style 33 | another-style 34 | 35 | 36 | shape 37 | empty 38 | 39 | 40 | 41 | 42 | default 43 | another-style 44 | 45 | 46 | shape 47 | empty 48 | 49 | 50 | 51 | 52 | default 53 | 54 | 55 | shape 56 | empty 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /tests/ogcserver.conf: -------------------------------------------------------------------------------- 1 | [server] 2 | module=map_factory 3 | # if debug is on default to html handler 4 | debug=1 5 | 6 | [service] 7 | # title is used in encoding tests 8 | title=Title / Título 9 | abstract=Abstract 10 | maxheight=2048 11 | maxwidth=2048 12 | allowedepsgcodes=23031,4326 13 | 14 | onlineresource=http://example.com/ogcserver/ 15 | fees= 16 | keywordlist= 17 | accessconstraints= 18 | 19 | [contact] 20 | contactperson= 21 | contactorganization= 22 | contactposition= 23 | addresstype= 24 | address= 25 | city= 26 | stateorprovince= 27 | postcode= 28 | country= 29 | contactvoicetelephone= 30 | contactelectronicmailaddress= 31 | -------------------------------------------------------------------------------- /tests/shape_encoding.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | row 12 | 13 | 14 | shape 15 | shape_iso8859-1_col 16 | iso-8859-1 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/shape_iso8859-1_col.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/tests/shape_iso8859-1_col.dbf -------------------------------------------------------------------------------- /tests/shape_iso8859-1_col.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/tests/shape_iso8859-1_col.shp -------------------------------------------------------------------------------- /tests/shape_iso8859-1_col.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/tests/shape_iso8859-1_col.shx -------------------------------------------------------------------------------- /tests/shape_iso8859-1_col.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapnik/OGCServer/7066ed7a564638f6cfd3eecde87c3bd9a9ba8853/tests/shape_iso8859-1_col.zip -------------------------------------------------------------------------------- /tests/testGetCapabilities.py: -------------------------------------------------------------------------------- 1 | import nose 2 | import os 3 | from ogcserver.configparser import SafeConfigParser 4 | from ogcserver.WMS import BaseWMSFactory 5 | from ogcserver.wms111 import ServiceHandler as ServiceHandler111 6 | from ogcserver.wms130 import ServiceHandler as ServiceHandler130 7 | 8 | def _wms_capabilities(): 9 | base_path, tail = os.path.split(__file__) 10 | file_path = os.path.join(base_path, 'mapfile_encoding.xml') 11 | wms = BaseWMSFactory() 12 | wms.loadXML(file_path) 13 | wms.finalize() 14 | 15 | conf = SafeConfigParser() 16 | conf.readfp(open(os.path.join(base_path, 'ogcserver.conf'))) 17 | 18 | wms111 = ServiceHandler111(conf, wms, "localhost") 19 | wms130 = ServiceHandler130(conf, wms, "localhost") 20 | 21 | return (conf, { 22 | '1.1.1': wms111.GetCapabilities({}), 23 | '1.3.0': wms130.GetCapabilities({}) 24 | }) 25 | 26 | def test_encoding(): 27 | conf, caps = _wms_capabilities() 28 | 29 | # Check the response is encoded in UTF-8 30 | # Search for the title in the response 31 | if conf.get('service', 'title') not in caps['1.1.1'].content: 32 | raise Exception('GetCapabilities is not correctly encoded') 33 | 34 | return True 35 | 36 | def test_latlonbbox(): 37 | from xml.etree import ElementTree 38 | 39 | def find_in_root_layer(xml_string, layer_path, tag): 40 | caps_dom = ElementTree.XML(xml_string) 41 | root_lyr = caps_dom.find(layer_path) 42 | if root_lyr is None: 43 | raise Exception('Hm, couldn\'t find a layer') 44 | if root_lyr.find(tag) is None: 45 | print ElementTree.tostring(root_lyr) 46 | raise Exception('Root layer is missing %s' % tag) 47 | 48 | conf, caps = _wms_capabilities() 49 | find_in_root_layer(caps['1.1.1'].content, 'Capability/Layer', 'LatLonBoundingBox') 50 | find_in_root_layer(caps['1.3.0'].content, 51 | '{http://www.opengis.net/wms}Capability/{http://www.opengis.net/wms}Layer', 52 | '{http://www.opengis.net/wms}EX_GeographicBoundingBox') 53 | 54 | return True 55 | -------------------------------------------------------------------------------- /tests/testGetFeatureinfo.py: -------------------------------------------------------------------------------- 1 | import nose 2 | 3 | def test_encoding(): 4 | import os 5 | from ogcserver.configparser import SafeConfigParser 6 | from ogcserver.WMS import BaseWMSFactory 7 | from ogcserver.wms111 import ServiceHandler as ServiceHandler111 8 | from ogcserver.wms130 import ServiceHandler as ServiceHandler130 9 | 10 | base_path, tail = os.path.split(__file__) 11 | file_path = os.path.join(base_path, 'shape_encoding.xml') 12 | wms = BaseWMSFactory() 13 | wms.loadXML(file_path) 14 | wms.finalize() 15 | 16 | conf = SafeConfigParser() 17 | conf.readfp(open(os.path.join(base_path, 'ogcserver.conf'))) 18 | 19 | # srs = EPSG:4326 20 | # 3.00 , 42,35 - 3.15 , 42.51 21 | # x = 5 , y = 6 22 | params = {} 23 | params['srs'] = 'epsg:4326' 24 | params['x'] = 5 25 | params['y'] = 5 26 | params['bbox'] = [3.00,42.35,3.15,42.51] 27 | params['height'] = 10 28 | params['width'] = 10 29 | params['layers'] = ['row'] 30 | params['styles'] = '' 31 | params['query_layers'] = ['row'] 32 | 33 | for format in ['text/plain', 'text/xml']: 34 | params['info_format'] = format 35 | wms111 = ServiceHandler111(conf, wms, "localhost") 36 | result = wms111.GetFeatureInfo(params) 37 | 38 | wms130 = ServiceHandler130(conf, wms, "localhost") 39 | wms130.GetCapabilities({}) 40 | 41 | 42 | return True 43 | 44 | -------------------------------------------------------------------------------- /tests/testGetMap.py: -------------------------------------------------------------------------------- 1 | import nose 2 | import os 3 | from ogcserver.configparser import SafeConfigParser 4 | from ogcserver.WMS import BaseWMSFactory 5 | from ogcserver.wms111 import ServiceHandler as ServiceHandler111 6 | from ogcserver.wms130 import ServiceHandler as ServiceHandler130 7 | from ogcserver.common import ColorFactory 8 | 9 | def _wms_services(mapfile): 10 | base_path, tail = os.path.split(__file__) 11 | file_path = os.path.join(base_path, mapfile) 12 | wms = BaseWMSFactory() 13 | wms.loadXML(file_path) 14 | wms.finalize() 15 | 16 | conf = SafeConfigParser() 17 | conf.readfp(open(os.path.join(base_path, 'ogcserver.conf'))) 18 | 19 | wms111 = ServiceHandler111(conf, wms, "localhost") 20 | wms130 = ServiceHandler130(conf, wms, "localhost") 21 | 22 | return (conf, { 23 | '1.1.1': wms111, 24 | '1.3.0': wms130 25 | }) 26 | 27 | def test_no_background_color(): 28 | # load mapfile with no background-color definition 29 | conf, services = _wms_services('mapfile_encoding.xml') 30 | 31 | reqparams = { 32 | 'srs': 'EPSG:4326', 33 | 'bbox': '-180.0000,-90.0000,180.0000,90.0000', 34 | 'width': 800, 35 | 'height': 600, 36 | 'layers': '__all__', 37 | 'styles': '', 38 | 'format': 'image/png', 39 | } 40 | 41 | from ogcserver.WMS import ServiceHandlerFactory 42 | mapfactory = BaseWMSFactory() 43 | servicehandler = ServiceHandlerFactory(conf, mapfactory, '', '1.1.1') 44 | ogcparams = servicehandler.processParameters('GetMap', reqparams) 45 | ogcparams['crs'] = ogcparams['srs'] 46 | ogcparams['HTTP_USER_AGENT'] = 'unit_tests' 47 | 48 | m = services['1.1.1']._buildMap(ogcparams) 49 | print 'wms 1.1.1 backgound color: %s' % m.background 50 | assert m.background == ColorFactory('rgb(255,255,255)') 51 | 52 | m = services['1.3.0']._buildMap(ogcparams) 53 | print 'wms 1.3.0 backgound color: %s' % m.background 54 | assert m.background == ColorFactory('rgb(255,255,255)') 55 | 56 | def test_map_background_color(): 57 | conf, services = _wms_services('mapfile_background-color.xml') 58 | 59 | reqparams = { 60 | 'srs': 'EPSG:4326', 61 | 'bbox': '-180.0000,-90.0000,180.0000,90.0000', 62 | 'width': 800, 63 | 'height': 600, 64 | 'layers': '__all__', 65 | 'styles': '', 66 | 'format': 'image/png', 67 | } 68 | 69 | from ogcserver.WMS import ServiceHandlerFactory 70 | mapfactory = BaseWMSFactory() 71 | servicehandler = ServiceHandlerFactory(conf, mapfactory, '', '1.1.1') 72 | ogcparams = servicehandler.processParameters('GetMap', reqparams) 73 | ogcparams['crs'] = ogcparams['srs'] 74 | ogcparams['HTTP_USER_AGENT'] = 'unit_tests' 75 | 76 | m = services['1.1.1']._buildMap(ogcparams) 77 | print 'wms 1.1.1 backgound color: %s' % m.background 78 | assert m.background == ColorFactory('rgb(255,0,0)') 79 | 80 | m = services['1.3.0']._buildMap(ogcparams) 81 | print 'wms 1.3.0 backgound color: %s' % m.background 82 | assert m.background == ColorFactory('rgb(255,0,0)') 83 | 84 | def test_url_background_color(): 85 | conf, services = _wms_services('mapfile_background-color.xml') 86 | 87 | reqparams = { 88 | 'srs': 'EPSG:4326', 89 | 'bbox': '-180.0000,-90.0000,180.0000,90.0000', 90 | 'width': 800, 91 | 'height': 600, 92 | 'layers': '__all__', 93 | 'styles': '', 94 | 'format': 'image/png', 95 | 'bgcolor': '0x00FF00', 96 | } 97 | 98 | from ogcserver.WMS import ServiceHandlerFactory 99 | mapfactory = BaseWMSFactory() 100 | servicehandler = ServiceHandlerFactory(conf, mapfactory, '', '1.1.1') 101 | ogcparams = servicehandler.processParameters('GetMap', reqparams) 102 | ogcparams['crs'] = ogcparams['srs'] 103 | ogcparams['HTTP_USER_AGENT'] = 'unit_tests' 104 | 105 | m = services['1.1.1']._buildMap(ogcparams) 106 | print 'wms 1.1.1 backgound color: %s' % m.background 107 | assert m.background == ColorFactory('rgb(0,255,0)') 108 | 109 | m = services['1.3.0']._buildMap(ogcparams) 110 | print 'wms 1.3.0 backgound color: %s' % m.background 111 | assert m.background == ColorFactory('rgb(0,255,0)') 112 | 113 | def test_url_background_color_transparent(): 114 | conf, services = _wms_services('mapfile_background-color.xml') 115 | 116 | reqparams = { 117 | 'srs': 'EPSG:4326', 118 | 'bbox': '-180.0000,-90.0000,180.0000,90.0000', 119 | 'width': 800, 120 | 'height': 600, 121 | 'layers': '__all__', 122 | 'styles': '', 123 | 'format': 'image/png', 124 | 'bgcolor': '0x00FF00', 125 | 'transparent': 'TRUE', 126 | } 127 | 128 | from ogcserver.WMS import ServiceHandlerFactory 129 | mapfactory = BaseWMSFactory() 130 | servicehandler = ServiceHandlerFactory(conf, mapfactory, '', '1.1.1') 131 | ogcparams = servicehandler.processParameters('GetMap', reqparams) 132 | ogcparams['crs'] = ogcparams['srs'] 133 | ogcparams['HTTP_USER_AGENT'] = 'unit_tests' 134 | 135 | m = services['1.1.1']._buildMap(ogcparams) 136 | print 'wms 1.1.1 backgound color: %s' % m.background 137 | assert m.background == None 138 | 139 | m = services['1.3.0']._buildMap(ogcparams) 140 | print 'wms 1.3.0 backgound color: %s' % m.background 141 | assert m.background == None 142 | -------------------------------------------------------------------------------- /tests/testLayerStyles.py: -------------------------------------------------------------------------------- 1 | import nose 2 | import os, sys 3 | import StringIO 4 | from ogcserver.configparser import SafeConfigParser 5 | from ogcserver.WMS import BaseWMSFactory 6 | from ogcserver.wms111 import ServiceHandler as ServiceHandler111 7 | from ogcserver.wms130 import ServiceHandler as ServiceHandler130 8 | from ogcserver.exceptions import OGCException 9 | 10 | multi_style_err_text = 'Warning: Multi-style layer \'awkward-layer\' contains a regular \ 11 | style named \'default\'. This style will effectively be hidden by the \'all styles\' \ 12 | default style for multi-style layers.' 13 | 14 | def _wms_services(mapfile): 15 | base_path, tail = os.path.split(__file__) 16 | file_path = os.path.join(base_path, mapfile) 17 | wms = BaseWMSFactory() 18 | 19 | # Layer 'awkward-layer' contains a regular style named 'default', which will 20 | # be hidden by OGCServer's auto-generated 'default' style. A warning message 21 | # is written to sys.stderr in loadXML. 22 | # Since we don't want to see this several times while unit testing (nose only 23 | # redirects sys.stdout), we redirect sys.stderr here into a StringIO buffer 24 | # temporarily. 25 | # As a side effect, we can as well search for the warning message and fail the 26 | # test, if it occurs zero or more than one times per loadXML invocation. However, 27 | # this test highly depends on the warning message text. 28 | stderr = sys.stderr 29 | errbuf = StringIO.StringIO() 30 | sys.stderr = errbuf 31 | 32 | wms.loadXML(file_path) 33 | 34 | sys.stderr = stderr 35 | errbuf.seek(0) 36 | warnings = 0 37 | for line in errbuf: 38 | if line.strip('\r\n') == multi_style_err_text: 39 | warnings += 1 40 | else: 41 | sys.stderr.write(line) 42 | errbuf.close() 43 | 44 | if warnings == 0: 45 | raise Exception('Expected warning message for layer \'awkward-layer\' not found in stderr stream.') 46 | elif warnings > 1: 47 | raise Exception('Expected warning message for layer \'awkward-layer\' occurred several times (%d) in stderr stream.' % warnings) 48 | 49 | wms.finalize() 50 | 51 | conf = SafeConfigParser() 52 | conf.readfp(open(os.path.join(base_path, 'ogcserver.conf'))) 53 | 54 | wms111 = ServiceHandler111(conf, wms, "localhost") 55 | wms130 = ServiceHandler130(conf, wms, "localhost") 56 | 57 | return (conf, { 58 | '1.1.1': wms111, 59 | '1.3.0': wms130 60 | }) 61 | 62 | def _check_style_lists(request, version, lyr_number, lyr_name, lyr_styles, exp_styles): 63 | n_lyr_styles = len(lyr_styles) 64 | n_exp_styles = len(exp_styles) 65 | n_min = min(n_lyr_styles, n_exp_styles) 66 | indent = ' ' * (len(request) + len(version) + 2) 67 | 68 | for lyr_style, exp_style, idx in zip(lyr_styles, exp_styles, range(n_min)): 69 | sys.stdout.write('%s style #%d \'%s\':' % (indent, idx+1, lyr_style)) 70 | if lyr_style != exp_style: 71 | raise Exception('%s %s: Unexpected style #%d \'%s\' for layer #%d \'%s\': expected style: \'%s\'.' % (request, version, idx+1, lyr_style, lyr_number, lyr_name, exp_style)) 72 | sys.stdout.write(' OK' + os.linesep) 73 | 74 | if n_lyr_styles < n_exp_styles: 75 | s = '' 76 | for style in exp_styles[n_lyr_styles:]: 77 | s += '\'%s\', ' % style 78 | s = s[:len(s)-2] 79 | raise Exception('%s %s: Missing %d style(s) for layer #%d \'%s\': missing style(s): %s.' % (request, version, n_exp_styles-n_lyr_styles, lyr_number, lyr_name, s)) 80 | 81 | if n_lyr_styles > n_exp_styles: 82 | s = '' 83 | for style in lyr_styles[n_exp_styles:]: 84 | s += '\'%s\', ' % style 85 | s = s[:len(s)-2] 86 | raise Exception('%s %s: Found %d unexpected style(s) for layer #%d \'%s\': unexpected styles: %s.' % (request, version, n_lyr_styles-n_exp_styles, lyr_number, lyr_name, s)) 87 | 88 | 89 | def test_capabilities(): 90 | from xml.etree import ElementTree 91 | 92 | def get_caps_styles(xml_string, ns=''): 93 | 94 | result = [] 95 | caps_dom = ElementTree.XML(xml_string) 96 | 97 | for lyr_dom in caps_dom.findall('%sCapability/%sLayer/%sLayer' % (ns, ns, ns)): 98 | lyr_name = lyr_dom.findtext('%sName' % ns) 99 | if lyr_name: 100 | styles = [] 101 | for style in lyr_dom.findall('%sStyle' % ns): 102 | name = style.findtext('%sName' % ns) 103 | title = style.findtext('%sTitle' % ns) 104 | abstr = style.findtext('%sAbstract' % ns) 105 | styles.append((name, title, abstr)) 106 | 107 | result.append((lyr_name, styles)) 108 | 109 | return result 110 | 111 | def check_caps_styles(store, version, no, layer, styles): 112 | 113 | print 'GetCapabilities %s: layer #%d \'%s\':' % (version, no, layer) 114 | 115 | lyr = store[no-1] 116 | if (lyr[0] != layer): 117 | raise Exception('GetCapabilities %s: Unexpected name \'%s\' for layer #%d: expected name: \'%s\'.' % (version, lyr[0], no, layer)) 118 | 119 | lyr_styles = lyr[1] 120 | if len(lyr_styles) == 0: 121 | raise Exception('GetCapabilities %s: No styles found for layer \'%s\'.' % (version, lyr[0])) 122 | 123 | _check_style_lists('GetCapabilities', version, no, layer, [x[0] for x in lyr_styles], styles) 124 | 125 | if len(styles) > 1 and lyr_styles[0][2] == None: 126 | raise Exception('GetCapabilities %s: Missing Abstract text for default style #1 \'%s\' of layer #%d \'%s\'.' % (version, lyr_styles[0][0], no, lyr[0])) 127 | 128 | 129 | conf, services = _wms_services('mapfile_styles.xml') 130 | 131 | for version, ns in [('1.1.1', ''), ('1.3.0', '{http://www.opengis.net/wms}')]: 132 | 133 | caps = services[version].GetCapabilities({}).content 134 | styles = get_caps_styles(caps, ns) 135 | 136 | print 137 | print 'GetCapabilities %s: collected layers and styles:' % version 138 | print 139 | print styles 140 | print 141 | 142 | check_caps_styles(styles, version, 1, 'single-style-layer', ['simple-style']) 143 | check_caps_styles(styles, version, 2, 'multi-style-layer', ['default', 'simple-style', 'another-style']) 144 | check_caps_styles(styles, version, 3, 'awkward-layer', ['default', 'another-style']) 145 | check_caps_styles(styles, version, 4, 'single-default-layer', ['default']) 146 | 147 | return True 148 | 149 | def test_map(): 150 | from ogcserver.WMS import ServiceHandlerFactory 151 | 152 | reqparams = { 153 | 'srs': 'EPSG:4326', 154 | 'bbox': '-180.0000,-90.0000,180.0000,90.0000', 155 | 'width': 800, 156 | 'height': 600, 157 | 'layers': '__all__', 158 | 'styles': '', 159 | 'format': 'image/png', 160 | } 161 | 162 | def check_map_styles(version, no, layer, style_param, styles=None): 163 | 164 | print 'GetMap %s: layer #%d \'%s\': STYLES=%s' % (version, no, layer, style_param) 165 | 166 | ogcparams['layers'] = layer.split(',') 167 | ogcparams['styles'] = style_param.split(',') 168 | 169 | # Parameter 'styles' contains the list of expected styles. If styles 170 | # evaluates to False (e.g. None, Null), an invalid STYLE was provided 171 | # and so, an OGCException 'StyleNotDefined' is expected. 172 | try: 173 | m = services[version]._buildMap(ogcparams) 174 | except OGCException: 175 | if not styles: 176 | print ' style #0 \'invalid style\': OK (caught OGCException)' 177 | print 178 | return 179 | raise Exception('GetMap %s: Expected OGCExecption for invalid style \'%s\' for layer #%d \'%s\' was not thrown.' % (version, style_param, no, layer)) 180 | 181 | _check_style_lists('GetMap', version, no, layer, m.layers[0].styles, styles) 182 | print 183 | 184 | 185 | conf, services = _wms_services('mapfile_styles.xml') 186 | 187 | mapfactory = BaseWMSFactory() 188 | servicehandler = ServiceHandlerFactory(conf, mapfactory, '', '1.1.1') 189 | ogcparams = servicehandler.processParameters('GetMap', reqparams) 190 | ogcparams['crs'] = ogcparams['srs'] 191 | ogcparams['HTTP_USER_AGENT'] = 'unit_tests' 192 | 193 | for version in ['1.1.1', '1.3.0']: 194 | check_map_styles(version, 1, 'single-style-layer', '', ['simple-style']) 195 | check_map_styles(version, 1, 'single-style-layer', 'simple-style', ['simple-style']) 196 | check_map_styles(version, 1, 'single-style-layer', 'default') 197 | check_map_styles(version, 1, 'single-style-layer', 'invalid-style') 198 | 199 | check_map_styles(version, 2, 'multi-style-layer', '', ['simple-style', 'another-style']) 200 | check_map_styles(version, 2, 'multi-style-layer', 'default', ['simple-style', 'another-style']) 201 | check_map_styles(version, 2, 'multi-style-layer', 'simple-style', ['simple-style']) 202 | check_map_styles(version, 2, 'multi-style-layer', 'another-style', ['another-style']) 203 | check_map_styles(version, 2, 'multi-style-layer', 'invalid-style') 204 | 205 | check_map_styles(version, 3, 'awkward-layer', '', ['default', 'another-style']) 206 | check_map_styles(version, 3, 'awkward-layer', 'default', ['default', 'another-style']) 207 | check_map_styles(version, 3, 'awkward-layer', 'another-style', ['another-style']) 208 | check_map_styles(version, 3, 'awkward-layer', 'invalid-style') 209 | 210 | check_map_styles(version, 4, 'single-default-layer', '', ['default']) 211 | check_map_styles(version, 4, 'single-default-layer', 'default', ['default']) 212 | check_map_styles(version, 4, 'single-default-layer', 'invalid-style') 213 | 214 | # Some 'manually' created error cases for testing error reporting 215 | #check_map_styles(version, 2, 'multi-style-layer', 'default', ['simple-style', 'another-style', 'foo', 'bar']) 216 | #check_map_styles(version, 2, 'multi-style-layer', 'default', ['simple-style']) 217 | 218 | return True 219 | 220 | 221 | # Running the tests without nose 222 | #test_capabilities() 223 | #test_map() 224 | -------------------------------------------------------------------------------- /tests/testLoadMapFail.py: -------------------------------------------------------------------------------- 1 | import nose 2 | import os 3 | from ogcserver.WMS import BaseWMSFactory 4 | from ogcserver.exceptions import ServerConfigurationError 5 | 6 | def test_wms_capabilities(): 7 | wms = BaseWMSFactory() 8 | nose.tools.assert_raises(ServerConfigurationError, wms.loadXML) 9 | 10 | return True 11 | -------------------------------------------------------------------------------- /tests/testLoadMapFromString.py: -------------------------------------------------------------------------------- 1 | import nose 2 | import os 3 | from ogcserver.WMS import BaseWMSFactory 4 | 5 | def test_wms_capabilities(): 6 | base_path, tail = os.path.split(__file__) 7 | file_path = os.path.join(base_path, 'mapfile_encoding.xml') 8 | wms = BaseWMSFactory() 9 | with open(file_path) as f: 10 | settings = f.read() 11 | wms.loadXML(xmlstring=settings, basepath=base_path) 12 | wms.finalize() 13 | 14 | if len(wms.layers) != 1: 15 | raise Exception('Incorrect number of layers') 16 | if len(wms.styles) != 1: 17 | raise Exception('Incorrect number of styles') 18 | 19 | return True 20 | -------------------------------------------------------------------------------- /tests/testWsgi.py: -------------------------------------------------------------------------------- 1 | import nose 2 | 3 | def start_response_111(status, headers): 4 | for header in headers: 5 | if header[0] == 'Content-Type': 6 | assert header[1] == 'application/vnd.ogc.wms_xml' 7 | assert status == '200 OK' 8 | 9 | def start_response_130(status, headers): 10 | for header in headers: 11 | if header[0] == 'Content-Type': 12 | assert header[1] == 'text/xml' 13 | assert status == '200 OK' 14 | 15 | def start_response_check_404(status, headers): 16 | print('status code: %s' % status) 17 | assert status == '404 NOT FOUND' 18 | 19 | def get_wsgiapp(): 20 | import os 21 | from ogcserver.wsgi import WSGIApp 22 | base_path, tail = os.path.split(__file__) 23 | wsgi_app = WSGIApp(os.path.join(base_path, 'ogcserver.conf')) 24 | return wsgi_app 25 | 26 | def get_environment(): 27 | environ = {} 28 | environ['HTTP_HOST'] = "localhost" 29 | environ['SCRIPT_NAME'] = __name__ 30 | environ['PATH_INFO'] = '/' 31 | return environ 32 | 33 | def test_get_capabilities(): 34 | wsgi_app = get_wsgiapp() 35 | environ = get_environment() 36 | environ['QUERY_STRING'] = "EXCEPTION=application/vnd.ogc.se_xml&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetCapabilities&" 37 | response = wsgi_app.__call__(environ, start_response_111) 38 | content = ''.join(response) 39 | 40 | environ['QUERY_STRING'] = "EXCEPTION=application/vnd.ogc.se_xml&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetCapabilities&" 41 | response = wsgi_app.__call__(environ, start_response_130) 42 | ''.join(response) 43 | 44 | def test_bad_query(): 45 | wsgi_app = get_wsgiapp() 46 | environ = get_environment() 47 | environ['QUERY_STRING'] = "EXCEPTION=application/vnd.ogc.se_xml&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&" 48 | response = wsgi_app.__call__(environ, start_response_check_404) 49 | environ['QUERY_STRING'] = "EXCEPTION=application/vnd.ogc.se_xml&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap&" 50 | response = wsgi_app.__call__(environ, start_response_check_404) 51 | --------------------------------------------------------------------------------