├── .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 |
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 |
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
\nTraceback:
%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 |
--------------------------------------------------------------------------------
/tests/mapfile_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/mapfile_styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------