#------------------------------------------------------------------------ # # 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 = """<?xml version='1.0' encoding="UTF-8" standalone="no" ?> <!DOCTYPE WMT_MS_Capabilities SYSTEM "http://www.digitalearth.gov/wmt/xml/capabilities_1_0_0.dtd" [ <!ELEMENT VendorSpecificCapabilities EMPTY> ]> <WMT_MS_Capabilities version="1.0.0" updateSequence="0"> <!-- Service Metadata --> <Service> <!-- The WMT-defined name for this type of service --> <Name>GetMap</Name> <!-- Human-readable title for pick lists --> <Title>Basic Map Server</Title> <!-- Narrative description providing additional information --> <Abstract>Basic WMS Map Server built as an example for a WMS cookbook Contact: adoyle@intl-interfaces.com.</Abstract> <Keywords>Demo WMS Cookbook</Keywords> <!-- Top-level address of service or service provider. See also onlineResource attributes of <dcpType> children. --> <OnlineResource>http://www.intl-interfaces.net/cookbook/WMS/</OnlineResource> <!-- Fees or access constraints imposed. --> <Fees>none</Fees> <AccessConstraints>none</AccessConstraints> </Service> <Capability> <Request> <Map> <Format> <PNG /> <JPEG /> <PPM /> <TIFF /> </Format> <DCPType> <HTTP> <Get onlineResource="http://www.intl-interfaces.net/cookbook/WMS/basic-wms2/basic-wms2.py?" /> </HTTP> </DCPType> </Map> <Capabilities> <Format> <WMS_XML /> </Format> <DCPType> <HTTP> <Get onlineResource="http://www.intl-interfaces.net/cookbook/WMS/basic-wms2/basic-wms2.py?" /> </HTTP> </DCPType> </Capabilities> </Request> <Exception> <Format> <INIMAGE /> <BLANK /> </Format> </Exception> <Layer> <Title>Demo Map Server</Title> <SRS>EPSG:4326</SRS> <LatLonBoundingBox minx="-180" miny="-90" maxx="180" maxy="90" /> <Layer queryable="0"> <Name>RELIEF</Name> <Title>Relief (ETOPO/GTOPO)</Title> <Abstract>Colored relief map with political boundaries and coastlines</Abstract> </Layer> </Layer> </Capability> </WMT_MS_Capabilities> """ ## # Name/value utilities # ## split_args(args) # # Takes a CGI string. Turns it into a list of name/value pairs. Names with # no value are given a None value (a python special value). All names # are converted to upper case since WMS arguments are case insensitive # def split_args(args): "split_args : take a CGI string and return a list with name/value pairs" canon_args = {} # Start an empty list if args == None: # Return the empty list if no args return canon_args arglist = args.split('&') # Split into list of name=value strings for arg in arglist: # Now split each name=value and tmp = arg.split('=') # turn them into sub-lists if len(tmp) == 1: # with name in the first part canon_args[tmp[0]] = None # and value in the second part else: canon_args[tmp[0].upper()] = tmp[1] return canon_args ## send_html_error(req, s, status) # # Returns a text/html response to the client with an error message # packaged inside. The status is raised as an exception which neatly # bumps us all the way back to the apache server. # def send_html_error(req, s, status): req.content_type = 'text/html' # Set the return Content-Type req.send_http_header() # Send the HTTP return header req.write('<p>' + s + '</p>') # Wrap the message in <p></p> 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/<FORMAT>' req.content_type = "image/%s" % found['FORMAT'].lower() req.send_http_header() # Send the header req.write(pipe.read()) # Send the image raise apache.SERVER_RETURN, apache.OK # exit to apache