Skip to content

Instantly share code, notes, and snippets.

@BjoernSchilberg
Last active September 7, 2018 14:20
Show Gist options
  • Save BjoernSchilberg/e6382a9b9b70823b4ce3 to your computer and use it in GitHub Desktop.
Save BjoernSchilberg/e6382a9b9b70823b4ce3 to your computer and use it in GitHub Desktop.
basic-wms2.py
  • Python programm basic-wms2.py
  • How_to_build_a_WMS_from_Free_Parts.PDF
#------------------------------------------------------------------------
#
# 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
Display the source blob
Display the rendered blob
Raw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment