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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment