#------------------------------------------------------------------------
#
# basic-wms2.py : A very small WMS implementation
# V2 - removed PIL, trying PBM instead
#
#========================================================================
# LICENSE -- This is the "MIT License"
#
# Copyright (c) 2001 Allan Doyle
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#========================================================================
#
# Allan Doyle - adoyle@intl-interfaces.com
#
# This WMS works by keeping a 3600x1800 JPEG image which contains a full
# map of the world from -180,-90 to 180,90 and sending chunks of it out
# in response to map requests by clients.
#
# Things are deliberately hardcoded in this WMS to keep it simple.
#
# The image was generated using the CubeWerx WMS demo which you can find
# at http://www.cubewerx.com
#
# Debugging help was provided by Jeff de La Beaujardiere at NASA
#
#------------------------------------------------------------------------
###
### The big chunks of functionality come from mod_python and PIL
###
#
# mod_python is available from www.modpython.org
#
# It provides the connection from Apache CGI to python code
#
from mod_python import apache
#
# Python imports
#
import sys
import os
import string
# Open the map image once and load it (this may be causing a memory leak)
#
map = "/home/apache/www.intl-interfaces.net/htdocs/images/cubeserv-best.pnm"
# The WMS version that this WMS implements
#
version = '1.0.0'
# This is the capabilities XML
# Thanks to Jeff de La Beaujardiere of the NASA Digital Earth program
# for a good one that I used as a template
#
capabilities = """
]>
' + s + '
') # Wrap the message in raise apache.SERVER_RETURN, status # return to apache ## handler(req) # # The name of this function is dictated by mod_python. This is the entry # point to the WMS. It is called by apache with the map request. # A file in the local directory called .htaccess defines some of this. # (Or it can be configured into the main apache httpd.conf file) # def handler(req): "handler : called when apache gets a map request URI" # mod_python stuff # request = req.args # Provides a string with the CGI # arguments in it req.content_type = 'text/plain' # Useful if we have to send messages # back to the client. Later we'll # override it with image/jpeg # WMS argument processing # starts here... canon_args = split_args(req.args) # This turns the args into a python # list # If there are no arguments in the request, exit. The WMS spec does not # specify what to return here since technically, unless there's at least # a 'REQUEST' parameter, it's not a WMS request. For now, let's # use HTTP_BAD_REQUEST and return a message # if len(canon_args) == 0: send_html_error(req, 'No parameters found', apache.HTTP_BAD_REQUEST) # Next look at the REQUEST argument. # If it's not there, this is also not a WMS request... request = canon_args.get('REQUEST', None); if request == None: send_html_error(req, 'No REQUEST parameter found', apache.HTTP_BAD_REQUEST) # Here are the 3 choices. In the Capabilities XML we say that the # layer is not queryable, so we should not be getting a feature_info # request. If we do, we can say HTTP_BAD_REQUEST... this is consistent # with the WMS 1.0.0 spec 6.2.9.4 that says an error response must be # MIME typed. if request == 'capabilities': send_capabilities(req, canon_args) elif request == 'map': send_map(req, canon_args) elif request == 'feature_info': send_html_error(req, 'REQUEST=%s is not implemented:' % canon_args['REQUEST'], apache.HTTP_BAD_REQUEST) else: send_html_error(req,'REQUEST=%s is not a valid WMS request' % canon_args['REQUEST'], apache.HTTP_BAD_REQUEST) ## version_cmp(v1, v2) # # Compare version strings. Works like strcmp. # Since versions are dotted strings with 1, 2, or 3 components, we first # check if the strings are actually equal. If not, then we make sure we have # three components to compare by adding trailing '0' elements. Then we # compare the high-order part, the next part, and the next part. # # For this WMS we only need to know if they are equal or not equal. This # WMS does not do version negotiation. # def version_cmp(v1, v2): # If they are already equal, great if v1 == v2: return 0 # turn them into lists L1 = v1.split('.') L2 = v2.split('.') # build up things like '1.0' and '1' into '1.0.0' if len(L1) == 1: L1.append('0') if len(L1) == 2: L1.append('0') if len(L2) == 1: L2.append('0') if len(L2) == 2: L2.append('0') # now if they are equal, great if L1 == L2: return 0 if string.atoi(L1[0]) < string.atoi(L2[0]): return -1 if string.atoi(L1[0]) > string.atoi(L2[0]): return 1 if string.atoi(L1[1]) < string.atoi(L2[1]): return -1 if string.atoi(L1[1]) > string.atoi(L2[1]): return 1 if string.atoi(L1[2]) < string.atoi(L2[2]): return -1 if string.atoi(L1[2]) > string.atoi(L2[2]): return 1 ## send_capabilities(req, args) # # Simply sets the Content-Type to 'text/xml' and returns the Capabilities # string that's included at the top of this file. # def send_capabilities(req, args): # This is where version negotiation would go. We'll ignore it for now # since we only support one version. If the client tries to version # negotiate, we'll just send our 1.0.0 capabilities back each time. # Eventually the client will accept this or go away req.content_type = 'text/xml' req.send_http_header() req.write(capabilities) raise apache.SERVER_RETURN, apache.OK ## Projections # # Currently assume a base world image of 3600x1800 with the whole world # from -180,-90 to 180,90 # # No inverse is needed since we don't handle feature_info requests. # def LonToPix(lon): return int ((lon * 10) + 1800 + .5) def LatToPix(lat): return int ((-lat * 10) + 900 + .5) ## send_map(req, args) # # Checks to see if all the args that it knows about are present and correct # If so, send a map. # Note: 2001.05.07 adoyle - added .upper() to all references to # found['FORMAT'] to improve leniency for people who use lowercase 'png' # 'jpeg' etc by mistake. # def send_map(req, args): formats = {'JPEG' : '| ppmtojpeg', 'PNG' : '| pnmtopng', 'TIFF' : '| pnmtotiff', 'PPM' : ' '} # These are the required parameters (WMS 1.0.0 Table 6.3) required = ['LAYERS', 'STYLES', 'SRS', 'BBOX', 'WIDTH', 'HEIGHT', 'FORMAT'] # These are the optional parameters (WMS 1.0.0 Table 6.3) optional = ['TRANSPARENT', 'BGCOLOR', 'EXCEPTIONS'] # Loop through the list of required args. If any are missing, return # an error. # The ones that we start with are the optional ones, set to the default found = {'BGCOLOR' : '0xFFFFFF', 'TRANSPARENT' : 'FALSE', 'EXCEPTIONS' : 'INIMAGE'} for param in required: found[param] = args.get(param, None); if found[param] == None: send_html_error(req, 'No ' + param + ' parameter found', apache.HTTP_BAD_REQUEST) for param in optional: found[param] = args.get(param, found[param]); # Find the 4 values in the BBOX bbox = found['BBOX'].split(',') # Turn the BBOX values into pixel values for i in (0, 1, 2, 3): bbox[i] = string.atof(bbox[i]) x0 = LonToPix(bbox[0]) y0 = LatToPix(bbox[1]) x1 = LonToPix(bbox[2]) y1 = LatToPix(bbox[3]) # get the width/height values width = string.atoi(found['WIDTH']) height = string.atoi(found['HEIGHT']) error = 0 # Let's do a little checking if bbox[0] < -180.0 or bbox[0] > 180.0 \ or bbox[1] < -90.0 or bbox[1] > 90.0 \ or bbox[2] < -180.0 or bbox[2] > 180.0 \ or bbox[3] < -90.0 or bbox[3] > 90.0: error = 1 message = "BBOX out of range" # If there's an error, then we have to decide whether to return # an INIMAGE error (i.e. write an error message on an image) or # whether to make a blank image. In both cases, inimage or blank, we # then need to decide whether we're supposed to do transparency and # whether the image format supports it (only PNG does). Then we # have to make sure the result has transparency. if error and found['EXCEPTIONS'] == 'INIMAGE': # Build the image with the text cmd = 'ppmmake \#%s %s %s' % (found['BGCOLOR'][2:], width, height) \ + ' | ' \ + 'ppmlabel -background \#888888 -colour \#000000' \ + ' -x 5 -y 20 -text \"%s\"' \ % message + formats[found['FORMAT'].upper()] # decide whether to make it transparent if found['FORMAT'].upper() == 'PNG' and found['TRANSPARENT'] == 'TRUE': cmd = cmd + ' -force -transparent \#%s' % (found['BGCOLOR'][2:]) # If we're supposed to return a blank image, just make an image # with BGCOLOR as the entire image. elif error and found['EXCEPTIONS'] == 'BLANK': # Build the image cmd = 'ppmmake \#%s %s %s' % (found['BGCOLOR'][2:], width, height) \ + formats[found['FORMAT'].upper()] # decide whether to make it transparent if found['FORMAT'].upper() == 'PNG' and found['TRANSPARENT'] == 'TRUE': cmd = cmd + ' -force -transparent \#%s' % (found['BGCOLOR'][2:]) # If there was no error, then build the command that will return # a new map that is a rectangle cut from the old map and the scaled # into the new dimensions. else: cmd = "pnmcut -left %s -bottom %s -right %s -top %s < %s" \ % (x0,y0,x1,y1, map) \ + ' | ' \ + "pnmscale -xysize %s %s" % (width, height) \ + "| pnmsmooth" + formats[found['FORMAT'].upper()] # If you added a debug=1 (or any debug) parameter to the request # this bit will return some debugging info as text instead of the # image. This is not advertised in the capabilities because this # is not meant to be used by WMS clients. if args.get('DEBUG', None): req.send_http_header() req.write('bbox %s ' % bbox) req.write('WxH=%sx%s ' % (width, height)) req.write('x0,y0=%s,%s ' % (x0,y0)) req.write('x1,y1=%s,%s\n' % (x1,y1)) req.write(' params %s\n' % found) req.write(cmd + '\n') req.write(message) raise apache.SERVER_RETURN, apache.OK # This executes the command we built above and gathers the output # of the command for reading as a file pipe = os.popen(cmd, 'r') # Set the return Content-Type to 'image/