Browse files

oop

git-svn-id: https://mapnik-utils.googlecode.com/svn/trunk/serverside/cascadenik@127 dbea3d40-3153-0410-b551-57805770c6e6
  • Loading branch information...
0 parents commit 713d66e4a783171ec520fc7b57fb91df66934d7a migurski committed Sep 3, 2008
12 Makefile
@@ -0,0 +1,12 @@
+all: cssutils
+ #
+
+cssutils:
+ curl -sO "http://pypi.python.org/packages/source/c/cssutils/cssutils-0.9.5.1.zip"
+ unzip -q cssutils-0.9.5.1.zip
+ mv cssutils-0.9.5.1/src/cssutils ./
+ mv cssutils-0.9.5.1/src/encutils ./cssutils/
+ rm -rf cssutils-0.9.5.1 cssutils-0.9.5.1.zip
+
+clean:
+ rm -rf cssutils
15 README.txt
@@ -0,0 +1,15 @@
+- Usage -
+
+Run `make` to download a copy of cssutils, or get it directly from the source:
+
+ http://code.google.com/p/cssutils/
+
+Unroll the rules in example.mss and show their cascade order:
+
+ % python style.py example.mss > example-ordered-unrolled.mss
+
+Compile example.mml into a Mapnik-suitable XML file:
+
+ % python compile.py example.mml > example-compiled.xml
+
+Will write more here later.
769 compile.py
@@ -0,0 +1,769 @@
+import os, sys
+import math
+import pprint
+import urllib
+import urlparse
+import tempfile
+import StringIO
+import optparse
+from operator import lt, le, eq, ge, gt
+import xml.etree.ElementTree
+from xml.etree.ElementTree import Element
+import style
+import PIL.Image
+
+def main(file, dir):
+ """ Given an input layers file and a directory, print the compiled
+ XML file to stdout and save any encountered external image files
+ to the named directory.
+ """
+ print compile(file, dir)
+ return 0
+
+counter = 0
+
+opsort = {lt: 1, le: 2, eq: 3, ge: 4, gt: 5}
+opstr = {lt: '<', le: '<=', eq: '==', ge: '>=', gt: '>'}
+
+class Range:
+ """ Represents a range for use in min/max scale denominator.
+
+ Ranges can have a left side, a right side, neither, or both,
+ with sides specified as inclusive or exclusive.
+ """
+ def __init__(self, leftop=None, leftedge=None, rightop=None, rightedge=None):
+ assert leftop in (lt, le, eq, ge, gt, None)
+ assert rightop in (lt, le, eq, ge, gt, None)
+
+ self.leftop = leftop
+ self.rightop = rightop
+ self.leftedge = leftedge
+ self.rightedge = rightedge
+
+ def midpoint(self):
+ """ Return a point guranteed to fall within this range, hopefully near the middle.
+ """
+ minpoint = self.leftedge
+
+ if self.leftop is gt:
+ minpoint += 1
+
+ maxpoint = self.rightedge
+
+ if self.rightop is lt:
+ maxpoint -= 1
+
+ if minpoint is None:
+ return maxpoint
+
+ elif maxpoint is None:
+ return minpoint
+
+ else:
+ return (minpoint + maxpoint) / 2
+
+ def isOpen(self):
+ """ Return true if this range has any room in it.
+ """
+ if self.leftedge and self.rightedge and self.leftedge > self.rightedge:
+ return False
+
+ if self.leftedge == self.rightedge:
+ if self.leftop is gt or self.rightop is lt:
+ return False
+
+ return True
+
+ def __repr__(self):
+ """
+ """
+ if self.leftedge == self.rightedge and self.leftop is ge and self.rightop is le:
+ # equivalent to ==
+ return '(=%s)' % self.leftedge
+
+ try:
+ return '(%s%s ... %s%s)' % (self.leftedge, opstr[self.leftop], opstr[self.rightop], self.rightedge)
+ except KeyError:
+ try:
+ return '(... %s%s)' % (opstr[self.rightop], self.rightedge)
+ except KeyError:
+ return '(%s%s ...)' % (self.leftedge, opstr[self.leftop])
+
+class Filter:
+ """ Represents a filter of some sort for use in stylesheet rules.
+
+ Composed of a list of tests.
+ """
+ def __init__(self, *tests):
+ self.tests = list(tests)
+
+ def isOpen(self):
+ """ Return true if this filter is not trivially false, i.e. self-contradictory.
+ """
+ equals = {}
+
+ for test in self.tests:
+ if test.op == '=':
+ if equals.has_key(test.arg1) and test.arg2 != equals[test.arg1]:
+ # a contradiction!
+ return False
+
+ equals[test.arg1] = test.arg2
+
+ return True
+
+ def clone(self):
+ """
+ """
+ return Filter(*self.tests[:])
+
+ def minusExtras(self):
+ """ Return a new Filter that's equal to this one,
+ without extra terms that don't add meaning.
+ """
+ assert self.isOpen()
+
+ trimmed = self.clone()
+
+ equals = {}
+
+ for test in trimmed.tests:
+ if test.op == '=':
+ equals[test.arg1] = test.arg2
+
+ extras = []
+
+ for (i, test) in enumerate(trimmed.tests):
+ if test.op == '!=' and equals.has_key(test.arg1) and equals[test.arg1] != test.arg2:
+ extras.append(i)
+
+ while extras:
+ trimmed.tests.pop(extras.pop())
+
+ return trimmed
+
+ def __repr__(self):
+ """
+ """
+ return ''.join(map(repr, self.tests))
+
+def selectors_ranges(selectors):
+ """ Given a list of selectors and a map, return a list of Ranges that
+ fully describes all possible unique slices within those selectors.
+
+ If the map looks like it uses the well-known Google/VEarth maercator
+ projection, accept "zoom" attributes in place of "scale-denominator".
+
+ This function was hard to write, it should be hard to read.
+ """
+ repeated_breaks = []
+
+ # start by getting all the range edges from the selectors into a list of break points
+ for selector in selectors:
+ for test in selector.rangeTests():
+ repeated_breaks.append(test.rangeOpEdge())
+
+ # from here on out, *order will matter*
+ # it's expected that the breaks will be sorted from minimum to maximum,
+ # with greater/lesser/equal operators accounted for.
+ repeated_breaks.sort(key=lambda (o, e): (e, opsort[o]))
+
+ breaks = []
+
+ # next remove repetitions from the list
+ for (i, (op, edge)) in enumerate(repeated_breaks):
+ if i > 0:
+ if op is repeated_breaks[i - 1][0] and edge == repeated_breaks[i - 1][1]:
+ continue
+
+ breaks.append(repeated_breaks[i])
+
+ ranges = []
+
+ # now turn those breakpoints into a list of ranges
+ for (i, (op, edge)) in enumerate(breaks):
+ if i == 0:
+ # get a right-boundary for the first range
+ if op in (lt, le):
+ ranges.append(Range(None, None, op, edge))
+ elif op in (eq, ge):
+ ranges.append(Range(None, None, lt, edge))
+ elif op is gt:
+ ranges.append(Range(None, None, le, edge))
+
+ elif i > 0:
+ # get a left-boundary based on the previous right-boundary
+ if ranges[-1].rightop is lt:
+ ranges.append(Range(ge, ranges[-1].rightedge))
+ else:
+ ranges.append(Range(gt, ranges[-1].rightedge))
+
+ # get a right-boundary for the current range
+ if op in (lt, le):
+ ranges[-1].rightop, ranges[-1].rightedge = op, edge
+ elif op in (eq, ge):
+ ranges[-1].rightop, ranges[-1].rightedge = lt, edge
+ elif op is gt:
+ ranges[-1].rightop, ranges[-1].rightedge = le, edge
+
+ # equals is a special case, sometimes
+ # an extra element may need to sneak in.
+ if op is eq:
+ if ranges[-1].leftedge == edge:
+ # the previous range also covered just this one slice.
+ ranges.pop()
+
+ # equals is expressed as greater-than-equals and less-than-equals.
+ ranges.append(Range(ge, edge, le, edge))
+
+ if i == len(breaks) - 1:
+ # get a left-boundary for the final range
+ if op in (lt, ge):
+ ranges.append(Range(ge, edge))
+ else:
+ ranges.append(Range(gt, edge))
+
+ ranges = [range for range in ranges if range.isOpen()]
+
+ # print breaks
+ # print ranges
+
+ if ranges:
+ return ranges
+
+ else:
+ # if all else fails, return a Range that covers everything
+ return [Range()]
+
+def selectors_filters(selectors):
+ """ Given a list of selectors and a map, return a list of Filters that
+ fully describes all possible unique equality tests within those selectors.
+ """
+ tests = {}
+ arg1s = set()
+
+ # get all the tests and test.arg1 values out of the selectors
+ for selector in selectors:
+ for test in selector.allTests():
+ if test.isSimple():
+ tests[str(test)] = test
+ arg1s.add(test.arg1)
+
+ tests = tests.values()
+ filters = []
+
+ # create something like a truth table
+ for i in range(int(math.pow(2, len(tests)))):
+ filter = Filter()
+
+ for (j, test) in enumerate(tests):
+ if bool(i & (0x01 << j)):
+ filter.tests.append(test)
+ else:
+ filter.tests.append(test.inverse())
+
+ if filter.isOpen():
+ filters.append(filter.minusExtras())
+
+ if len(filters):
+ return filters
+
+ # if no filters have been defined, return a blank one that matches anything
+ return [Filter()]
+
+def next_counter():
+ global counter
+ counter += 1
+ return counter
+
+def is_gym_projection(map_el):
+ """ Return true if the map projection matches that used by VEarth, Google, OSM, etc.
+
+ Will be useful for a zoom-level shorthand for scale-denominator.
+ """
+ # expected
+ gym = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null'
+ gym = dict([p.split('=') for p in gym.split() if '=' in p])
+
+ # observed
+ srs = map_el.get('srs', '')
+ srs = dict([p.split('=') for p in srs.split() if '=' in p])
+
+ for p in gym:
+ if srs.get(p, None) != gym.get(p, None):
+ return False
+
+ return True
+
+def extract_declarations(map_el, base):
+ """ Given a Map element and a URL base string, remove and return a complete
+ list of style declarations from any Stylesheet elements found within.
+ """
+ declarations = []
+
+ for stylesheet in map_el.findall('Stylesheet'):
+ map_el.remove(stylesheet)
+
+ if 'src' in stylesheet.attrib:
+ url = urlparse.urljoin(base, stylesheet.attrib['src'])
+ styles, local_base = urllib.urlopen(url).read(), url
+
+ elif stylesheet.text:
+ styles, local_base = stylesheet.text, base
+
+ else:
+ continue
+
+ rulesets = style.parse_stylesheet(styles, base=local_base, is_gym=is_gym_projection(map_el))
+ declarations += style.unroll_rulesets(rulesets)
+
+ return declarations
+
+def make_rule_element(range, filter, *symbolizer_els):
+ """ Given a Range, return a Rule element prepopulated
+ with applicable min/max scale denominator elements.
+ """
+ rule_el = Element('Rule')
+
+ if range.leftedge:
+ minscale = Element('MinScaleDenominator')
+ rule_el.append(minscale)
+
+ if range.leftop is ge:
+ minscale.text = str(range.leftedge)
+ elif range.leftop is gt:
+ minscale.text = str(range.leftedge + 1)
+
+ if range.rightedge:
+ maxscale = Element('MaxScaleDenominator')
+ rule_el.append(maxscale)
+
+ if range.rightop is le:
+ maxscale.text = str(range.rightedge)
+ elif range.rightop is lt:
+ maxscale.text = str(range.rightedge - 1)
+
+ filter_text = ' and '.join("[%s] %s '%s'" % (test.arg1, test.op, test.arg2) for test in filter.tests)
+
+ if filter_text:
+ filter_el = Element('Filter')
+ filter_el.text = filter_text
+ rule_el.append(filter_el)
+
+ rule_el.tail = '\n '
+
+ for symbolizer_el in symbolizer_els:
+ if symbolizer_el != False:
+ rule_el.append(symbolizer_el)
+
+ return rule_el
+
+def insert_layer_style(map_el, layer_el, style_el):
+ """ Given a Map element, a Layer element, and a Style element, insert the
+ Style element into the flow and point to it from the Layer element.
+ """
+ style_el.tail = '\n '
+ map_el.insert(map_el._children.index(layer_el), style_el)
+
+ stylename = Element('StyleName')
+ stylename.text = style_el.get('name')
+ stylename.tail = '\n '
+ layer_el.insert(layer_el._children.index(layer_el.find('Datasource')), stylename)
+
+def is_applicable_selector(selector, range, filter):
+ """ Given a Selector, Range, and Filter, return True if the Selector is
+ compatible with the given Range and Filter, and False if they contradict.
+ """
+ if not selector.inRange(range.midpoint()) and selector.isRanged():
+ return False
+
+ for test in selector.allTests():
+ if not test.inFilter(filter.tests):
+ return False
+
+ return True
+
+def add_map_style(map_el, declarations):
+ """
+ """
+ property_map = {'map-bgcolor': 'bgcolor'}
+
+ for dec in declarations:
+ if dec.property.name in property_map:
+ map_el.set(property_map[dec.property.name], str(dec.value))
+
+def ranged_filtered_property_declarations(declarations, property_map):
+ """ Given a list of declarations and a map of properties, return a list
+ of rule tuples: (range, filter, parameter_values), where parameter_values
+ is a list of (parameter, value) tuples.
+ """
+ # just the ones we care about here
+ declarations = [dec for dec in declarations if dec.property.name in property_map]
+
+ # a place to put rules
+ rules = []
+
+ # a matrix of checks for filter and min/max scale limitations
+ ranges = selectors_ranges([dec.selector for dec in declarations])
+ filters = selectors_filters([dec.selector for dec in declarations])
+
+ for range in ranges:
+ for filter in filters:
+ rule = (range, filter, {})
+
+ # collect all the applicable declarations into a list of parameters and values
+ for dec in declarations:
+ if is_applicable_selector(dec.selector, range, filter):
+ parameter = property_map[dec.property.name]
+ rule[2][parameter] = dec.value
+
+ if rule[2]:
+ rules.append(rule)
+
+ return rules
+
+def add_polygon_style(map_el, layer_el, declarations):
+ """ Given a Map element, a Layer element, and a list of declarations,
+ create a new Style element with a PolygonSymbolizer, add it to Map
+ and refer to it in Layer.
+ """
+ property_map = {'polygon-fill': 'fill', 'polygon-opacity': 'fill-opacity'}
+
+ # a place to put rule elements
+ rule_els = []
+
+ for (range, filter, parameter_values) in ranged_filtered_property_declarations(declarations, property_map):
+ symbolizer_el = Element('PolygonSymbolizer')
+
+ for (parameter, value) in parameter_values.items():
+ parameter = Element('CssParameter', {'name': parameter})
+ parameter.text = str(value)
+ symbolizer_el.append(parameter)
+
+ rule_el = make_rule_element(range, filter, symbolizer_el)
+ rule_els.append(rule_el)
+
+ if rule_els:
+ style_el = Element('Style', {'name': 'polygon style %d' % next_counter()})
+ style_el.text = '\n '
+
+ for rule_el in rule_els:
+ style_el.append(rule_el)
+
+ insert_layer_style(map_el, layer_el, style_el)
+
+def add_line_style(map_el, layer_el, declarations):
+ """ Given a Map element, a Layer element, and a list of declarations,
+ create a new Style element with a LineSymbolizer, add it to Map
+ and refer to it in Layer.
+
+ This function is wise to both line-<foo> and outline-<foo> properties,
+ and will generate pairs of LineSymbolizers if necessary.
+ """
+ property_map = {'line-color': 'stroke', 'line-width': 'stroke-width',
+ 'line-opacity': 'stroke-opacity', 'line-join': 'stroke-linejoin',
+ 'line-cap': 'stroke-linecap', 'line-dasharray': 'stroke-dasharray'}
+
+ # temporarily prepend parameter names with 'in:' and 'out:' to be removed later
+ for (property_name, parameter) in property_map.items():
+ property_map['out' + property_name] = 'out:' + parameter
+ property_map[property_name] = 'in:' + parameter
+
+ # a place to put rule elements
+ rule_els = []
+
+ for (range, filter, parameter_values) in ranged_filtered_property_declarations(declarations, property_map):
+ if 'in:stroke-width' in parameter_values:
+ insymbolizer_el = Element('LineSymbolizer')
+ else:
+ # we can do nothing with a weightless line
+ continue
+
+ if 'out:stroke-width' in parameter_values:
+ outsymbolizer_el = Element('LineSymbolizer')
+ else:
+ # we can do nothing with a weightless outline
+ outsymbolizer_el = False
+
+ for (parameter, value) in parameter_values.items():
+ if parameter.startswith('in:'):
+ # knock off the leading 'in:' from above
+ parameter = Element('CssParameter', {'name': parameter[3:]})
+ parameter.text = str(value)
+ insymbolizer_el.append(parameter)
+
+ elif parameter.startswith('out:') and outsymbolizer_el != False:
+ # for the width...
+ if parameter == 'out:stroke-width':
+ # ...double the weight and add the interior to make a proper outline
+ value = parameter_values['in:stroke-width'].value + 2 * value.value
+
+ # knock off the leading 'out:' from above
+ parameter = Element('CssParameter', {'name': parameter[4:]})
+ parameter.text = str(value)
+ outsymbolizer_el.append(parameter)
+
+ rule_el = make_rule_element(range, filter, outsymbolizer_el, insymbolizer_el)
+ rule_els.append(rule_el)
+
+ if rule_els:
+ style_el = Element('Style', {'name': 'line style %d' % next_counter()})
+ style_el.text = '\n '
+
+ for rule_el in rule_els:
+ style_el.append(rule_el)
+
+ insert_layer_style(map_el, layer_el, style_el)
+
+def add_text_styles(map_el, layer_el, declarations):
+ """ Given a Map element, a Layer element, and a list of declarations,
+ create new Style elements with a TextSymbolizer, add them to Map
+ and refer to them in Layer.
+ """
+ property_map = {'text-face-name': 'face_name', 'text-size': 'size',
+ 'text-ratio': 'text_ratio', 'text-wrap-width': 'wrap_width', 'text-spacing': 'spacing',
+ 'text-label-position-tolerance': 'label_position_tolerance',
+ 'text-max-char-angle-delta': 'max_char_angle_delta', 'text-fill': 'fill',
+ 'text-halo-fill': 'halo_fill', 'text-halo-radius': 'halo_radius',
+ 'text-dx': 'dx', 'text-dy': 'dy',
+ 'text-avoid-edges': 'avoid_edges', 'text-min-distance': 'min_distance',
+ 'text-allow-overlap': 'allow_overlap', 'text-placement': 'placement'}
+
+ # pull out all the names
+ text_names = [dec.selector.elements[1].names[0]
+ for dec in declarations
+ if len(dec.selector.elements) is 2 and len(dec.selector.elements[1].names) is 1]
+
+ # a separate style element for each text name
+ for text_name in set(text_names):
+
+ # just the ones we care about here.
+ # the complicated conditional means: get all declarations that
+ # apply to this text_name specifically, or text in general.
+ name_declarations = [dec for dec in declarations
+ if dec.property.name in property_map
+ and (len(dec.selector.elements) == 1
+ or (len(dec.selector.elements) == 2
+ and dec.selector.elements[1].names[0] in (text_name, '*')))]
+
+ # a place to put rule elements
+ rule_els = []
+
+ for (range, filter, parameter_values) in ranged_filtered_property_declarations(name_declarations, property_map):
+ symbolizer_el = Element('TextSymbolizer')
+
+ for (parameter, value) in parameter_values.items():
+ symbolizer_el.set(parameter, str(value))
+
+ rule_el = make_rule_element(range, filter, symbolizer_el)
+ rule_els.append(rule_el)
+
+ if rule_els:
+ style_el = Element('Style', {'name': 'text style %d (%s)' % (next_counter(), text_name)})
+ style_el.text = '\n '
+
+ for rule_el in rule_els:
+ style_el.append(rule_el)
+
+ insert_layer_style(map_el, layer_el, style_el)
+
+def postprocess_symbolizer_image_file(symbolizer_el, out, temp_name):
+ """ Given a sumbolizer element, output directory name, and temporary
+ file name, find the "file" attribute in the symbolizer and save it
+ to a temporary location as a PING while noting its dimensions.
+ """
+ # read the image to get some more details
+ img_path = symbolizer_el.get('file')
+ img_data = urllib.urlopen(img_path).read()
+ img_file = StringIO.StringIO(img_data)
+ img = PIL.Image.open(img_file)
+
+ # save the image to a tempfile, making it a PNG no matter what
+ (handle, path) = tempfile.mkstemp('.png', 'cascadenik-%s-' % temp_name, out)
+ os.close(handle)
+
+ img.save(path)
+ symbolizer_el.set('file', path)
+ symbolizer_el.set('type', 'png')
+
+ # if no width/height have been provided, set them
+ if not (symbolizer_el.get('width', False) and symbolizer_el.get('height', False)):
+ symbolizer_el.set('width', str(img.size[0]))
+ symbolizer_el.set('height', str(img.size[1]))
+
+def add_point_style(map_el, layer_el, declarations, out=None):
+ """ Given a Map element, a Layer element, and a list of declarations,
+ create a new Style element with a PointSymbolizer, add it to Map
+ and refer to it in Layer.
+
+ Optionally provide an output directory for local copies of image files.
+ """
+ property_map = {'point-file': 'file', 'point-width': 'width',
+ 'point-height': 'height', 'point-type': 'type',
+ 'point-allow-overlap': 'allow_overlap'}
+
+ # a place to put rule elements
+ rule_els = []
+
+ for (range, filter, parameter_values) in ranged_filtered_property_declarations(declarations, property_map):
+ symbolizer_el = Element('PointSymbolizer')
+
+ # collect all the applicable declarations into a symbolizer element
+ for (parameter, value) in parameter_values.items():
+ symbolizer_el.set(parameter, str(value))
+
+ if symbolizer_el.get('file', False):
+ postprocess_symbolizer_image_file(symbolizer_el, out, 'point')
+
+ rule_el = make_rule_element(range, filter, symbolizer_el)
+ rule_els.append(rule_el)
+
+ if rule_els:
+ style_el = Element('Style', {'name': 'point style %d' % next_counter()})
+ style_el.text = '\n '
+
+ for rule_el in rule_els:
+ style_el.append(rule_el)
+
+ insert_layer_style(map_el, layer_el, style_el)
+
+def add_polygon_pattern_style(map_el, layer_el, declarations, out=None):
+ """ Given a Map element, a Layer element, and a list of declarations,
+ create a new Style element with a PolygonPatternSymbolizer, add it to Map
+ and refer to it in Layer.
+
+ Optionally provide an output directory for local copies of image files.
+ """
+ property_map = {'polygon-pattern-file': 'file', 'polygon-pattern-width': 'width',
+ 'polygon-pattern-height': 'height', 'polygon-pattern-type': 'type'}
+
+ # a place to put rule elements
+ rule_els = []
+
+ for (range, filter, parameter_values) in ranged_filtered_property_declarations(declarations, property_map):
+ symbolizer_el = Element('PolygonPatternSymbolizer')
+
+ # collect all the applicable declarations into a symbolizer element
+ for (parameter, value) in parameter_values.items():
+ symbolizer_el.set(parameter, str(value))
+
+ if symbolizer_el.get('file', False):
+ postprocess_symbolizer_image_file(symbolizer_el, out, 'polygon-pattern')
+
+ rule_el = make_rule_element(range, filter, symbolizer_el)
+ rule_els.append(rule_el)
+
+ if rule_els:
+ style_el = Element('Style', {'name': 'polygon pattern style %d' % next_counter()})
+ style_el.text = '\n '
+
+ for rule_el in rule_els:
+ style_el.append(rule_el)
+
+ insert_layer_style(map_el, layer_el, style_el)
+
+def add_line_pattern_style(map_el, layer_el, declarations, out=None):
+ """ Given a Map element, a Layer element, and a list of declarations,
+ create a new Style element with a LinePatternSymbolizer, add it to Map
+ and refer to it in Layer.
+
+ Optionally provide an output directory for local copies of image files.
+ """
+ property_map = {'line-pattern-file': 'file', 'line-pattern-width': 'width',
+ 'line-pattern-height': 'height', 'line-pattern-type': 'type'}
+
+ # a place to put rule elements
+ rule_els = []
+
+ for (range, filter, parameter_values) in ranged_filtered_property_declarations(declarations, property_map):
+ symbolizer_el = Element('LinePatternSymbolizer')
+
+ # collect all the applicable declarations into a symbolizer element
+ for (parameter, value) in parameter_values.items():
+ symbolizer_el.set(parameter, str(value))
+
+ if symbolizer_el.get('file', False):
+ postprocess_symbolizer_image_file(symbolizer_el, out, 'line-pattern')
+
+ rule_el = make_rule_element(range, filter, symbolizer_el)
+ rule_els.append(rule_el)
+
+ if rule_els:
+ style_el = Element('Style', {'name': 'line pattern style %d' % next_counter()})
+ style_el.text = '\n '
+
+ for rule_el in rule_els:
+ style_el.append(rule_el)
+
+ insert_layer_style(map_el, layer_el, style_el)
+
+def get_applicable_declarations(element, declarations):
+ """ Given an XML element and a list of declarations, return the ones
+ that match as a list of (property, value, selector) tuples.
+ """
+ element_tag = element.tag
+ element_id = element.get('id', None)
+ element_classes = element.get('class', '').split()
+
+ return [dec for dec in declarations
+ if dec.selector.matches(element_tag, element_id, element_classes)]
+
+def compile(src, dir=None):
+ """
+ """
+ doc = xml.etree.ElementTree.parse(urllib.urlopen(src))
+ map = doc.getroot()
+
+ declarations = extract_declarations(map, src)
+
+ add_map_style(map, get_applicable_declarations(map, declarations))
+
+ for layer in map.findall('Layer'):
+ layer_declarations = get_applicable_declarations(layer, declarations)
+
+ #pprint.PrettyPrinter().pprint(layer_declarations)
+
+ add_polygon_style(map, layer, layer_declarations)
+ add_polygon_pattern_style(map, layer, layer_declarations, dir)
+ add_line_style(map, layer, layer_declarations)
+ add_line_pattern_style(map, layer, layer_declarations, dir)
+ add_text_styles(map, layer, layer_declarations)
+ add_point_style(map, layer, layer_declarations, dir)
+
+ layer.set('name', 'layer %d' % next_counter())
+
+ if 'id' in layer.attrib:
+ del layer.attrib['id']
+
+ if 'class' in layer.attrib:
+ del layer.attrib['class']
+
+ if layer_declarations:
+ layer.set('status', 'on')
+ else:
+ layer.set('status', 'off')
+
+ out = StringIO.StringIO()
+ doc.write(out)
+
+ return out.getvalue()
+
+parser = optparse.OptionParser(usage="""compile.py [options]
+
+Example map of San Francisco and Oakland:
+ python compose.py -o out.png -p MICROSOFT_ROAD -d 800 800 -c 37.8 -122.3 11
+
+Map provider and output image dimensions MUST be specified before extent
+or center/zoom. Multiple extents and center/zooms may be specified, but
+only the last will be used.""")
+
+parser.add_option('-d', '--dir', dest='directory',
+ help='Write to output directory')
+
+if __name__ == '__main__':
+
+ (options, args) = parser.parse_args()
+
+ layersfile = args[0]
+
+ sys.exit(main(layersfile, options.directory))
22 doc/example1.mml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Map srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
+ <Stylesheet><![CDATA[
+ Map
+ {
+ map-bgcolor: #69f;
+ }
+
+ Layer
+ {
+ line-width: 1;
+ line-color: #696;
+ polygon-fill: #6f9;
+ }
+ ]]></Stylesheet>
+ <Layer srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
+ <Datasource>
+ <Parameter name="type">shape</Parameter>
+ <Parameter name="file">world_borders</Parameter>
+ </Datasource>
+ </Layer>
+</Map>
BIN doc/example1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 doc/example2.mml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Map srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
+ <Stylesheet><![CDATA[
+ *
+ {
+ map-bgcolor: #69f;
+ }
+
+ #world-borders
+ {
+ line-width: 1;
+ line-color: #696;
+ polygon-fill: #6f9;
+ }
+
+ #world-borders NAME
+ {
+ text-face-name: "DejaVu Sans Book";
+ text-size: 10;
+ text-fill: #000;
+ text-halo-fill: #9ff;
+ text-halo-radius: 2;
+ text-placement: point;
+ text-wrap-width: 50;
+ text-avoid-edges: true;
+ }
+ ]]></Stylesheet>
+ <Layer id="world-borders" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
+ <Datasource>
+ <Parameter name="type">shape</Parameter>
+ <Parameter name="file">world_borders</Parameter>
+ </Datasource>
+ </Layer>
+</Map>
BIN doc/example2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 doc/example3.mml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Map srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null">
+ <Stylesheet><![CDATA[
+ *
+ {
+ map-bgcolor: #69f;
+ }
+
+ .world-borders
+ {
+ line-width: 1;
+ line-color: #696;
+ polygon-fill: #6f9;
+ }
+
+ .world-borders NAME
+ {
+ text-face-name: "DejaVu Sans Book";
+ text-size: 10;
+ text-fill: #000;
+ text-halo-fill: #9ff;
+ text-halo-radius: 2;
+ text-placement: point;
+ text-wrap-width: 50;
+ text-avoid-edges: true;
+
+ point-file: url("purple-point.png");
+ text-dy: 10;
+ }
+ ]]></Stylesheet>
+ <Layer class="world-borders" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
+ <Datasource>
+ <Parameter name="type">shape</Parameter>
+ <Parameter name="file">world_borders</Parameter>
+ </Datasource>
+ </Layer>
+</Map>
BIN doc/example3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 doc/example4.mml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Map srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null">
+ <Stylesheet src="example4.mss"/>
+ <Layer class="world-borders countries" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
+ <Datasource>
+ <Parameter name="type">shape</Parameter>
+ <Parameter name="file">world_borders</Parameter>
+ </Datasource>
+ </Layer>
+</Map>
40 doc/example4.mss
@@ -0,0 +1,40 @@
+*
+{
+ map-bgcolor: #69f;
+}
+
+.world-borders, .some-other-class
+{
+ line-width: 0.5;
+ line-color: #030;
+ polygon-fill: #6f9;
+
+ point-file: url("purple-point.png");
+ polygon-pattern-file: url("http://www.inkycircus.com/jargon/images/grass_by_conformity.jpg");
+}
+
+.world-borders.countries[zoom>10] NAME
+{
+ text-face-name: "DejaVu Sans Book";
+ text-size: 10;
+ text-fill: #000;
+ text-halo-fill: #9ff;
+ text-halo-radius: 2;
+ text-placement: point;
+ text-wrap-width: 50;
+ text-avoid-edges: true;
+ text-dy: 10;
+}
+
+.world-borders.countries[zoom<=10] FIPS
+{
+ text-face-name: "DejaVu Sans Book";
+ text-size: 10;
+ text-fill: #000;
+ text-halo-fill: #9ff;
+ text-halo-radius: 2;
+ text-placement: point;
+ text-wrap-width: 50;
+ text-avoid-edges: true;
+ text-dy: 10;
+}
BIN doc/example4.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
429 doc/index.html
@@ -0,0 +1,429 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <title>Cascading Stylesheets For Mapnik</title>
+ <style type="text/css" title="text/css">
+ <!--
+
+ body {
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 16px;
+ margin: 2em;
+ }
+
+ pre {
+ font-size: 11px;
+ line-height: 12px;
+ color: #666;
+ background-color: #eee;
+ border: 1px solid #ccc;
+ padding: 1em;
+ }
+
+ pre em {
+ color: #333;
+ font-style: normal;
+ background-color: #f8f8f8;
+ }
+
+ img {
+ border: 1px solid #666;
+ }
+
+ -->
+ </style>
+</head>
+<body>
+
+<h1>Cascading Stylesheets For Mapnik</h1>
+
+<p>
+ The examples here demonstrate some of the basic usage of Mapnik cascading
+ style sheets. Most of the syntax has been borrowed wholesale from
+ <a href="http://www.w3.org/TR/REC-CSS2/">CSS</a>, so it should be familiar
+ to web designers.
+</p>
+
+<p>
+ The examples show:
+</p>
+
+<ol>
+ <li>Basic fill and stroke styles.</li>
+ <li>Text, ID selectors.</li>
+ <li>Images, class selector, projections.</li>
+ <li>Externally-linked files, expanded class selectors.</li>
+</ol>
+
+<p>
+ The complete library is available from the <a href="http://code.google.com/p/mapnik-utils/">mapnik-utils Google Code project</a>.
+</p>
+
+<h2>Example 1</h2>
+
+<p>
+ We start with a basic set of styles: a background color for the whole map,
+ and an outline and fill for each country in the world borders data set.
+</p>
+
+<h3>Input example1.mml</h3>
+
+<p>
+ There are two blocks in the Stylesheet portion of the file. One will define
+ the map background color, the other will define line and polygon
+ symbolizers for the world borders layer.
+</p>
+
+<pre>&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;Map srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"&gt;
+<em> &lt;Stylesheet&gt;&lt;![CDATA[
+ Map
+ {
+ map-bgcolor: #69f;
+ }
+
+ Layer
+ {
+ line-width: 1;
+ line-color: #696;
+ polygon-fill: #6f9;
+ }
+ ]]&gt;&lt;/Stylesheet&gt;</em>
+ &lt;Layer srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"&gt;
+ &lt;Datasource&gt;
+ &lt;Parameter name="type"&gt;shape&lt;/Parameter&gt;
+ &lt;Parameter name="file"&gt;world_borders&lt;/Parameter&gt;
+ &lt;/Datasource&gt;
+ &lt;/Layer&gt;
+&lt;/Map&gt;</pre>
+
+<h3>Output XML</h3>
+
+<p>
+ Converting <i>example1.mml</i> to <i>output.xml</i> looks like this:<br>
+ <code>python compile.py example1.mml > output.xml</code>
+</p>
+
+<pre>&lt;Map <em>bgcolor="#6699ff"</em> srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"&gt;
+<em> &lt;Style name="poly style 1"&gt;
+ &lt;Rule&gt;&lt;PolygonSymbolizer&gt;&lt;CssParameter name="fill"&gt;#66ff99&lt;/CssParameter&gt;&lt;/PolygonSymbolizer&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;
+ &lt;Style name="line style 2"&gt;
+ &lt;Rule&gt;&lt;LineSymbolizer&gt;&lt;CssParameter name="stroke"&gt;#669966&lt;/CssParameter&gt;&lt;CssParameter name="stroke-width"&gt;1&lt;/CssParameter&gt;&lt;/LineSymbolizer&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;</em>
+ &lt;Layer name="layer 3" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs" status="on"&gt;
+<em> &lt;StyleName&gt;poly style 1&lt;/StyleName&gt;
+ &lt;StyleName&gt;line style 2&lt;/StyleName&gt;</em>
+ &lt;Datasource&gt;
+ &lt;Parameter name="type"&gt;shape&lt;/Parameter&gt;
+ &lt;Parameter name="file"&gt;world_borders&lt;/Parameter&gt;
+ &lt;/Datasource&gt;
+ &lt;/Layer&gt;
+&lt;/Map&gt;</pre>
+
+<h3>Rendered Image</h3>
+
+<p><img src="example1.png"></p>
+
+<h2>Example 2</h2>
+
+<p>
+ Here we've added a separate block that labels each country. The selector
+ for the block is <i>Layer NAME</i>, where <i>NAME</i> is the field from the
+ data set to use as a label source. Note how it appears as <i>name="NAME"</i>
+ in the output XML below.
+</p>
+
+<p>
+ We've also added an <i>id</i> attribute to the Layer, and used it in place
+ of <em>Layer</em> for two of the selectors. The first block has been changed
+ to a wildcard, <i>*</i>.
+</p>
+
+<h3>Input example2.mml</h3>
+
+<pre>&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;Map srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"&gt;
+ &lt;Stylesheet&gt;&lt;![CDATA[
+ <em>*</em>
+ {
+ map-bgcolor: #69f;
+ }
+
+ <em>#world-borders</em>
+ {
+ line-width: 1;
+ line-color: #696;
+ polygon-fill: #6f9;
+ }
+
+<em> #world-borders NAME
+ {
+ text-face-name: "DejaVu Sans Book";
+ text-size: 10;
+ text-fill: #000;
+ text-halo-fill: #9ff;
+ text-halo-radius: 2;
+ text-placement: point;
+ text-wrap-width: 50;
+ text-avoid-edges: true;
+ }</em>
+ ]]&gt;&lt;/Stylesheet&gt;
+ &lt;Layer <em>id="world-borders"</em> srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"&gt;
+ &lt;Datasource&gt;
+ &lt;Parameter name="type"&gt;shape&lt;/Parameter&gt;
+ &lt;Parameter name="file"&gt;world_borders&lt;/Parameter&gt;
+ &lt;/Datasource&gt;
+ &lt;/Layer&gt;
+&lt;/Map&gt;</pre>
+
+<h3>Output XML</h3>
+
+<pre>&lt;Map bgcolor="#6699ff" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"&gt;
+ &lt;Style name="poly style 1"&gt;
+ &lt;Rule&gt;&lt;PolygonSymbolizer&gt;&lt;CssParameter name="fill"&gt;#66ff99&lt;/CssParameter&gt;&lt;/PolygonSymbolizer&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;
+ &lt;Style name="line style 2"&gt;
+ &lt;Rule&gt;&lt;LineSymbolizer&gt;&lt;CssParameter name="stroke"&gt;#669966&lt;/CssParameter&gt;&lt;CssParameter name="stroke-width"&gt;1&lt;/CssParameter&gt;&lt;/LineSymbolizer&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;
+<em> &lt;Style name="text style 3 (NAME)"&gt;
+ &lt;Rule&gt;&lt;TextSymbolizer avoid_edges="true" face_name="DejaVu Sans Book" fill="#000000" halo_fill="#99ffff" halo_radius="2" name="NAME" placement="point" size="10" wrap_width="50" /&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;</em>
+ &lt;Layer name="layer 4" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs" status="on"&gt;
+ &lt;StyleName&gt;poly style 1&lt;/StyleName&gt;
+ &lt;StyleName&gt;line style 2&lt;/StyleName&gt;
+<em> &lt;StyleName&gt;text style 3 (NAME)&lt;/StyleName&gt;</em>
+ &lt;Datasource&gt;
+ &lt;Parameter name="type"&gt;shape&lt;/Parameter&gt;
+ &lt;Parameter name="file"&gt;world_borders&lt;/Parameter&gt;
+ &lt;/Datasource&gt;
+ &lt;/Layer&gt;
+&lt;/Map&gt;</pre>
+
+<h3>Rendered Image</h3>
+
+<p><img src="example2.png"></p>
+
+<h2>Example 3</h2>
+
+<p>
+ It's possible to refer to external images for use as point markers, such
+ as this use of <i>purple-point.png</i> to accompany each country label.
+ The point is found relative to <i>example3.mml</i>, and is referred to by
+ an absolute path in the output XML. Although Mapnik can only accept
+ PNG and TIFF images, we use <a href="http://www.pythonware.com/library/">PIL</a>
+ to convert all images to PNG's and save them in a temporary location for
+ Mapnik's use. All the text has been offset by 10 pixels to make room.
+</p>
+
+<p>
+ The ID selectors from above have been replaced by class selectors, referring
+ to the <i>class="world-borders"</i> attribute of the Layer. These work just like
+ you'd expect from CSS. It is expected that a document has just one example of
+ each ID, but potentially many mixed examples of each class.
+</p>
+
+<p>
+ We've also switched to a mercator projection.
+</p>
+
+<h3>Input example3.mml</h3>
+
+<pre>&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;Map <em>srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null"</em>&gt;
+ &lt;Stylesheet&gt;&lt;![CDATA[
+ *
+ {
+ map-bgcolor: #69f;
+ }
+
+ <em>.world-borders</em>
+ {
+ line-width: 1;
+ line-color: #696;
+ polygon-fill: #6f9;
+ }
+
+ <em>.world-borders NAME</em>
+ {
+ text-face-name: "DejaVu Sans Book";
+ text-size: 10;
+ text-fill: #000;
+ text-halo-fill: #9ff;
+ text-halo-radius: 2;
+ text-placement: point;
+ text-wrap-width: 50;
+ text-avoid-edges: true;
+
+<em> point-file: url("purple-point.png");
+ text-dy: 10;</em>
+ }
+ ]]&gt;&lt;/Stylesheet&gt;
+ &lt;Layer <em>class="world-borders"</em> srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"&gt;
+ &lt;Datasource&gt;
+ &lt;Parameter name="type"&gt;shape&lt;/Parameter&gt;
+ &lt;Parameter name="file"&gt;world_borders&lt;/Parameter&gt;
+ &lt;/Datasource&gt;
+ &lt;/Layer&gt;
+&lt;/Map&gt;</pre>
+
+<h3>Output XML</h3>
+
+<pre>&lt;Map bgcolor="#6699ff" <em>srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null"</em>&gt;
+ &lt;Style name="poly style 1"&gt;
+ &lt;Rule&gt;&lt;PolygonSymbolizer&gt;&lt;CssParameter name="fill"&gt;#66ff99&lt;/CssParameter&gt;&lt;/PolygonSymbolizer&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;
+ &lt;Style name="line style 2"&gt;
+ &lt;Rule&gt;&lt;LineSymbolizer&gt;&lt;CssParameter name="stroke"&gt;#669966&lt;/CssParameter&gt;&lt;CssParameter name="stroke-width"&gt;1&lt;/CssParameter&gt;&lt;/LineSymbolizer&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;
+ &lt;Style name="text style 3 (NAME)"&gt;
+ &lt;Rule&gt;&lt;TextSymbolizer avoid_edges="true" dy="10" face_name="DejaVu Sans Book" fill="#000000" halo_fill="#99ffff" halo_radius="2" name="NAME" placement="point" size="10" wrap_width="50" /&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;
+<em> &lt;Style name="point style 4"&gt;
+ &lt;Rule&gt;&lt;PointSymbolizer file="/tmp/cascadenik-point-JqnnpU.png" height="8" type="png" width="8" /&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;</em>
+ &lt;Layer name="layer 5" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs" status="on"&gt;
+ &lt;StyleName&gt;poly style 1&lt;/StyleName&gt;
+ &lt;StyleName&gt;line style 2&lt;/StyleName&gt;
+ &lt;StyleName&gt;text style 3 (NAME)&lt;/StyleName&gt;
+ &lt;StyleName&gt;point style 4&lt;/StyleName&gt;
+ &lt;Datasource&gt;
+ &lt;Parameter name="type"&gt;shape&lt;/Parameter&gt;
+ &lt;Parameter name="file"&gt;world_borders&lt;/Parameter&gt;
+ &lt;/Datasource&gt;
+ &lt;/Layer&gt;
+&lt;/Map&gt;</pre>
+
+<h3>Rendered Image</h3>
+
+<p><img src="example3.png"></p>
+
+<h2>Example 4</h2>
+
+<p>
+ Since the stylesheet has started to grow a little large, we've moved it off
+ to a separate file, <i>example4.mss</i>. We've introduced a pattern background
+ for each country pulled from a remote server by URL.
+</p>
+
+<p>
+ The selector syntax has been extended in two ways.
+</p>
+
+<p>
+ First, note the scale selection attribute selector syntax, e.g. <i>[zoom>10]</i>.
+ The use of the <i>zoom</i> variable as a shorthand for MinScaleDenominator
+ and MaxScaleDenominator is only meaningful when we use this exact projection
+ SRS, which matches that used by OpenStreetMap, Google Maps, Virtual Earth,
+ and others. If we were using some other projection but wanted to use Mapnik's
+ scale support, we'd use something like <i>[scale-denominator>=400000]</i> instead.
+</p>
+
+<p>
+ Second, we see how multiple class names can be give to a single Layer, and how
+ multiple matches can be used in selectors. In these examples there's only a single
+ defined layer, but the <em>.some-other-class</em> line in <i>example4.mss</i>
+ shows how it's possible to use single blocks of rules to affect numerous layers
+ concurrently.
+</p>
+
+<h3>Input example4.mml</h3>
+
+<pre>&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;Map srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null"&gt;
+<em> &lt;Stylesheet src="example4.mss"/&gt;</em>
+ &lt;Layer <em>class="world-borders countries"</em> srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs"&gt;
+ &lt;Datasource&gt;
+ &lt;Parameter name="type"&gt;shape&lt;/Parameter&gt;
+ &lt;Parameter name="file"&gt;world_borders&lt;/Parameter&gt;
+ &lt;/Datasource&gt;
+ &lt;/Layer&gt;
+&lt;/Map&gt;</pre>
+
+<h3>example4.mss</h3>
+
+<pre>*
+{
+ map-bgcolor: #69f;
+}
+
+<em>.world-borders, .some-other-class</em>
+{
+ line-width: 0.5;
+ line-color: #030;
+ polygon-fill: #6f9;
+
+ point-file: url("purple-point.png");
+<em> polygon-pattern-file: url("http://www.inkycircus.com/jargon/images/grass_by_conformity.jpg");</em>
+}
+
+<em>.world-borders.countries[zoom>10] NAME</em>
+{
+ text-face-name: "DejaVu Sans Book";
+ text-size: 10;
+ text-fill: #000;
+ text-halo-fill: #9ff;
+ text-halo-radius: 2;
+ text-placement: point;
+ text-wrap-width: 50;
+ text-avoid-edges: true;
+ text-dy: 10;
+}
+
+<em>.world-borders.countries[zoom<=10] FIPS</em>
+{
+ text-face-name: "DejaVu Sans Book";
+ text-size: 10;
+ text-fill: #000;
+ text-halo-fill: #9ff;
+ text-halo-radius: 2;
+ text-placement: point;
+ text-wrap-width: 50;
+ text-avoid-edges: true;
+ text-dy: 10;
+}</pre>
+
+<h3>Output XML</h3>
+
+<pre>&lt;Map bgcolor="#6699ff" srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null"&gt;
+ &lt;Style name="poly style 1"&gt;
+ &lt;Rule&gt;&lt;PolygonSymbolizer&gt;&lt;CssParameter name="fill"&gt;#66ff99&lt;/CssParameter&gt;&lt;/PolygonSymbolizer&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;
+<em> &lt;Style name="pattern style 2"&gt;
+ &lt;Rule&gt;&lt;PolygonPatternSymbolizer file="/tmp/cascadenik-pattern-8q8uHI.png" height="352" type="png" width="470" /&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;</em>
+ &lt;Style name="line style 3"&gt;
+ &lt;Rule&gt;&lt;LineSymbolizer&gt;&lt;CssParameter name="stroke"&gt;#003300&lt;/CssParameter&gt;&lt;CssParameter name="stroke-width"&gt;0.5&lt;/CssParameter&gt;&lt;/LineSymbolizer&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;
+<em> &lt;Style name="text style 4 (FIPS)"&gt;
+ &lt;Rule&gt;&lt;MinScaleDenominator&gt;400000&lt;/MinScaleDenominator&gt;&lt;TextSymbolizer avoid_edges="true" dy="10" face_name="DejaVu Sans Book" fill="#000000" halo_fill="#99ffff" halo_radius="2" name="FIPS" placement="point" size="10" wrap_width="50" /&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;
+ &lt;Style name="text style 5 (NAME)"&gt;
+ &lt;Rule&gt;&lt;MaxScaleDenominator&gt;399999&lt;/MaxScaleDenominator&gt;&lt;TextSymbolizer avoid_edges="true" dy="10" face_name="DejaVu Sans Book" fill="#000000" halo_fill="#99ffff" halo_radius="2" name="NAME" placement="point" size="10" wrap_width="50" /&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;</em>
+ &lt;Style name="point style 6"&gt;
+ &lt;Rule&gt;&lt;PointSymbolizer file="/tmp/cascadenik-point-E-iU9U.png" height="8" type="png" width="8" /&gt;&lt;/Rule&gt;
+ &lt;/Style&gt;
+ &lt;Layer name="layer 7" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs" status="on"&gt;
+ &lt;StyleName&gt;poly style 1&lt;/StyleName&gt;
+<em> &lt;StyleName&gt;pattern style 2&lt;/StyleName&gt;</em>
+ &lt;StyleName&gt;line style 3&lt;/StyleName&gt;
+<em> &lt;StyleName&gt;text style 4 (FIPS)&lt;/StyleName&gt;
+ &lt;StyleName&gt;text style 5 (NAME)&lt;/StyleName&gt;</em>
+ &lt;StyleName&gt;point style 6&lt;/StyleName&gt;
+ &lt;Datasource&gt;
+ &lt;Parameter name="type"&gt;shape&lt;/Parameter&gt;
+ &lt;Parameter name="file"&gt;world_borders&lt;/Parameter&gt;
+ &lt;/Datasource&gt;
+ &lt;/Layer&gt;
+&lt;/Map&gt;</pre>
+
+<h3>Rendered Image</h3>
+
+<p><img src="example4.png"></p>
+
+</body>
+</html>
BIN doc/purple-point.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 example.mml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Map srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null">
+ <Stylesheet>
+ Map { map-bgcolor: #ccc; }
+ </Stylesheet>
+ <Stylesheet src="example.mss"/>
+ <Layer id="world-borders" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
+ <Datasource>
+ <Parameter name="type">shape</Parameter>
+ <Parameter name="file">world_borders</Parameter>
+ </Datasource>
+ </Layer>
+</Map>
41 example.mss
@@ -0,0 +1,41 @@
+*
+{
+ line-width: 1;
+ line-color: #999;
+ polygon-fill: #fff;
+
+ /* point-file: url("purple-point.png"); */
+ /* pattern-file: url("http://www.istockphoto.com/file_thumbview_approve/3055566/2/istockphoto_3055566_crazy_background.jpg"); */
+}
+
+*[zoom>=6][zoom<12]
+{
+ line-color: #f90;
+}
+
+*[zoom=12]
+{
+ line-color: #f0f;
+}
+
+*[zoom>12]
+{
+ line-color: #f00;
+}
+
+Layer
+{
+ text-face-name: "DejaVu Sans Book";
+ text-size: 10;
+ text-placement: point;
+}
+
+#world-borders[zoom<10] NAME
+{
+ text-fill: #333;
+}
+
+*[zoom>=10] FIPS
+{
+ text-fill: #000;
+}
899 style.py
@@ -0,0 +1,899 @@
+import re
+import sys
+import pprint
+import urlparse
+import operator
+from binascii import unhexlify as unhex
+from cssutils.tokenize2 import Tokenizer as cssTokenizer
+
+# recognized properties
+
+def main(file):
+ """ Given an input file containing nothing but styles, print out an
+ unrolled list of declarations in cascade order.
+ """
+ input = open(file, 'r').read()
+ rulesets = parse_stylesheet(input)
+
+ for dec in unroll_rulesets(rulesets):
+ print dec.selector,
+ print '{',
+ print dec.property.name+':',
+
+ if properties[dec.property.name] in (color, boolean, numbers):
+ print str(dec.value.value)+';',
+
+ elif properties[dec.property.name] is uri:
+ print 'url("'+str(dec.value.value)+'");',
+
+ elif properties[dec.property.name] is str:
+ print '"'+str(dec.value.value)+'";',
+
+ elif properties[dec.property.name] in (int, float) or type(properties[dec.property.name]) is tuple:
+ print str(dec.value.value)+';',
+
+ print '}'
+
+ return 0
+
+class color:
+ def __init__(self, r, g, b):
+ self.channels = r, g, b
+
+ def __repr__(self):
+ return '#%02x%02x%02x' % self.channels
+
+ def __str__(self):
+ return repr(self)
+
+class uri:
+ def __init__(self, address, base=None):
+ if base:
+ self.address = urlparse.urljoin(base, address)
+ else:
+ self.address = address
+
+ def __repr__(self):
+ return str(self.address) #'url("%(address)s")' % self.__dict__
+
+ def __str__(self):
+ return repr(self)
+
+class boolean:
+ def __init__(self, value):
+ self.value = value
+
+ def __repr__(self):
+ if self.value:
+ return 'true'
+ else:
+ return 'false'
+
+ def __str__(self):
+ return repr(self)
+
+class numbers:
+ def __init__(self, *values):
+ self.values = values
+
+ def __repr__(self):
+ return ','.join(map(str, self.values))
+
+ def __str__(self):
+ return repr(self)
+
+properties = {
+ #--------------- map
+
+ #
+ 'map-bgcolor': color,
+
+ #--------------- polygon symbolizer
+
+ #
+ 'polygon-fill': color,
+
+ #
+ 'polygon-opacity': float,
+
+ #--------------- line symbolizer
+
+ # CSS colour (default "black")
+ 'line-color': color,
+
+ # 0.0 - n (default 1.0)
+ 'line-width': float,
+
+ # 0.0 - 1.0 (default 1.0)
+ 'line-opacity': float,
+
+ # miter, round, bevel (default miter)
+ 'line-join': ('miter', 'round', 'bevel'),
+
+ # round, butt, square (default butt)
+ 'line-cap': ('butt', 'round', 'square'),
+
+ # d0,d1, ... (default none)
+ 'line-dasharray': numbers, # Number(s)
+
+ #--------------- line symbolizer for outlines
+
+ # CSS colour (default "black")
+ 'outline-color': color,
+
+ # 0.0 - n (default 1.0)
+ 'outline-width': float,
+
+ # 0.0 - 1.0 (default 1.0)
+ 'outline-opacity': float,
+
+ # miter, round, bevel (default miter)
+ 'outline-join': ('miter', 'round', 'bevel'),
+
+ # round, butt, square (default butt)
+ 'outline-cap': ('butt', 'round', 'square'),
+
+ # d0,d1, ... (default none)
+ 'outline-dasharray': None, # Number(s)
+
+ #--------------- text symbolizer
+
+ # Font name
+ 'text-face-name': str,
+
+ # Font size
+ 'text-size': int,
+
+ # ?
+ 'text-ratio': None, # ?
+
+ # length before wrapping long names
+ 'text-wrap-width': int,
+
+ # space between repeated labels
+ 'text-spacing': int,
+
+ # allow labels to be moved from their point
+ 'text-label-position-tolerance': None, # ?
+
+ # Maximum angle (in degrees) between two consecutive characters in a label allowed (to stop placing labels around sharp corners)
+ 'text-max-char-angle-delta': int,
+
+ # Color of the fill ie #FFFFFF
+ 'text-fill': color,
+
+ # Color of the halo
+ 'text-halo-fill': color,
+
+ # Radius of the halo in whole pixels, fractional pixels are not accepted
+ 'text-halo-radius': int,
+
+ # displace label by fixed amount on either axis.
+ 'text-dx': int,
+ 'text-dy': int,
+
+ # Boolean to avoid labeling near intersection edges.
+ 'text-avoid-edges': boolean,
+
+ # Minimum distance between repeated labels such as street names or shield symbols
+ 'text-min-distance': int,
+
+ # Allow labels to overlap other labels
+ 'text-allow-overlap': boolean,
+
+ # "line" to label along lines instead of by point
+ 'text-placement': ('point', 'line'),
+
+ #--------------- point symbolizer
+
+ # path to image file
+ 'point-file': uri, # none
+
+ # px (default 4), generally omit this and let PIL handle it
+ 'point-width': int,
+ 'point-height': int,
+
+ # image type: png or tiff, omitted thanks to PIL
+ 'point-type': None,
+
+ # true/false
+ 'point-allow-overlap': boolean,
+
+ #--------------- polygon pattern symbolizer
+
+ # path to image file (default none)
+ 'polygon-pattern-file': uri,
+
+ # px (default 4), generally omit this and let PIL handle it
+ 'polygon-pattern-width': int,
+ 'polygon-pattern-height': int,
+
+ # image type: png or tiff, omitted thanks to PIL
+ 'polygon-pattern-type': None,
+
+ #--------------- line pattern symbolizer
+
+ # path to image file (default none)
+ 'line-pattern-file': uri,
+
+ # px (default 4), generally omit this and let PIL handle it
+ 'line-pattern-width': int,
+ 'line-pattern-height': int,
+
+ # image type: png or tiff, omitted thanks to PIL
+ 'line-pattern-type': None,
+
+ #--------------- shield symbolizer
+
+ #
+ 'shield-name': None, # (use selector for this)
+
+ #
+ 'shield-face-name': str,
+
+ #
+ 'shield-size': None, # ?
+
+ #
+ 'shield-fill': color,
+
+ #
+ 'shield-file': uri,
+
+ #
+ 'shield-type': None, # png, tiff (derived from file)
+
+ #
+ 'shield-width': int,
+
+ #
+ 'shield-height': int
+}
+
+class ParseException(Exception):
+
+ def __init__(self, msg, line, col):
+ Exception.__init__(self, '%(msg)s (line %(line)d, column %(col)d)' % locals())
+
+class Declaration:
+ """ Bundle with a selector, single property and value.
+ """
+ def __init__(self, selector, property, value, sort_key):
+ self.selector = selector
+ self.property = property
+ self.value = value
+ self.sort_key = sort_key
+
+ def __repr__(self):
+ return '%(selector)s { %(property)s: %(value)s }' % self.__dict__
+
+class Selector:
+ """ Represents a complete selector with elements and attribute checks.
+ """
+ def __init__(self, *elements):
+ assert len(elements) in (1, 2)
+ assert elements[0].names[0] in ('Map', 'Layer') or elements[0].names[0][0] in ('.', '#', '*')
+ assert len(elements) == 1 or not elements[1].countTests()
+ assert len(elements) == 1 or not elements[1].countIDs()
+ assert len(elements) == 1 or not elements[1].countClasses()
+
+ self.elements = elements[:]
+
+ def convertZoomTests(self):
+ """ Modify the tests on this selector to use mapnik-friendly
+ scale-denominator instead of shorthand zoom.
+ """
+ # somewhat-fudged values for mapniks' scale denominator at a range
+ # of zoom levels when using the Google/VEarth mercator projection.
+ zooms = {
+ 1: (200000000, 500000000),
+ 2: (100000000, 200000000),
+ 3: (50000000, 100000000),
+ 4: (25000000, 50000000),
+ 5: (12500000, 25000000),
+ 6: (6500000, 12500000),
+ 7: (3000000, 6500000),
+ 8: (1500000, 3000000),
+ 9: (750000, 1500000),
+ 10: (400000, 750000),
+ 11: (200000, 400000),
+ 12: (100000, 200000),
+ 13: (50000, 100000),
+ 14: (25000, 50000),
+ 15: (12500, 25000),
+ 16: (5000, 12500),
+ 17: (2500, 5000),
+ 18: (1000, 2500)
+ }
+
+ for test in self.elements[0].tests:
+ if test.arg1 == 'zoom':
+ test.arg1 = 'scale-denominator'
+
+ if test.op == '=':
+ # zoom level equality implies two tests, so we add one and modify one
+ self.elements[0].addTest(SelectorAttributeTest('scale-denominator', '<=', max(zooms[test.arg2])))
+ test.op, test.arg2 = '>=', min(zooms[test.arg2])
+
+ elif test.op == '<':
+ test.op, test.arg2 = '>', max(zooms[test.arg2])
+ elif test.op == '<=':
+ test.op, test.arg2 = '>=', min(zooms[test.arg2])
+ elif test.op == '>=':
+ test.op, test.arg2 = '<=', max(zooms[test.arg2])
+ elif test.op == '>':
+ test.op, test.arg2 = '<', min(zooms[test.arg2])
+
+
+ def specificity(self):
+ """ Loosely based on http://www.w3.org/TR/REC-CSS2/cascade.html#specificity
+ """
+ ids = sum(a.countIDs() for a in self.elements)
+ non_ids = sum((a.countNames() - a.countIDs()) for a in self.elements)
+ tests = sum(len(a.tests) for a in self.elements)
+
+ return (ids, non_ids, tests)
+
+ def matches(self, tag, id, classes):
+ """ Given an id and a list of classes, return True if this selector would match.
+ """
+ element = self.elements[0]
+ unmatched_ids = [name[1:] for name in element.names if name.startswith('#')]
+ unmatched_classes = [name[1:] for name in element.names if name.startswith('.')]
+ unmatched_tags = [name for name in element.names if name is not '*' and not name.startswith('#') and not name.startswith('.')]
+
+ if tag and tag in unmatched_tags:
+ unmatched_tags.remove(tag)
+
+ if id and id in unmatched_ids:
+ unmatched_ids.remove(id)
+
+ for class_ in classes:
+ if class_ in unmatched_classes:
+ unmatched_classes.remove(class_)
+
+ if unmatched_tags or unmatched_ids or unmatched_classes:
+ return False
+
+ else:
+ return True
+
+ def isRanged(self):
+ """
+ """
+ return bool(self.rangeTests())
+
+ def rangeTests(self):
+ """
+ """
+ return [test for test in self.allTests() if test.isRanged()]
+
+ def allTests(self):
+ """
+ """
+ tests = []
+
+ for test in self.elements[0].tests:
+ tests.append(test)
+
+ return tests
+
+ def inRange(self, value):
+ """
+ """
+ for test in self.rangeTests():
+ if not test.inRange(value):
+ return False
+
+ return True
+
+ def __repr__(self):
+ return ' '.join(repr(a) for a in self.elements)
+
+class SelectorElement:
+ """ One element in selector, with names and tests.
+ """
+ def __init__(self, names=None, tests=None):
+ if names:
+ self.names = names
+ else:
+ self.names = []
+
+ if tests:
+ self.tests = tests
+ else:
+ self.tests = []
+
+ def addName(self, name):
+ self.names.append(name)
+
+ def addTest(self, test):
+ self.tests.append(test)
+
+ def countTests(self):
+ return len(self.tests)
+
+ def countIDs(self):
+ return len([n for n in self.names if n.startswith('#')])
+
+ def countNames(self):
+ return len(self.names)
+
+ def countClasses(self):
+ return len([n for n in self.names if n.startswith('.')])
+
+ def __repr__(self):
+ return ''.join(self.names) + ''.join(repr(t) for t in self.tests)
+
+class SelectorAttributeTest:
+ """ Attribute test for a Selector, i.e. the part that looks like "[foo=bar]"
+ """
+ def __init__(self, arg1, op, arg2):
+ self.op = op
+ self.arg1 = arg1
+ self.arg2 = arg2
+
+ def __repr__(self):
+ return '[%(arg1)s%(op)s%(arg2)s]' % self.__dict__
+
+ def isSimple(self):
+ """
+ """
+ return self.op in ('=', '!=') and not self.isRanged()
+
+ def inverse(self):
+ """
+ """
+ assert self.isSimple()
+
+ if self.op == '=':
+ return SelectorAttributeTest(self.arg1, '!=', self.arg2)
+
+ elif self.op == '!=':
+ return SelectorAttributeTest(self.arg1, '=', self.arg2)
+
+ def isRanged(self):
+ """
+ """
+ return self.arg1 == 'scale-denominator'
+
+ def inRange(self, scale_denominator):
+ """
+ """
+ if not self.isRanged():
+ # always in range
+ return True
+
+ elif self.op == '>' and scale_denominator > self.arg2:
+ return True
+
+ elif self.op == '>=' and scale_denominator >= self.arg2:
+ return True
+
+ elif self.op == '=' and scale_denominator == self.arg2:
+ return True
+
+ elif self.op == '<=' and scale_denominator <= self.arg2:
+ return True
+
+ elif self.op == '<' and scale_denominator < self.arg2:
+ return True
+
+ return False
+
+ def inFilter(self, tests):
+ """ Given a collection of tests, return false if this test contradicts any of them.
+ """
+ for test in tests:
+ if self.arg1 == test.arg1:
+ if test.op == '=' and self.op == '=' and self.arg2 != test.arg2:
+ # equal different things
+ return False
+
+ elif test.op == '!=' and self.op == '=' and self.arg2 == test.arg2:
+ # contradict: equal vs. not equal
+ return False
+
+ elif test.op == '=' and self.op == '!=' and self.arg2 == test.arg2:
+ # contradict: equal vs. not equal
+ return False
+
+ return True
+
+ def rangeOpEdge(self):
+ if self.isRanged():
+ ops = {'<': operator.lt, '<=': operator.le, '=': operator.eq, '>=': operator.ge, '>': operator.gt}
+ return ops[self.op], self.arg2
+
+ return None
+
+class Property:
+ """ A style property.
+ """
+ def __init__(self, name):
+ assert name in properties
+
+ self.name = name
+
+ def group(self):
+ return self.name.split('-')[0]
+
+ def __repr__(self):
+ return self.name
+
+ def __str__(self):
+ return repr(self)
+
+class Value:
+ """ A style value.
+ """
+ def __init__(self, value, important):
+ self.value = value
+ self.important = important
+
+ def importance(self):
+ return int(self.important)
+
+ def __repr__(self):
+ return repr(self.value)
+
+ def __str__(self):
+ return str(self.value)
+
+def parse_stylesheet(string, base=None, is_gym=False):
+ """ Parse a string representing a stylesheet into a list of rulesets.
+
+ Optionally, accept a base string so we know where linked files come from,
+ and a flag letting us know whether this is a Google/VEarth mercator projection
+ so we know what to do with zoom/scale-denominator in postprocess_selector().
+ """
+ in_selectors = False
+ in_block = False
+ in_declaration = False # implies in_block
+ in_property = False # implies in_declaration
+
+ rulesets = []
+ tokens = cssTokenizer().tokenize(string)
+
+ for token in tokens:
+ nname, value, line, col = token
+
+ try:
+ if not in_selectors and not in_block:
+ if nname == 'CHAR' and value == '{':
+ #
+ raise ParseException('Encountered unexpected opening "{"', line, col)
+
+ elif (nname in ('IDENT', 'HASH')) or (nname == 'CHAR' and value != '{'):
+ # beginning of a
+ rulesets.append({'selectors': [[(nname, value)]], 'declarations': []})
+ in_selectors = True
+
+ elif in_selectors and not in_block:
+ ruleset = rulesets[-1]
+
+ if (nname == 'CHAR' and value == '{'):
+ # open curly-brace means we're on to the actual rule sets
+ ruleset['selectors'][-1] = postprocess_selector(ruleset['selectors'][-1], is_gym, line, col)
+ in_selectors = False
+ in_block = True
+
+ elif (nname == 'CHAR' and value == ','):
+ # comma means there's a break between selectors
+ ruleset['selectors'][-1] = postprocess_selector(ruleset['selectors'][-1], is_gym, line, col)
+ ruleset['selectors'].append([])
+
+ elif nname not in ('COMMENT'):
+ # we're just in a selector is all
+ ruleset['selectors'][-1].append((nname, value))
+
+ elif in_block and not in_declaration:
+ ruleset = rulesets[-1]
+
+ if nname == 'IDENT':
+ # right at the start of a declaration
+ ruleset['declarations'].append({'property': [(nname, value)], 'value': [], 'position': (line, col)})
+ in_declaration = True
+ in_property = True
+
+ elif (nname == 'CHAR' and value == '}'):
+ # end of block
+ in_block = False
+
+ elif nname not in ('S', 'COMMENT'):
+ # something else
+ raise ParseException('Unexpected %(nname)s while looking for a property' % locals(), line, col)
+
+ elif in_declaration and in_property:
+ declaration = rulesets[-1]['declarations'][-1]
+
+ if nname == 'CHAR' and value == ':':
+ # end of property
+ declaration['property'] = postprocess_property(declaration['property'], line, col)
+ in_property = False
+
+ elif nname not in ('COMMENT'):
+ # in a declaration property
+ declaration['property'].append((nname, value))
+
+ elif in_declaration and not in_property:
+ declaration = rulesets[-1]['declarations'][-1]
+
+ if nname == 'CHAR' and value == ';':
+ # end of declaration
+ declaration['value'] = postprocess_value(declaration['value'], declaration['property'], base, line, col)
+ in_declaration = False
+
+ elif nname not in ('COMMENT'):
+ # in a declaration value
+ declaration['value'].append((nname, value))
+
+ except ParseException, e:
+ #raise ParseException(e.message + ' (line %(line)d, column %(col)d)' % locals(), line, col)
+ raise
+
+ return rulesets
+
+def unroll_rulesets(rulesets):
+ """ Convert a list of rulesets (as returned by parse_stylesheet)
+ into an ordered list of individual selectors and declarations.
+ """
+ declarations = []
+
+ for ruleset in rulesets:
+ for declaration in ruleset['declarations']:
+ for selector in ruleset['selectors']:
+ declarations.append(Declaration(selector, declaration['property'], declaration['value'],
+ (declaration['value'].importance(), selector.specificity(), declaration['position'])))
+
+ # sort by a css-like method
+ return sorted(declarations, key=operator.attrgetter('sort_key'))
+
+def trim_extra(tokens):
+ """ Trim comments and whitespace from each end of a list of tokens.
+ """
+ if len(tokens) == 0:
+ return tokens
+
+ while tokens[0][0] in ('S', 'COMMENT'):
+ tokens = tokens[1:]
+
+ while tokens[-1][0] in ('S', 'COMMENT'):
+ tokens = tokens[:-1]
+
+ return tokens
+
+def postprocess_selector(tokens, is_gym, line=0, col=0):
+ """ Convert a list of tokens into a Selector.
+ """
+ tokens = (token for token in trim_extra(tokens))
+
+ elements = []
+ parts = []
+
+ in_element = False
+ in_attribute = False
+
+ for token in tokens:
+ nname, value = token
+
+ if not in_element:
+ if (nname == 'CHAR' and value in ('.', '*')) or nname in ('IDENT', 'HASH'):
+ elements.append(SelectorElement())
+ in_element = True
+ # continue on to if in_element below...
+
+ if in_element and not in_attribute:
+ if nname == 'CHAR' and value == '.':
+ next_nname, next_value = tokens.next()
+
+ if next_nname == 'IDENT':
+ elements[-1].addName(value + next_value)
+
+ elif nname in ('IDENT', 'HASH') or (nname == 'CHAR' and value == '*'):
+ elements[-1].addName(value)
+
+ elif nname == 'CHAR' and value == '[':
+ in_attribute = True
+
+ elif nname == 'S':
+ in_element = False
+
+ elif in_attribute:
+ if nname in ('IDENT', 'NUMBER'):
+ parts.append(value)
+
+ elif nname == 'CHAR' and value in ('<', '=', '>', '!'):
+ if value is '=' and parts[-1] in ('<', '>', '!'):
+ parts[-1] += value
+ else:
+ if len(parts) != 1:
+ raise ParseException('Comparison operator must be in the middle of selector attribute', line, col)
+
+ parts.append(value)
+
+ elif nname == 'CHAR' and value == ']':
+ if len(parts) != 3:
+ raise ParseException('Incorrect number of items in selector attribute', line, col)
+
+ args = parts[-3:]
+ parts = []
+
+ try:
+ args[2] = int(args[2])
+ except ValueError:
+ try:
+ args[2] = float(args[2])
+ except ValueError:
+ if args[1] in ('<', '<=', '=>', '>'):
+ raise ParseException('Selector attribute must use a number for comparison tests', line, col)
+ else:
+ pass
+
+ elements[-1].addTest(SelectorAttributeTest(*args))
+ in_attribute = False
+
+ elif nname == 'S':
+ in_element = False
+
+ if len(elements) > 2:
+ raise ParseException('Only two-element selectors are supported for Mapnik styles', line, col)
+
+ if len(elements) == 0:
+ raise ParseException('At least one element must be present in selectors for Mapnik styles', line, col)
+
+ if elements[0].names[0] not in ('Map', 'Layer') and elements[0].names[0][0] not in ('.', '#', '*'):
+ raise ParseException('All non-ID, non-class first elements must be "Layer" Mapnik styles', line, col)
+
+ if len(elements) == 2 and elements[1].countTests():
+ raise ParseException('Only the first element in a selector may have attributes in Mapnik styles', line, col)
+
+ if len(elements) == 2 and elements[1].countIDs():
+ raise ParseException('Only the first element in a selector may have an ID in Mapnik styles', line, col)
+
+ if len(elements) == 2 and elements[1].countClasses():
+ raise ParseException('Only the first element in a selector may have a class in Mapnik styles', line, col)
+
+ selector = Selector(*elements)
+
+ if is_gym:
+ selector.convertZoomTests()
+
+ return selector
+
+def postprocess_property(tokens, line=0, col=0):
+ """ Convert a one-element list of tokens into a Property.
+ """
+ tokens = trim_extra(tokens)
+
+ if len(tokens) != 1:
+ raise ParseException('Too many tokens in property: ' + repr(tokens), line, col)
+
+ if tokens[0][0] != 'IDENT':
+ raise ParseException('Incorrect type of token in property: ' + repr(tokens), line, col)
+
+ if tokens[0][1] not in properties:
+ raise ParseException('"%s" is not a recognized property name' % tokens[0][1], line, col)
+
+ return Property(tokens[0][1])
+
+def postprocess_value(tokens, property, base=None, line=0, col=0):
+ """
+ """
+ tokens = trim_extra(tokens)
+
+ if len(tokens) >= 2 and (tokens[-2] == ('CHAR', '!')) and (tokens[-1] == ('IDENT', 'important')):
+ important = True
+ tokens = trim_extra(tokens[:-2])
+
+ else:
+ important = False
+
+ if properties[property.name] in (int, float) and len(tokens) == 2 and tokens[0] == ('CHAR', '-') and tokens[1][0] == 'NUMBER':
+ # put the negative sign on the number
+ tokens = [(tokens[1][0], '-' + tokens[1][1])]
+
+ value = tokens
+
+ if properties[property.name] in (int, float, str, color, uri, boolean) or type(properties[property.name]) is tuple:
+ if len(tokens) != 1:
+ raise ParseException('Single value only for property "%(property)s"' % locals(), line, col)
+
+ if properties[property.name] is int:
+ if tokens[0][0] != 'NUMBER':
+ raise ParseException('Number value only for property "%(property)s"' % locals(), line, col)
+
+ value = int(tokens[0][1])
+
+ elif properties[property.name] is float:
+ if tokens[0][0] != 'NUMBER':
+ raise ParseException('Number value only for property "%(property)s"' % locals(), line, col)
+
+ value = float(tokens[0][1])
+
+ elif properties[property.name] is str:
+ if tokens[0][0] != 'STRING':
+ raise ParseException('String value only for property "%(property)s"' % locals(), line, col)
+
+ value = tokens[0][1][1:-1]
+
+ elif properties[property.name] is color:
+ if tokens[0][0] != 'HASH':
+ raise ParseException('Hash value only for property "%(property)s"' % locals(), line, col)
+
+ if not re.match(r'^#([0-9a-f]{3}){1,2}$', tokens[0][1], re.I):
+ raise ParseException('Unrecognized color value for property "%(property)s"' % locals(), line, col)
+
+ hex = tokens[0][1][1:]
+
+ if len(hex) == 3:
+ hex = hex[0]+hex[0] + hex[1]+hex[1] + hex[2]+hex[2]
+
+ rgb = (ord(unhex(h)) for h in (hex[0:2], hex[2:4], hex[4:6]))
+
+ value = color(*rgb)
+
+ elif properties[property.name] is uri:
+ if tokens[0][0] != 'URI':
+ raise ParseException('URI value only for property "%(property)s"' % locals(), line, col)
+
+ raw = tokens[0][1]
+
+ if raw.startswith('url("') and raw.endswith('")'):
+ raw = raw[5:-2]
+
+ elif raw.startswith("url('") and raw.endswith("')"):
+ raw = raw[5:-2]
+
+ elif raw.startswith('url(') and raw.endswith(')'):
+ raw = raw[4:-1]
+
+ value = uri(raw, base)
+
+ elif properties[property.name] is boolean:
+ if tokens[0][0] != 'IDENT' or tokens[0][1] not in ('true', 'false'):
+ raise ParseException('true/false value only for property "%(property)s"' % locals(), line, col)
+
+ value = boolean(tokens[0][1] == 'true')
+
+ elif type(properties[property.name]) is tuple:
+ if tokens[0][0] != 'IDENT':
+ raise ParseException('Identifier value only for property "%(property)s"' % locals(), line, col)
+
+ if tokens[0][1] not in properties[property.name]:
+ raise ParseException('Unrecognized value for property "%(property)s"' % locals(), line, col)
+
+ value = tokens[0][1]
+
+ elif properties[property.name] is numbers:
+ values = []
+
+ # strip the list down to what we think goes number, comma, number, etc.
+ relevant_tokens = [token for token in tokens
+ if token[0] == 'NUMBER' or token == ('CHAR', ',')]
+
+ for (i, token) in enumerate(relevant_tokens):
+ if (i % 2) == 0 and token[0] == 'NUMBER':
+ try: