#------------------------------------------------------------------------
#
# 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