diff --git a/source/.html b/source/.html new file mode 100644 index 0000000..e69de29 diff --git a/source/__init__.html b/source/__init__.html new file mode 100644 index 0000000..75682f7 --- /dev/null +++ b/source/__init__.html @@ -0,0 +1,33 @@ + + + + + __init__.py + + + +
+
+
+

__init__.py

+
+
+
+
+
+ # +
+ +
+
+
from kartograph import Kartograph
+from map import projections
+
+__all__ = ['Kartograph', 'projections']
+
+
+
+
+
+
+ diff --git a/source/cartogram.html b/source/cartogram.html new file mode 100644 index 0000000..2b936e5 --- /dev/null +++ b/source/cartogram.html @@ -0,0 +1,858 @@ + + + + + cartogram.py + + + +
+
+
+

cartogram.py

+
+
+
+
+
+ # +
+

computes a circle cartogram for a given svg map + data file

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
class Cartogram:
+
+
+
+
+
+
+ # +
+ +
+
+
    def generate(self, svg_src, attr, csv_src, key, value):
+        regions = self.load_regions_from_svg(svg_src, attr)
+        data = self.load_csv(csv_src, key, value)
+        circles = []
+        for id in regions:
+            cx, cy = regions[id]
+            val = data[id]
+            circles.append(Circle(cx, cy, id, val))
+
+        self.attr = attr
+        self.key = value
+        self.circles = circles
+        self.compute_radii()
+        self.layout(700)
+        self.rescale()
+        self.correct()
+        self.layout(200, True)
+        self.rescale()
+        self.correct()
+        self.layout(100, False)
+        self.rescale()
+        self.correct()
+        self.to_svg()
+
+
+
+
+
+
+ # +
+ +
+
+
    def load_regions_from_svg(self, url, attr):
+        import svg as svgdoc
+        svg = svgdoc.Document.load(url)
+        self.svg = svg
+        g = svg.doc.getElementsByTagName('g')[0]
+        coords = {}
+        for path in g.getElementsByTagName('path'):
+            path_str = path.getAttribte('d')
+            id = path.getAttribte('data-' + attr)
+            poly = restore_poly_from_path_str(path_str)
+            coords[id] = poly.center()
+        return coords
+
+
+
+
+
+
+ # +
+ +
+
+
    def load_csv(self, url, key='id', value='val'):
+        import csv
+        doc = csv.reader(open(url), dialect='excel-tab')
+        head = None
+        data = {}
+        for row in doc:
+            if not head:
+                head = row
+                print head
+            else:
+                id = row[head.index(key)].strip()
+                val = float(row[head.index(value)])
+                data[id] = val
+        return data
+
+
+
+
+
+
+ # +
+ +
+
+
    def compute_radii(self):
+        import sys, math
+        minv = 0
+        maxv = sys.maxint * -1
+        for c in self.circles:
+            minv = min(minv, c.value)
+            maxv = max(maxv, c.value)
+
+        for c in self.circles:
+            c.r = math.pow((c.value - minv) / (maxv - minv), 0.50) * 60
+            c.weight = c.value / maxv
+
+
+
+
+
+
+ # +
+ +
+
+
    def layout(self, steps=100, correct=False):
+        for i in range(steps):
+
+
+
+
+
+
+ # +
+

if i % 100 == 0: + self.toSVG()

+
+
+
            self.layout_step(correct)
+
+
+
+
+
+
+ # +
+ +
+
+
    def layout_step(self, correct=False):
+        import math
+        pad = 0
+
+        if correct:
+            for C in self.circles:
+                v = Vector(C.ox - C.x, C.oy - C.y)
+                v.normalize()
+                v.resize(0.5)
+                C._move(v.x, v.y)
+
+        for A in self.circles:
+            for B in self.circles:
+                if A != B:
+                    radsq = (A.r + B.r) * (A.r + B.r)
+                    d = A.sqdist(B)
+                    if radsq + pad > d:
+
+
+
+
+
+
+ # +
+

move circles away from each other

+
+
+
                        v = Vector(B.x - A.x, B.y - A.y)
+                        v.normalize()
+                        m = (math.sqrt(radsq) - math.sqrt(d)) * 0.25
+                        v.resize(m)
+                        A._move(v.x * -1 * B.weight, v.y * -1 * B.weight)
+                        B._move(v.x * A.weight, v.y * A.weight)
+
+        for C in self.circles:
+            C.move()
+
+
+
+
+
+
+ # +
+ +
+
+
    def rescale(self):
+        from geometry import BBox, View
+        svg = self.svg
+        svg_view = svg[1][0][0]
+        vh = float(svg_view['h'])
+        vw = float(svg_view['w'])
+
+        bbox = BBox()
+        for c in self.circles:
+            r = c.r
+            bbox.update((c.x + r, c.y + r))
+            bbox.update((c.x + r, c.y - r))
+            bbox.update((c.x - r, c.y + r))
+            bbox.update((c.x - r, c.y - r))
+
+        view = View(bbox, vw, vh)
+        for c in self.circles:
+            c.r *= view.scale
+            x, y = view.project((c.x, c.y))
+            c.x = x
+            c.y = y
+
+
+
+
+
+
+ # +
+ +
+
+
    def correct(self):
+        for A in self.circles:
+            intersects = False
+            for B in self.circles:
+                if A != B:
+                    radsq = (A.r + B.r) * (A.r + B.r)
+                    d = A.sqdist_o(B)
+                    if radsq > d:
+                        intersects = True
+                        break
+            if not intersects:
+                A.x = A.ox
+                A.y = A.oy
+
+
+
+
+
+
+ # +
+ +
+
+
    def to_svg(self):
+        svg = self.svg
+
+        g = svg.node('g', svg.root, id="cartogram", fill="red", fill_opacity="0.5")
+
+        for circle in self.circles:
+            c = svg.node('circle', g, cx=circle.x, cy=circle.y, r=circle.r)
+            c.setAttribute('data-' + self.attr, circle.id)
+            c.setAttribute('data-' + self.key.lower(), circle.value)
+            g.append(c)
+
+        svg.preview()
+
+
+
+
+
+
+ # +
+

svg.save('cartogram.svg')

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
class Circle:
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, x, y, id, value):
+        self.x = self.ox = float(x)
+        self.y = self.oy = float(y)
+        self.id = id
+        self.value = float(value)
+        self.dx = 0
+        self.dy = 0
+
+
+
+
+
+
+ # +
+ +
+
+
    def _move(self, x, y):
+        self.dx += x
+        self.dy += y
+
+
+
+
+
+
+ # +
+ +
+
+
    def move(self):
+        self.x += self.dx
+        self.y += self.dy
+        self.dx = 0
+        self.dy = 0
+
+
+
+
+
+
+ # +
+ +
+
+
    def __repr__(self):
+        return '<Circle x=%.1f, y=%.1f, id=%s, val=%f >' % (self.x, self.y, self.id, self.value)
+
+
+
+
+
+
+ # +
+ +
+
+
    def sqdist(self, circ):
+        dx = self.x - circ.x
+        dy = self.y - circ.y
+        return dx * dx + dy * dy
+
+
+
+
+
+
+ # +
+

been too lazy to code this myself, instead I took code from here +http://www.kokkugia.com/wiki/index.php5?title=Python_vector_class

+
+
+
    def sqdist_o(self, circ):
+        dx = self.ox - circ.x
+        dy = self.oy - circ.y
+        return dx * dx + dy * dy
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
class Vector:
+
+
+
+
+
+
+ # +
+

Class properties

+
+
+
    def __init__(self, x, y):
+        self.x = float(x)
+        self.y = float(y)
+
+
+
+
+
+
+ # +
+

represent as a string

+
+
+
    def __repr__(self):
+        return 'Vector(%s, %s)' % (self.x, self.y)
+
+    '''
+       Class Methods / Behaviours
+    '''
+
+
+
+
+
+
+ # +
+ +
+
+
    def zero(self):
+        self.x = 0.0
+        self.y = 0.0
+        return self
+
+
+
+
+
+
+ # +
+ +
+
+
    def clone(self):
+        return Vector(self.x, self.y)
+
+
+
+
+
+
+ # +
+ +
+
+
    def normalize(self):
+        from math import sqrt
+        if self.x == 0 and self.y == 0:
+            return self
+        norm = float(1.0 / sqrt(self.x * self.x + self.y * self.y))
+        self.x *= norm
+        self.y *= norm
+
+
+
+
+
+
+ # +
+

self.z *= norm

+
+
+
        return self
+
+
+
+
+
+
+ # +
+ +
+
+
    def invert(self):
+        self.x = -(self.x)
+        self.y = -(self.y)
+        return self
+
+
+
+
+
+
+ # +
+ +
+
+
    def resize(self, sizeFactor):
+        self.normalize
+        self.scale(sizeFactor)
+        return self
+
+
+
+
+
+
+ # +
+ +
+
+
    def minus(self, t):
+        self.x -= t.x
+        self.y -= t.y
+
+
+
+
+
+
+ # +
+

self.z -= t.z

+
+
+
        return self
+
+
+
+
+
+
+ # +
+ +
+
+
    def plus(self, t):
+        self.x += t.x
+        self.y += t.y
+
+
+
+
+
+
+ # +
+

self.z += t.z

+
+
+
        return self
+
+
+
+
+
+
+ # +
+ +
+
+
    def roundToInt(self):
+        self.x = int(self.x)
+        self.y = int(self.y)
+        return self
+
+
+
+
+
+
+ # +
+

Returns the squared length of this vector.

+
+
+
    def lengthSquared(self):
+        return float((self.x * self.x) + (self.y * self.y))
+
+
+
+
+
+
+ # +
+

Returns the length of this vector.

+
+
+
    def length(self):
+        from math import sqrt
+        return float(sqrt(self.x * self.x + self.y * self.y))
+
+
+
+
+
+
+ # +
+

Computes the dot product of this vector and vector v2

+
+
+
    def dot(self, v2):
+        return (self.x * v2.x + self.y * v2.y)
+
+
+
+
+
+
+ # +
+

Linearly interpolates between vectors v1 and v2 and returns the result point = (1-alpha)v1 + alphav2.

+
+
+
    def interpolate(self, v2, alpha):
+        self.x = float((1 - alpha) * self.x + alpha * v2.x)
+        self.y = float((1 - alpha) * self.y + alpha * v2.y)
+        return Vector(self.x, self.y)
+
+
+
+
+
+
+ # +
+

Returns the angle in radians between this vector and the vector parameter; +the return value is constrained to the range [0,PI].

+
+
+
    def angle(self, v2):
+        from math import acos
+        vDot = self.dot(v2) / (self.length() * v2.length())
+        if vDot < -1.0:
+            vDot = -1.0
+        if vDot > 1.0:
+            vDot = 1.0
+        return float(acos(vDot))
+
+
+
+
+
+
+ # +
+

Limits this vector to a given size. +NODEBOX USERS: name should change as 'size' and 'scale' are reserved words in Nodebox!

+
+
+
    def limit(self, size):
+        if (self.length() > size):
+            self.normalize()
+            self.scale(size)
+
+
+
+
+
+
+ # +
+

Point Methods +Returns the square of the distance between this tuple and tuple t1.

+
+
+
    def distanceSquared(self, t1):
+        dx = self.x - t1.x
+        dy = self.y - t1.y
+        return (dx * dx + dy * dy)
+
+
+
+
+
+
+ # +
+

NODEBOX USERS: name should change as 'scale' is reserved word in Nodebox!

+
+
+
    def scale(self, s):
+        self.x *= s
+        self.y *= s
+        return self
+
+
+
+
+
+
+ # +
+

NODEBOX USERS: name should change as 'translate' is reserved word in Nodebox!

+
+
+
    def translate(self, vec):
+        self.plus(vec)
+
+
+
+
+
+
+ # +
+ +
+
+
    def distance(self, pt):
+        from math import sqrt
+        dx = self.x - pt.x
+        dy = self.y - pt.y
+        return float(sqrt(dx * dx + dy * dy))
+
+
+
+
+
+
+ # +
+

restores a list of polygons from a SVG path string

+
+
+
def restore_poly_from_path_str(path_str):
+
+
+
+
+
+
+ # +
+ +
+
+
    contours = path_str.split('Z')  # last contour may be empty
+    from Polygon import Polygon as Poly
+    poly = Poly()
+    for c_str in contours:
+        if c_str.strip() != "":
+            pts_str = c_str.strip()[1:].split("L")
+            pts = []
+            for pt_str in pts_str:
+                x, y = map(float, pt_str.split(','))
+                pts.append((x, y))
+            poly.addContour(pts, is_clockwise(pts))
+    return poly
+
+
+
+
+
+
+ # +
+

returns true if a given polygon is in clockwise order

+
+
+
def is_clockwise(pts):
+
+
+
+
+
+
+ # +
+ +
+
+
    s = 0
+    for i in range(len(pts) - 1):
+        if 'x' in pts[i]:
+            x1 = pts[i].x
+            y1 = pts[i].y
+            x2 = pts[i + 1].x
+            y2 = pts[i + 1].y
+        else:
+            x1, y1 = pts[i]
+            x2, y2 = pts[i + 1]
+        s += (x2 - x1) * (y2 + y1)
+    return s >= 0
+
+
+
+
+
+
+ diff --git a/source/cli.html b/source/cli.html new file mode 100644 index 0000000..19ce355 --- /dev/null +++ b/source/cli.html @@ -0,0 +1,214 @@ + + + + + cli.py + + + +
+
+
+

cli.py

+
+
+
+
+
+ # +
+

command line interface for kartograph

+
+
+
import argparse
+import os.path
+import json
+from errors import KartographError
+
+
+
+
+
+
+ # +
+ +
+
+
class bcolors:
+    HEADER = '\033[95m'
+    OKBLUE = '\033[94m'
+    OKGREEN = '\033[92m'
+    WARNING = '\033[93m'
+    FAIL = '\033[91m'
+    ENDC = '\033[0m'
+
+
+
+
+
+
+ # +
+ +
+
+
    def disable(self):
+        self.HEADER = ''
+        self.OKBLUE = ''
+        self.OKGREEN = ''
+        self.WARNING = ''
+        self.FAIL = ''
+        self.ENDC = ''
+
+parser = argparse.ArgumentParser(prog='kartograph', description='generating svg maps from shapefiles')
+
+
+
+
+
+
+ # +
+

subparsers = parser.add_subparsers(help='sub-command help')

+
+
+
+
+
+
+
+
+
+ # +
+

parser_svg = subparsers.add_parser('svg', help='generates svg map')

+
+
+
parser.add_argument('config', type=argparse.FileType('r'), help='the configuration for the map. accepts json and yaml.')
+parser.add_argument('--output', '-o', metavar='FILE', type=argparse.FileType('w'), help='the file in which the map will be stored')
+parser.add_argument('--verbose', '-v', nargs='?', metavar='', const=True, help='verbose mode')
+parser.add_argument('--format', '-f', metavar='svg|kml', help='output format, if not specified it will be guessed from output filename or default to svg')
+parser.add_argument('--preview', '-p', nargs='?', metavar='', const=True, help='opens the generated svg for preview')
+
+from kartograph import Kartograph
+import time
+import sys
+import os
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_config(f):
+    content = f.read()
+    if f.name[-5:].lower() == '.json':
+        try:
+            cfg = json.loads(content)
+        except Exception, e:
+            raise KartographError('parsing of json map configuration failed.\n' + e)
+        else:
+            return cfg
+    elif f.name[-5:].lower() == '.yaml':
+        import yaml
+        try:
+            cfg = yaml.load(content)
+        except Exception, e:
+            raise KartographError('parsing of yaml map configuration failed.\n' + e)
+        else:
+            return cfg
+    else:
+        raise KartographError('supported config formats are .json and .yaml')
+
+
+
+
+
+
+ # +
+ +
+
+
def render_map(args):
+    cfg = parse_config(args.config)
+    K = Kartograph()
+    if args.format:
+        format = args.format
+    elif args.output:
+        format = os.path.splitext(args.output.name)[1][1:]
+    else:
+        format = 'svg'
+    try:
+
+
+
+
+
+
+ # +
+

generate the map

+
+
+
        K.generate(cfg, args.output, verbose=args.verbose, preview=args.preview, format=format)
+
+    except Exception, e:
+        import traceback
+        ignore_path_len = len(__file__) - 7
+        exc = sys.exc_info()
+        for (filename, line, func, code) in traceback.extract_tb(exc[2]):
+            if filename[:len(__file__) - 7] == __file__[:-7]:
+                print '  \033[1;33;40m%s\033[0m, \033[0;37;40min\033[0m %s()\n  \033[1;31;40m%d:\033[0m \033[0;37;40m%s\033[0m' % (filename[ignore_path_len:], func, line, code)
+            else:
+                print '  %s, in %s()\n  %d: %s' % (filename, func, line, code)
+        print
+        print e
+        exit(-1)
+
+parser.set_defaults(func=render_map)
+
+
+
+
+
+
+ # +
+ +
+
+
def main():
+    start = time.time()
+
+    try:
+        args = parser.parse_args()
+    except IOError, e:
+        parser.print_help()
+        print '\nIOError:', e
+    except Exception, e:
+        parser.print_help()
+        print '\nError:', e
+    else:
+        args.func(args)
+
+    elapsed = (time.time() - start)
+    print 'execution time: %.3f secs' % elapsed
+    sys.exit(0)
+
+
+if __name__ == "__main__":
+    main()
+
+
+
+
+
+
+ diff --git a/source/errors.html b/source/errors.html new file mode 100644 index 0000000..7e9f2fd --- /dev/null +++ b/source/errors.html @@ -0,0 +1,94 @@ + + + + + errors.py + + + +
+
+
+

errors.py

+
+
+
+
+
+ # +
+

error classes for kartograph

+
+
+
+
+
+
+
+
+
+ # +
+

Base class for exceptions in this module.

+
+
+
class KartographError(Exception):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __str__(self):
+        return '\033[0;31;40mKartograph-Error:\033[0m ' + super(KartographError, self).__str__()
+
+
+
+
+
+
+ # +
+ +
+
+
class KartographOptionParseError(KartographError):
+    pass
+
+
+
+
+
+
+ # +
+ +
+
+
class KartographShapefileAttributesError(KartographError):
+    pass
+
+
+
+
+
+
+ # +
+ +
+
+
class KartographLayerSourceError(KartographError):
+    pass
+
+
+
+
+
+
+ diff --git a/source/filter.html b/source/filter.html new file mode 100644 index 0000000..7cc2a7e --- /dev/null +++ b/source/filter.html @@ -0,0 +1,114 @@ + + + + + filter.py + + + +
+
+
+

filter.py

+
+
+
+
+
+ # +
+

layer filter

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
import re
+
+
+
+
+
+
+ # +
+ +
+
+
def filter_record(filt, record):
+    if isinstance(filt, dict):
+        if 'and' in filt:
+            res = True
+            for sfilt in filt['and']:
+                res = res and filter_record(sfilt, record)
+        elif 'or' in filt:
+            res = False
+            for sfilt in filt['or']:
+                res = res or filter_record(sfilt, record)
+    elif isinstance(filt, list):
+        res = filter_single(filt, record)
+    return res
+
+
+
+
+
+
+ # +
+ +
+
+
def filter_single(filt, record):
+    key, comp, val = filt
+    prop = record[key]
+    comp = comp.lower().split(' ')
+
+    if 'in' in comp:
+        res = prop in val
+    elif 'like' in comp:
+        res = re.search('^' + _escape_regex(val).replace('%', '.*') + '$', prop) is not None
+    elif 'matches' in comp:
+        res = re.search(val, prop) is not None
+    elif 'is' in comp or '=' in comp:
+        res = prop == val
+    elif 'greater' in comp or ('>' in comp):
+        res = prop > val
+    elif 'less' in comp or '<' in comp:
+        res = prop < val
+    if 'not' in comp:
+        return not res
+    else:
+        return res
+
+
+
+
+
+
+ # +
+ +
+
+
def _escape_regex(s):
+    chars = ('.', '*', '?', '+', '(', ')', '[', ']', '-')
+    for c in chars:
+        s = s.replace(c, '\\' + c)
+    return s
+
+
+
+
+
+
+ diff --git a/source/geometry/__init__.html b/source/geometry/__init__.html new file mode 100644 index 0000000..4ec2d26 --- /dev/null +++ b/source/geometry/__init__.html @@ -0,0 +1,50 @@ + + + + + __init__.py + + + +
+
+
+

__init__.py

+
+
+
+
+
+ # +
+

geometry package

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
__all__ = ['Feature', 'Geometry', 'SolidGeometry', 'MultiPolygon', 'BBox', 'Point', 'View', 'Line', 'PolyLine', 'create_feature']
+
+from feature import *
+from geometry import Geometry, SolidGeometry
+from polygon import MultiPolygon
+from point import Point
+from bbox import BBox
+from view import View
+from line import Line, PolyLine
+
+
+
+
+
+
+ diff --git a/source/geometry/bbox.html b/source/geometry/bbox.html new file mode 100644 index 0000000..ead41c7 --- /dev/null +++ b/source/geometry/bbox.html @@ -0,0 +1,195 @@ + + + + + bbox.py + + + +
+
+
+

bbox.py

+
+
+
+
+
+ # +
+ +
+
+
from point import Point
+
+
+
+
+
+
+ # +
+

2D bounding box

+
+
+
class BBox(object):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, width=None, height=None, left=0, top=0):
+        import sys
+        if width == None:
+            self.xmin = sys.maxint
+            self.xmax = sys.maxint * -1
+        else:
+            self.xmin = self.left = left
+            self.xmax = self.right = left + width
+            self.width = width
+        if height == None:
+            self.ymin = sys.maxint
+            self.ymax = sys.maxint * -1
+        else:
+            self.ymin = self.top = top
+            self.ymax = self.bottom = height + top
+            self.height = height
+
+
+
+
+
+
+ # +
+ +
+
+
    def update(self, pt):
+        if not isinstance(pt, Point):
+            pt = Point(pt[0], pt[1])
+        self.xmin = min(self.xmin, pt.x)
+        self.ymin = min(self.ymin, pt.y)
+        self.xmax = max(self.xmax, pt.x)
+        self.ymax = max(self.ymax, pt.y)
+
+        self.left = self.xmin
+        self.top = self.ymin
+        self.right = self.xmax
+        self.bottom = self.ymax
+        self.width = self.xmax - self.xmin
+        self.height = self.ymax - self.ymin
+
+
+
+
+
+
+ # +
+

returns true if two bounding boxes overlap

+
+
+
    def intersects(self, bbox):
+
+
+
+
+
+
+ # +
+ +
+
+
        return bbox.left < self.right and bbox.right > self.left and bbox.top < self.bottom and bbox.bottom > self.top
+
+
+
+
+
+
+ # +
+

check if a point is inside the bbox

+
+
+
    def check_point(self, pt):
+
+
+
+
+
+
+ # +
+ +
+
+
        return pt[0] > self.xmin and pt[0] < self.xmax and pt[1] > self.ymin and pt[1] < self.ymax
+
+
+
+
+
+
+ # +
+ +
+
+
    def __str__(self):
+        return 'BBox(x=%.2f, y=%.2f, w=%.2f, h=%.2f)' % (self.left, self.top, self.width, self.height)
+
+
+
+
+
+
+ # +
+ +
+
+
    def join(self, bbox):
+        self.update(Point(bbox.left, bbox.top))
+        self.update(Point(bbox.right, bbox.bottom))
+
+
+
+
+
+
+ # +
+ +
+
+
    def inflate(self, amount=0, inflate=False):
+        if inflate:
+            d = min(self.width, self.height)
+            amount += d * inflate
+        self.xmin -= amount
+        self.ymin -= amount
+        self.xmax += amount
+        self.ymax += amount
+
+        self.left = self.xmin
+        self.top = self.ymin
+        self.right = self.xmax
+        self.bottom = self.ymax
+        self.width = self.xmax - self.xmin
+        self.height = self.ymax - self.ymin
+
+
+
+
+
+
+ diff --git a/source/geometry/geometry.html b/source/geometry/geometry.html new file mode 100644 index 0000000..d54ab56 --- /dev/null +++ b/source/geometry/geometry.html @@ -0,0 +1,279 @@ + + + + + geometry.py + + + +
+
+
+

geometry.py

+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

DEPRECATED

+
+
+
+
+
+
+
+
+
+ # +
+

base class for all geometry

+
+
+
class Geometry:
+
+
+
+
+
+
+ # +
+

project geometry

+
+
+
    def project(self, proj):
+
+
+
+
+
+
+ # +
+ +
+
+
        raise NotImplementedError('project() is not implemented')
+
+
+
+
+
+
+ # +
+ +
+
+
    def bbox(self):
+        raise NotImplementedError('bbox() is not implemented')
+
+
+
+
+
+
+ # +
+ +
+
+
    def project_view(self, view):
+        raise NotImplementedError('project_view() is not implemented')
+
+
+
+
+
+
+ # +
+ +
+
+
    def to_svg(self, round=0):
+        raise NotImplementedError('to_svg() is not implemented')
+
+
+
+
+
+
+ # +
+ +
+
+
    def crop_to(self, view_bounds):
+        raise NotImplementedError('crop_to() is not implemented')
+
+
+
+
+
+
+ # +
+ +
+
+
    def substract_geom(self, geom):
+        raise NotImplementedError('substract_geom() is not implemented yet')
+
+
+
+
+
+
+ # +
+ +
+
+
    def is_emtpy(self):
+        return False
+
+
+
+
+
+
+ # +
+ +
+
+
    def unify(self, point_store, precision=None):
+        raise NotImplementedError('unify() is not implemented yet')
+
+
+
+
+
+
+ # +
+

returns a list of point lists

+
+
+
    def points(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        raise NotImplementedError('points() is not implemented yet')
+
+
+
+
+
+
+ # +
+ +
+
+
    def join(self, geom):
+        raise NotImplementedError('join() is not implemented yet')
+
+
+
+
+
+
+ # +
+

base class for all solid geometry, e.g. polygons

+
+
+
class SolidGeometry(Geometry):
+
+
+
+
+
+
+ # +
+

calculates area for this geometry

+
+
+
    def area():
+
+
+
+
+
+
+ # +
+ +
+
+
        raise NotImplementedError('area() is not implemented')
+
+
+
+
+
+
+ # +
+

calculates centroid for this geometry

+
+
+
    def centroid():
+
+
+
+
+
+
+ # +
+ +
+
+
        raise NotImplementedError('centroid() is not implemented')
+
+
+
+
+
+
+ # +
+ +
+
+
    def invalidate(self):
+        self.__area = None
+
+
+
+
+
+
+ diff --git a/source/geometry/line.html b/source/geometry/line.html new file mode 100644 index 0000000..b5c0982 --- /dev/null +++ b/source/geometry/line.html @@ -0,0 +1,519 @@ + + + + + line.py + + + +
+
+
+

line.py

+
+
+
+
+
+ # +
+ +
+
+
from bbox import BBox
+from geometry import Geometry
+
+
+
+
+
+
+ # +
+

polyline

+
+
+
class PolyLine(Geometry):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, contours):
+        self.contours = contours
+
+
+
+
+
+
+ # +
+

returns the bounding box of the line

+
+
+
    def bbox(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        bbox = BBox()
+        for pts in self.contours:
+            for pt in pts:
+                bbox.update(pt)
+        return bbox
+
+
+
+
+
+
+ # +
+

projects the line to a map projection

+
+
+
    def project(self, proj):
+
+
+
+
+
+
+ # +
+ +
+
+
        contours = []
+        for points in self.contours:
+            pts = []
+            for pt in points:
+                if proj._visible(pt[0], pt[1]):
+                    p = proj.project(pt[0], pt[1])
+                    if p is not None:
+                        pts.append(p)
+                else:
+                    if len(pts) > 0:
+                        contours.append(pts)
+                        pts = []
+            if len(pts) > 0:
+                contours.append(pts)
+        return PolyLine(contours)
+
+
+
+
+
+
+ # +
+

transforms the line to a new view

+
+
+
    def project_view(self, view):
+
+
+
+
+
+
+ # +
+ +
+
+
        contours = []
+        for points in self.contours:
+            pts = []
+            for pt in points:
+                p = view.project(pt)
+                pts.append(p)
+            contours.append(pts)
+        return PolyLine(contours)
+
+
+
+
+
+
+ # +
+

constructs a svg representation of this line

+
+
+
    def to_svg(self, svg, round):
+
+
+
+
+
+
+ # +
+ +
+
+
        path_str = ""
+        if round is False:
+            fmt = '%f,%f'
+        else:
+            fmt = '%.' + str(round) + 'f'
+            fmt = fmt + ',' + fmt
+
+        for points in self.contours:
+            path_str += "M "
+            pts = []
+            for pt in points:
+                pts.append(fmt % (pt[0], pt[1]))
+            path_str += 'L '.join(pts)
+
+        path = svg.node('path', d=path_str)
+        return path
+
+
+
+
+
+
+ # +
+ +
+
+
    def crop_to(self, geom):
+
+
+
+
+
+
+ # +
+

skip

+
+
+
        return self
+
+
+
+
+
+
+ # +
+ +
+
+
    def is_empty(self):
+        return len(self.contours) == 0
+
+
+
+
+
+
+ # +
+ +
+
+
    def unify(self, point_store, precision):
+        from kartograph.simplify import unify_polygon
+        for i in range(len(self.contours)):
+            self.contours[i] = unify_polygon(self.contours[i], point_store, precision)
+
+
+
+
+
+
+ # +
+ +
+
+
    def points(self):
+        return self.contours
+
+
+
+
+
+
+ # +
+

is called after the points of this geometry have been +changed from outside this class

+
+
+
    def update(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        pass
+
+
+
+
+
+
+ # +
+

simple line (= list of points)

+
+
+
class Line(Geometry):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, points):
+        self.pts = points
+
+
+
+
+
+
+ # +
+

returns the bounding box of the line

+
+
+
    def bbox(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        bbox = BBox()
+        for pt in self.pts:
+            bbox.update(pt)
+        return bbox
+
+
+
+
+
+
+ # +
+

projects the line to a map projection

+
+
+
    def project(self, proj):
+
+
+
+
+
+
+ # +
+ +
+
+
        pts = []
+        for pt in self.pts:
+            p = proj.project(pt[0], pt[1])
+            if p is not None:
+                pts.append(p)
+        return Line(pts)
+
+
+
+
+
+
+ # +
+

transforms the line to a new view

+
+
+
    def project_view(self, view):
+
+
+
+
+
+
+ # +
+ +
+
+
        pts = []
+        for pt in self.pts:
+            p = view.project(pt)
+            pts.append(p)
+        return Line(pts)
+
+
+
+
+
+
+ # +
+

constructs a svg representation of this line

+
+
+
    def to_svg(self, svg, round):
+
+
+
+
+
+
+ # +
+ +
+
+
        path_str = ""
+        if round is False:
+            fmt = '%f,%f'
+        else:
+            fmt = '%.' + str(round) + 'f'
+            fmt = fmt + ',' + fmt
+
+        for pt in self.pts:
+            if path_str == "":
+                path_str = "M"
+            else:
+                path_str += "L"
+            path_str += fmt % (pt[0], pt[1])
+
+        path = svg.node('path', d=path_str)
+        return path
+
+
+
+
+
+
+ # +
+ +
+
+
    def crop_to(self, geom):
+
+
+
+
+
+
+ # +
+

skip

+
+
+
        return self
+
+
+
+
+
+
+ # +
+ +
+
+
    def is_empty(self):
+        return len(self.pts) == 0
+
+
+
+
+
+
+ # +
+ +
+
+
    def unify(self, point_store, precision):
+        from kartograph.simplify import unify_polygon
+        self.pts = unify_polygon(self.pts, point_store, precision)
+
+
+
+
+
+
+ # +
+ +
+
+
    def points(self):
+        return [self.pts]
+
+
+
+
+
+
+ # +
+

is called after the points of this geometry have been +changed from outside this class

+
+
+
    def update(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        pass
+
+
+
+
+
+
+ diff --git a/source/geometry/point.html b/source/geometry/point.html new file mode 100644 index 0000000..c10c17d --- /dev/null +++ b/source/geometry/point.html @@ -0,0 +1,141 @@ + + + + + point.py + + + +
+
+
+

point.py

+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

base class for points, used by line and bbox

+
+
+
class Point():
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+
+
+
+
+
+
+ # +
+

emulate python's container types

+
+
+
    def project(self, proj):
+        (x, y) = proj.project(self.x, self.y)
+        self.x = x
+        self.y = y
+
+
+
+
+
+
+ # +
+ +
+
+
    def __len__(self):
+        return 2
+
+
+
+
+
+
+ # +
+ +
+
+
    def __getitem__(self, k):
+        pt = (self.x, self.y)
+        return pt[k]
+
+
+
+
+
+
+ # +
+ +
+
+
    def __setitem__(self, k, value):
+        if k == 0:
+            self.x = value
+        elif k == 1:
+            self.y = value
+        else:
+            raise IndexError
+
+
+
+
+
+
+ # +
+ +
+
+
    def __delitem__(self, key):
+        raise TypeError('deletion not supported')
+
+
+
+
+
+
+ diff --git a/source/geometry/polygon.html b/source/geometry/polygon.html new file mode 100644 index 0000000..cf5a761 --- /dev/null +++ b/source/geometry/polygon.html @@ -0,0 +1,625 @@ + + + + + polygon.py + + + +
+
+
+

polygon.py

+
+
+
+
+
+ # +
+ +
+
+
from geometry import *
+from bbox import BBox
+import utils
+
+
+
+
+
+
+ # +
+

DEPRECATED! we use shapely.geometry.MultiPolygon instead

+
+
+
+
+
+
+
+
+
+ # +
+

Complex polygons

+
+
+
class MultiPolygon(SolidGeometry):
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, contours):
+        self.__area = None
+        self.__areas = None
+        self.__centroid = None
+        self.apply_contours(contours)
+
+
+
+
+
+
+ # +
+

constructs a Polygon from contours

+
+
+
    @staticmethod
+    def fromPoly(poly):
+        contours = []
+        for i in range(len(poly)):
+            pts = poly.contour(i)
+            contours.append(pts)
+        return MultiPolygon(contours)
+
+    def apply_contours(self, contours):
+
+
+
+
+
+
+ # +
+ +
+
+
        self.contours = contours
+        from Polygon import Polygon as GPCPoly
+        poly = GPCPoly()
+        skip = 0
+        for pts_ in contours:
+            pts = []
+            for pt in pts_:
+                if 'deleted' in pt and pt.deleted is True:
+                    skip += 1
+                    continue
+                pts.append((pt[0], pt[1]))
+            ishole = utils.is_clockwise(pts)
+
+            if len(pts) > 2:
+                poly.addContour(pts, ishole)
+        self.poly = poly
+
+
+
+
+
+
+ # +
+ +
+
+
    def area(self):
+        if self.__area is not None:
+            return self.__area
+        self.__area = self.poly.area()
+        return self.__area
+
+
+
+
+
+
+ # +
+

returns array of areas of all sub-polygons areas of holes are < 0

+
+
+
    def areas(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        if self.__areas is not None:
+            return self.__areas
+        a = []
+        for i in range(len(self.poly)):
+            t = self.poly.area(i)
+            if self.poly.isHole(i):
+                t *= -1
+            a.append(t)
+        self.__areas = a
+        return a
+
+
+
+
+
+
+ # +
+

returns the center of gravity for this polygon

+
+
+
    def centroid(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        if self.__centroid is not None:
+            return self.__centroid
+        self.__centroid = self.poly.center()
+        return self.__centroid
+
+
+
+
+
+
+ # +
+

smart bounding box

+
+
+
    def bbox(self, min_area=0):
+
+
+
+
+
+
+ # +
+ +
+
+
        bb = []
+        bbox = BBox()
+        if min_area == 0:
+            bb.append(self.poly.boundingBox())
+        else:
+            areas = self.areas()
+            max_a = max(areas)
+            for i in range(len(self.poly)):
+                if self.poly.isHole(i):
+                    continue
+                a = areas[i]
+                if a < max_a * min_area:
+                    continue
+                bb.append(self.poly.boundingBox(i))
+        for b in bb:
+            bbox.update((b[0], b[2]))
+            bbox.update((b[1], b[2]))
+            bbox.update((b[0], b[3]))
+            bbox.update((b[1], b[3]))
+        return bbox
+
+
+
+
+
+
+ # +
+

returns a new multi-polygon whose contours are +projected to a map projection

+
+
+
    def project(self, proj):
+
+
+
+
+
+
+ # +
+ +
+
+
        in_contours = self.contours
+        out_contours = []
+        for pts in in_contours:
+            pcont = proj.plot(pts)
+            if pcont != None:
+                out_contours += pcont
+        return MultiPolygon(out_contours)
+
+
+
+
+
+
+ # +
+

returns a new multi-polygon whose contours are +projected to a new view

+
+
+
    def project_view(self, view):
+
+
+
+
+
+
+ # +
+ +
+
+
        contours = self.contours
+        out = []
+        for contour in contours:
+            out_c = []
+            for pt in contour:
+                pt = view.project(pt)
+                out_c.append(pt)
+            out.append(out_c)
+        self.contours = out
+        out_poly = MultiPolygon(out)
+        return out_poly
+
+
+
+
+
+
+ # +
+ +
+
+
    def crop_to(self, view_bounds):
+        poly = self.poly & view_bounds.poly
+        return MultiPolygon.fromPoly(poly)
+
+
+
+
+
+
+ # +
+ +
+
+
    def substract_geom(self, geom):
+        if not isinstance(geom, MultiPolygon):
+            raise NotImplementedError('substraction is allowed for polygons only, yet')
+        poly = self.poly - geom.poly
+        return MultiPolygon.fromPoly(poly)
+
+
+
+
+
+
+ # +
+

constructs a svg representation of this polygon

+
+
+
    def to_svg(self, svg, round):
+
+
+
+
+
+
+ # +
+ +
+
+
        path_str = ""
+        if round is False:
+            fmt = '%f,%f'
+        else:
+            fmt = '%.' + str(round) + 'f'
+            fmt = fmt + ',' + fmt
+
+        for pts in self.contours:
+            cont_str = ""
+            kept = []
+            for pt in pts:
+                if 'deleted' in pt and pt.deleted is True:
+                    continue
+                kept.append(pt)
+
+            if len(kept) <= 3:
+                continue
+            for pt in kept:
+                if cont_str == "":
+                    cont_str = "M"
+                else:
+                    cont_str += "L"
+                cont_str += fmt % pt
+            cont_str += "Z "
+            path_str += cont_str
+
+        if path_str == "":
+            return None
+
+        path = svg.node('path', d=path_str)
+        return path
+
+
+
+
+
+
+ # +
+ +
+
+
    def is_empty(self):
+        return len(self.contours) == 0
+
+
+
+
+
+
+ # +
+ +
+
+
    def unify(self, point_store, precision=None):
+        from kartograph.simplify import unify_polygons
+        contours = self.contours
+        contours = unify_polygons(contours, point_store, precision)
+        self.apply_contours(contours)
+
+
+
+
+
+
+ # +
+ +
+
+
    def points(self):
+        return self.contours
+
+
+
+
+
+
+ # +
+

is called after the points of this geometry are +changed from outside this class

+
+
+
    def update(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        self.apply_contours(self.contours)
+
+
+
+
+
+
+ # +
+ +
+
+
    def to_kml(self, round=None):
+        from pykml.factory import KML_ElementMaker as KML
+        poly = KML.Polygon(
+            KML.tesselate("1")
+        )
+        outer = KML.outerBoundaryIs()
+        inner = KML.innerBoundaryIs()
+        has_inner = False
+        for i in range(len(self.poly)):
+            cnt = self.poly[i]
+            coords = ''
+            for p in cnt:
+                coords += ','.join(map(str, p)) + ' '
+            ring = KML.LinearRing(
+                KML.coordinates(coords)
+            )
+
+
+
+
+
+
+ # +
+

hole = self.poly.isHole(i) +if hole == False:

+
+
+
            outer.append(ring)
+
+
+
+
+
+
+ # +
+

else: + inner.append(ring) + has_inner = True

+
+
+
        poly.append(outer)
+        if has_inner:
+            poly.append(inner)
+
+        return poly
+
+
+
+
+
+
+ # +
+

splits the geometry into several line segments

+
+
+
    def to_line_segments(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        self.lines = []
+
+
+
+
+
+
+ # +
+ +
+
+
    def from_line_segments(self):
+        self.lines = []
+
+
+
+
+
+
+ # +
+ +
+
+
class Polygon(SolidGeometry):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, points):
+        self.__area = None
+        self.__centroid = None
+        self.points = points
+
+
+
+
+
+
+ # +
+ +
+
+
    def area(self):
+        if self.__area is not None:
+            return self.__area
+        a = 0
+        pts = self.points
+        for i in range(len(pts) - 1):
+            p0 = pts[i]
+            p1 = pts[i + 1]
+            a += p0.x * p1.y - p1.x * p0.y
+        self.__area = abs(a) * .5
+        return self.__area
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, proj):
+        self.invalidate()
+        self.points = proj.plot(self.points)
+
+
+
+
+
+
+ diff --git a/source/geometry/utils.html b/source/geometry/utils.html new file mode 100644 index 0000000..cde77ec --- /dev/null +++ b/source/geometry/utils.html @@ -0,0 +1,196 @@ + + + + + utils.py + + + +
+
+
+

utils.py

+
+
+
+
+
+ # +
+

geometry utils

+
+
+
+
+
+
+
+
+
+ # +
+

returns true if a given linear ring is in clockwise order

+
+
+
def is_clockwise(pts):
+
+
+
+
+
+
+ # +
+ +
+
+
    s = 0
+    for i in range(len(pts) - 1):
+        if 'x' in pts[i]:
+            x1 = pts[i].x
+            y1 = pts[i].y
+            x2 = pts[i + 1].x
+            y2 = pts[i + 1].y
+        else:
+            x1, y1 = pts[i]
+            x2, y2 = pts[i + 1]
+        s += (x2 - x1) * (y2 + y1)
+    return s >= 0
+
+
+
+
+
+
+ # +
+ +
+
+
def bbox_to_polygon(bbox):
+    from shapely.geometry import Polygon
+    s = bbox
+    poly = Polygon([(s.left, s.bottom), (s.left, s.top), (s.right, s.top), (s.right, s.bottom)])
+    return poly
+
+
+
+
+
+
+ # +
+ +
+
+
def geom_to_bbox(geom, min_area=0):
+    from kartograph.geometry import BBox
+    from shapely.geometry import MultiPolygon
+    if True or min_area == 0 or not isinstance(geom, MultiPolygon):
+
+
+
+
+
+
+ # +
+

if no minimum area ratio is set or the geometry +is not a multipart geometry, we simply use the +full bbox

+
+
+
        minx, miny, maxx, maxy = geom.bounds
+        return BBox(width=maxx - minx, height=maxy - miny, left=minx, top=miny)
+    else:
+
+
+
+
+
+
+ # +
+

for multipart geometry we use only the bbox of +the 'biggest' sub-geometries, depending on min_area

+
+
+
        bbox = BBox()
+        areas = []
+        bb = []
+        for polygon in geom.geoms:
+            areas.append(polygon.area)
+        max_a = max(areas)
+        for i in range(len(geom.geoms)):
+            a = areas[i]
+            if a < max_a * min_area:
+
+
+
+
+
+
+ # +
+

ignore this sub polygon since it is too small

+
+
+
                continue
+            bb.append(geom.geoms[i].bounds)
+    for b in bb:
+        bbox.update((b[0], b[2]))
+        bbox.update((b[1], b[2]))
+        bbox.update((b[0], b[3]))
+        bbox.update((b[1], b[3]))
+    return bbox
+
+
+
+
+
+
+ # +
+

joins polygonal features

+
+
+
def join_features(features, props):
+
+
+
+
+
+
+ # +
+ +
+
+
    from feature import MultiPolygonFeature
+
+    if len(features) == 0:
+        return features
+
+    joined = []
+    polygons = []
+
+    for feat in features:
+        if isinstance(feat, MultiPolygonFeature):
+            polygons.append(feat.geom)
+        else:
+            joined.append(feat)  # cannot join this
+
+    polygons = filter(lambda x: x is not None, polygons)
+    if len(polygons) > 0:
+        poly = polygons[0]
+        for poly2 in polygons[1:]:
+            poly = poly.union(poly2)
+        joined.append(MultiPolygonFeature(poly, props))
+    return joined
+
+
+
+
+
+
+ diff --git a/source/geometry/view.html b/source/geometry/view.html new file mode 100644 index 0000000..aa4f76b --- /dev/null +++ b/source/geometry/view.html @@ -0,0 +1,226 @@ + + + + + view.py + + + +
+
+
+

view.py

+
+
+
+
+
+ # +
+ +
+
+
from shapely.geometry import Polygon, MultiPolygon, LineString, MultiLineString, MultiPoint, Point
+from kartograph.errors import KartographError
+
+
+
+
+
+
+ # +
+

translates a point to a view

+
+
+
class View(object):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, bbox=None, width=None, height=None, padding=0):
+        self.bbox = bbox
+        self.width = width
+        self.padding = padding
+        self.height = height
+        if bbox:
+            self.scale = min((width - padding * 2) / bbox.width, (height - padding * 2) / bbox.height)
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, pt):
+        bbox = self.bbox
+        if not bbox:
+            return pt
+        s = self.scale
+        h = self.height
+        w = self.width
+        px = pt[0]
+        py = pt[1]
+        x = (px - bbox.left) * s + (w - bbox.width * s) * .5
+        y = (py - bbox.top) * s + (h - bbox.height * s) * .5
+        return ((x, y), Point(x, y))[isinstance(pt, Point)]
+
+
+
+
+
+
+ # +
+ +
+
+
    def project_inverse(self, pt):
+        bbox = self.bbox
+        if not bbox:
+            return pt
+        s = self.scale
+        h = self.height
+        w = self.width
+        x = pt[0]
+        y = pt[1]
+        px = (x - (w - bbox.width * s) * .5) / s + bbox.left
+        py = (y - (h - bbox.height * s) * .5) / s + bbox.top
+        return ((px, py), Point(px, py))[isinstance(pt, Point)]
+
+
+
+
+
+
+ # +
+

converts the given geometry to the view coordinates

+
+
+
    def project_geometry(self, geometry):
+
+
+
+
+
+
+ # +
+ +
+
+
        geometries = hasattr(geometry, 'geoms') and geometry.geoms or [geometry]
+        res = []
+
+
+
+
+
+
+ # +
+

at first shift polygons +geometries = [] +for geom in unshifted_geometries: + geometries += self._shift_polygon(geom)

+
+
+
        for geom in geometries:
+            if isinstance(geom, Polygon):
+                res += self.project_polygon(geom)
+            elif isinstance(geom, LineString):
+                rings = self.project_linear_ring(geom.coords)
+                res += map(LineString, rings)
+            elif isinstance(geom, Point):
+                if self._visible(geom.x, geom.y):
+                    res.append(self.project(geom))
+            else:
+                raise KartographError('unknown geometry type %s' % geometry)
+
+        if len(res) > 0:
+            if isinstance(res[0], Polygon):
+                if len(res) > 1:
+                    return MultiPolygon(res)
+                else:
+                    return res[0]
+            elif isinstance(res[0], LineString):
+                if len(res) > 1:
+                    return MultiLineString(res)
+                else:
+                    return LineString(res[0])
+            else:
+                if len(res) > 1:
+                    return MultiPoint(res)
+                else:
+                    return res[0]
+
+
+
+
+
+
+ # +
+ +
+
+
    def project_polygon(self, polygon):
+        ext = self.project_linear_ring(polygon.exterior)
+        if len(ext) == 1:
+            pts_int = []
+            for interior in polygon.interiors:
+                pts_int += self.project_linear_ring(interior)
+            return [Polygon(ext[0], pts_int)]
+        elif len(ext) == 0:
+            return []
+        else:
+            raise KartographError('unhandled case: exterior is split into multiple rings')
+
+
+
+
+
+
+ # +
+ +
+
+
    def project_linear_ring(self, ring):
+        points = []
+        for pt in ring.coords:
+            x, y = self.project(pt)
+            points.append((x, y))
+        return [points]
+
+
+
+
+
+
+ # +
+ +
+
+
    def __str__(self):
+        return 'View(w=%f, h=%f, pad=%f, scale=%f, bbox=%s)' % (self.width, self.height, self.padding, self.scale, self.bbox)
+
+
+
+
+
+
+ diff --git a/source/img/crop-bbox-01.png b/source/img/crop-bbox-01.png new file mode 100644 index 0000000..0c0023f Binary files /dev/null and b/source/img/crop-bbox-01.png differ diff --git a/source/img/crop-polygon-1.png b/source/img/crop-polygon-1.png new file mode 100644 index 0000000..d3e7487 Binary files /dev/null and b/source/img/crop-polygon-1.png differ diff --git a/source/img/crop-polygon-minarea.png b/source/img/crop-polygon-minarea.png new file mode 100644 index 0000000..3c05a47 Binary files /dev/null and b/source/img/crop-polygon-minarea.png differ diff --git a/source/img/crop-polygon.png b/source/img/crop-polygon.png new file mode 100644 index 0000000..b5d6268 Binary files /dev/null and b/source/img/crop-polygon.png differ diff --git a/source/img/join.png b/source/img/join.png new file mode 100644 index 0000000..b331391 Binary files /dev/null and b/source/img/join.png differ diff --git a/source/img/logo.png b/source/img/logo.png new file mode 100644 index 0000000..3a64c2b Binary files /dev/null and b/source/img/logo.png differ diff --git a/source/img/simplify.png b/source/img/simplify.png new file mode 100644 index 0000000..ed830a2 Binary files /dev/null and b/source/img/simplify.png differ diff --git a/source/kartograph.html b/source/kartograph.html new file mode 100644 index 0000000..f4a0b4d --- /dev/null +++ b/source/kartograph.html @@ -0,0 +1,326 @@ + + + + + kartograph.py + + + +
+
+
+

kartograph.py

+
+
+
+
+
+ # +
+ +
+
+
from options import parse_options
+from shapely.geometry import Polygon, LineString, MultiPolygon
+from errors import *
+from copy import deepcopy
+from renderer import SvgRenderer, KmlRenderer
+from map import Map
+
+
+
+
+
+
+ # +
+

Kartograph

+
+
+
verbose = False
+
+
+
+
+
+
+ # +
+

These renderers are currently available. See renderer/svg.py and renderer/kml.py +for more details on those.

+
+
+
_known_renderer = {
+    'svg': SvgRenderer,
+    'kml': KmlRenderer
+}
+
+
+
+
+
+
+ # +
+ +
+
+
class Kartograph(object):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self):
+        self.layerCache = {}
+        pass
+
+
+
+
+
+
+ # +
+

Generates a the map and renders it using the specified output format.

+
+
+
    def generate(self, opts, outfile=None, format='svg', preview=None):
+
+
+
+
+
+
+ # +
+ +
+
+
        if preview is None:
+            preview = outfile is not None
+
+
+
+
+
+
+ # +
+

Create a deep copy of the options dictionary so our changes will not be +visible to the calling application.

+
+
+
        opts = deepcopy(opts)
+
+
+
+
+
+
+ # +
+

Parse the options dictionary. See options.py for more details.

+
+
+
        parse_options(opts)
+
+
+
+
+
+
+ # +
+

Create the map instance. It will do all the hard work for us, so you +definitely should check out map.py for all the fun stuff happending +there..

+
+
+
        _map = Map(opts, self.layerCache, format=format)
+
+
+
+
+
+
+ # +
+

Check if the format is handled by a renderer.

+
+
+
        format = format.lower()
+        if format in _known_renderer:
+
+
+
+
+
+
+ # +
+

Create a renderer instance and render the map.

+
+
+
            renderer = _known_renderer[format](_map)
+            renderer.render()
+
+
+
+
+
+
+ # +
+

If requested, we try to preview the created map now, which means that +we open the created SVG file in Firefox or open the KML in Google Earth. +Of course, preview modes are highly dependent on the operating system, but +we don't care about that now.

+
+
+
            if preview:
+                renderer.preview()
+
+
+
+
+
+
+ # +
+

Write the map to a file or return the renderer instance.

+
+
+
            if outfile is None:
+                return renderer
+            else:
+                renderer.write(outfile)
+        else:
+            raise KartographError('unknown format: %s' % format)
+
+
+
+
+
+
+ # +
+

Here are some handy methods for debugging Kartograph. It will plot a given shapely +geometry using matplotlib and descartes.

+
+
+
def _plot_geometry(geom, fill='#ffcccc', stroke='#333333', alpha=1, msg=None):
+    from matplotlib import pyplot
+    from matplotlib.figure import SubplotParams
+    from descartes import PolygonPatch
+
+    if isinstance(geom, (Polygon, MultiPolygon)):
+        b = geom.bounds
+        geoms = hasattr(geom, 'geoms') and geom.geoms or [geom]
+        w, h = (b[2] - b[0], b[3] - b[1])
+        ratio = w / h
+        pad = 0.15
+        fig = pyplot.figure(1, figsize=(5, 5 / ratio), dpi=110, subplotpars=SubplotParams(left=pad, bottom=pad, top=1 - pad, right=1 - pad))
+        ax = fig.add_subplot(111, aspect='equal')
+        for geom in geoms:
+            patch1 = PolygonPatch(geom, linewidth=0.5, fc=fill, ec=stroke, alpha=alpha, zorder=0)
+            ax.add_patch(patch1)
+    p = (b[2] - b[0]) * 0.03  # some padding
+    pyplot.axis([b[0] - p, b[2] + p, b[3] + p, b[1] - p])
+    pyplot.grid(True)
+    if msg:
+        fig.suptitle(msg, y=0.04, fontsize=9)
+    pyplot.show()
+
+
+
+
+
+
+ # +
+ +
+
+
def _plot_lines(lines):
+    from matplotlib import pyplot
+
+
+
+
+
+
+ # +
+ +
+
+
    def plot_line(ax, line):
+        filtered = []
+        for pt in line:
+            if not pt.deleted:
+                filtered.append(pt)
+        if len(filtered) < 2:
+            return
+        ob = LineString(line)
+        x, y = ob.xy
+        ax.plot(x, y, '-', color='#333333', linewidth=0.5, solid_capstyle='round', zorder=1)
+
+    fig = pyplot.figure(1, figsize=(4, 5.5), dpi=90, subplotpars=SubplotParams(left=0, bottom=0.065, top=1, right=1))
+    ax = fig.add_subplot(111, aspect='equal')
+    for line in lines:
+        plot_line(ax, line)
+    pyplot.grid(False)
+    ax.xaxis.set_visible(False)
+    ax.yaxis.set_visible(False)
+    ax.set_frame_on(False)
+    return (ax, fig)
+
+
+
+
+
+
+ # +
+ +
+
+
def _debug_show_features(features, message=None):
+    from descartes import PolygonPatch
+    from matplotlib import pyplot
+    from matplotlib.figure import SubplotParams
+
+    fig = pyplot.figure(1, figsize=(9, 5.5), dpi=110, subplotpars=SubplotParams(left=0, bottom=0.065, top=1, right=1))
+    ax = fig.add_subplot(111, aspect='equal')
+    b = (100000, 100000, -100000, -100000)
+    for feat in features:
+        if feat.geom is None:
+            continue
+        c = feat.geom.bounds
+        b = (min(c[0], b[0]), min(c[1], b[1]), max(c[2], b[2]), max(c[3], b[3]))
+        geoms = hasattr(feat.geom, 'geoms') and feat.geom.geoms or [feat.geom]
+        for geom in geoms:
+            patch1 = PolygonPatch(geom, linewidth=0.25, fc='#ddcccc', ec='#000000', alpha=0.75, zorder=0)
+            ax.add_patch(patch1)
+    p = (b[2] - b[0]) * 0.05  # some padding
+    pyplot.axis([b[0] - p, b[2] + p, b[3], b[1] - p])
+    ax.xaxis.set_visible(False)
+    ax.yaxis.set_visible(False)
+    ax.set_frame_on(True)
+    if message:
+        fig.suptitle(message, y=0.04, fontsize=9)
+    pyplot.show()
+
+
+
+
+
+
+ diff --git a/source/layersource/__init__.html b/source/layersource/__init__.html new file mode 100644 index 0000000..5d6b737 --- /dev/null +++ b/source/layersource/__init__.html @@ -0,0 +1,68 @@ + + + + + __init__.py + + + +
+
+
+

__init__.py

+
+
+
+
+
+ # +
+

as of version 2.0 kartograph supports multiple import formats

+
    +
  • Shapefile
  • +
  • KML ? (only polygons and polylines)
  • +
  • GeoJSON ?
  • +
+
+
+
__all__ = ['LayerSource', 'ShapefileLayer', 'GraticuleLayer']
+
+from shplayer import ShapefileLayer
+from layersource import LayerSource
+from special import GraticuleLayer, SeaLayer
+from kartograph.errors import *
+
+
+
+
+
+
+ # +
+ +
+
+
def handle_layer_source(layer, cache={}):
+    if 'src' in layer:
+        src = layer['src']
+        if src in cache:
+            return cache[src]
+        if src[-4:].lower() == ".shp":  # shapefile layer
+            src = ShapefileLayer(src)
+        if isinstance(src, LayerSource):
+            cache[layer['src']] = src
+            return src
+        else:
+            raise KartographLayerSourceError('don\'t know how to handle "' + src + '"')
+    elif 'special' in layer:
+        if layer['special'] == 'graticule':
+            return GraticuleLayer()
+        elif layer['special'] == 'sea':
+            return SeaLayer()
+
+
+
+
+
+
+ diff --git a/source/layersource/layersource.html b/source/layersource/layersource.html new file mode 100644 index 0000000..f4be231 --- /dev/null +++ b/source/layersource/layersource.html @@ -0,0 +1,55 @@ + + + + + layersource.py + + + +
+
+
+

layersource.py

+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

base class for layer source data providers (e.g. shapefiles)

+
+
+
class LayerSource:
+
+
+
+
+
+
+ # +
+ +
+
+
    def get_features(self, attr=None, filter=None, bbox=None):
+        raise NotImplementedError()
+
+
+
+
+
+
+ diff --git a/source/layersource/shapefile.html b/source/layersource/shapefile.html new file mode 100644 index 0000000..7db0d00 --- /dev/null +++ b/source/layersource/shapefile.html @@ -0,0 +1,2669 @@ + + + + + shapefile.py + + + +
+
+
+

shapefile.py

+
+
+
+
+
+ # +
+

shapefile.py +Provides read and write support for ESRI Shapefiles. +author: jlawheadgeospatialpython.com +date: 20110927 +version: 1.1.4 +Compatible with Python versions 2.4-3.x

+
+
+
from struct import pack, unpack, calcsize, error
+import os
+import sys
+import time
+import array
+
+
+
+
+
+
+ # +
+

Constants for shape types

+
+
+
NULL = 0
+POINT = 1
+POLYLINE = 3
+POLYGON = 5
+MULTIPOINT = 8
+POINTZ = 11
+POLYLINEZ = 13
+POLYGONZ = 15
+MULTIPOINTZ = 18
+POINTM = 21
+POLYLINEM = 23
+POLYGONM = 25
+MULTIPOINTM = 28
+MULTIPATCH = 31
+
+PYTHON3 = sys.version_info[0] == 3
+
+
+
+
+
+
+ # +
+ +
+
+
def b(v):
+    if PYTHON3:
+        if isinstance(v, str):
+
+
+
+
+
+
+ # +
+

For python 3 encode str to bytes.

+
+
+
            return v.encode('utf-8')
+        elif isinstance(v, bytes):
+
+
+
+
+
+
+ # +
+

Already bytes.

+
+
+
            return v
+        else:
+
+
+
+
+
+
+ # +
+

Error.

+
+
+
            raise Exception('Unknown input type')
+    else:
+
+
+
+
+
+
+ # +
+

For python 2 assume str passed in and return str.

+
+
+
        return v
+
+
+
+
+
+
+ # +
+ +
+
+
def u(v):
+    if PYTHON3:
+        if isinstance(v, bytes):
+
+
+
+
+
+
+ # +
+

For python 3 decode bytes to str.

+
+
+
            return v.decode('utf-8')
+        elif isinstance(v, str):
+
+
+
+
+
+
+ # +
+

Already str.

+
+
+
            return v
+        else:
+
+
+
+
+
+
+ # +
+

Error.

+
+
+
            raise Exception('Unknown input type')
+    else:
+
+
+
+
+
+
+ # +
+

For python 2 assume str passed in and return str.

+
+
+
        return v
+
+
+
+
+
+
+ # +
+ +
+
+
def is_string(v):
+    if PYTHON3:
+        return isinstance(v, str)
+    else:
+        return isinstance(v, basestring)
+
+
+
+
+
+
+ # +
+

Converts python tuples to lits of the appropritate type.

+
+
+
class _Array(array.array):
+
+
+
+
+
+
+ # +
+

Used to unpack different shapefile header parts.""" +def repr(self): + return str(self.tolist())

+

s _Shape: +def init(self, shapeType=None): +Stores the geometry of the different shape types

+
+
+
        specified in the Shapefile spec. Shape types are
+        usually point, polyline, or polygons. Every shape type
+        except the "Null" type contains points at some level for
+        example verticies in a polygon. If a shape type has
+        multiple shapes containing points within a single
+        geometry record then those shapes are called parts. Parts
+        are designated by their starting index in geometry record's
+        list of shapes."""
+        self.shapeType = shapeType
+        self.points = []
+#DIVIDER
+class _ShapeRecord:
+#DIVIDER
+    def __init__(self, shape=None, record=None):
+        self.shape = shape
+        self.record = record
+#DIVIDER
+class ShapefileException(Exception):
+#DIVIDER
+    pass
+#DIVIDER
+class Reader:
+#DIVIDER
+    def __init__(self, *args, **kwargs):
+        self.shp = None
+        self.shx = None
+        self.dbf = None
+        self.shapeName = "Not specified"
+        self._offsets = []
+        self.shpLength = None
+        self.numRecords = None
+        self.fields = []
+        self.__dbfHdrLength = 0
+#DIVIDER
+        if len(args) > 0:
+            if type(args[0]) is type("stringTest"):
+                self.load(args[0])
+                return
+        if "shp" in kwargs.keys():
+            if hasattr(kwargs["shp"], "read"):
+                self.shp = kwargs["shp"]
+                if hasattr(self.shp, "seek"):
+                    self.shp.seek(0)
+            if "shx" in kwargs.keys():
+                if hasattr(kwargs["shx"], "read"):
+                    self.shx = kwargs["shx"]
+                    if hasattr(self.shx, "seek"):
+                        self.shx.seek(0)
+        if "dbf" in kwargs.keys():
+            if hasattr(kwargs["dbf"], "read"):
+                self.dbf = kwargs["dbf"]
+                if hasattr(self.dbf, "seek"):
+                    self.dbf.seek(0)
+        if self.shp or self.dbf:        
+            self.load()
+        else:
+            raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.")
+#DIVIDER
+    def load(self, shapefile=None):
+#DIVIDER
+        available. If not a ShapefileException is raised."""
+        if not f:
+            raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.")
+        if self.shp and self.shpLength is None:
+            self.load()
+        if self.dbf and len(self.fields) == 0:
+            self.load()
+        return f
+
+
+
+
+
+
+ # +
+

A shape object of any type.

+
+
+
    def __restrictIndex(self, i):
+
+
+
+
+
+
+ # +
+ +
+
+
        if not self.shp:
+            raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no shp file found")
+        shp = self.shp
+
+
+
+
+
+
+ # +
+

An exception to handle shapefile specific problems.

+
+
+
        shp.seek(24)
+        self.shpLength = unpack(">i", shp.read(4))[0] * 2
+
+
+
+
+
+
+ # +
+ +
+
+
        shp.seek(32)
+        self.shapeType= unpack("<i", shp.read(4))[0]
+
+
+
+
+
+
+ # +
+

Reads the three files of a shapefile as a unit or

+
+
+
        self.bbox = _Array('d', unpack("<4d", shp.read(32)))
+
+
+
+
+
+
+ # +
+

separately. If one of the three files (.shp, .shx, +.dbf) is missing no exception is thrown until you try +to call a method that depends on that particular file. +The .shx index file is used if available for efficiency +but is not required to read the geometry from the .shp +file. The "shapefile" argument in the constructor is the +name of the file you want to open.

+

You can instantiate a Reader without specifying a shapefile +and then specify one later with the load() method.

+

Only the shapefile headers are read upon loading. Content +within each file is only accessed when required and as +efficiently as possible. Shapefiles are usually not large +but they can be.

+
+
+
        self.elevation = _Array('d', unpack("<2d", shp.read(16)))
+
+
+
+
+
+
+ # +
+

See if a shapefile name was passed as an argument

+
+
+
        self.measure = _Array('d', unpack("<2d", shp.read(16)))
+
+
+
+
+
+
+ # +
+

Opens a shapefile from a filename or file-like

+
+
+
    def __shape(self):
+
+
+
+
+
+
+ # +
+

object. Normally this method would be called by the +constructor with the file object or file name as an +argument.""" +if shapefile: + (shapeName, ext) = os.path.splitext(shapefile) + self.shapeName = shapeName + try: + self.shp = open("%s.shp" % shapeName, "rb") + except IOError: + raise ShapefileException("Unable to open %s.shp" % shapeName) + try: + self.shx = open("%s.shx" % shapeName, "rb") + except IOError: + raise ShapefileException("Unable to open %s.shx" % shapeName) + try: + self.dbf = open("%s.dbf" % shapeName, "rb") + except IOError: + raise ShapefileException("Unable to open %s.dbf" % shapeName) +if self.shp: + self.shpHeader() +if self.dbf: + self.dbfHeader()

+

__getFileObj(self, f): +Checks to see if the requested shapefile file object is

+
+
+
        f = self.__getFileObj(self.shp)
+        record = _Shape()
+        nParts = nPoints = zmin = zmax = mmin = mmax = None
+        (recNum, recLength) = unpack(">2i", f.read(8))
+        shapeType = unpack("<i", f.read(4))[0]
+        record.shapeType = shapeType
+
+
+
+
+
+
+ # +
+

Provides list-like handling of a record index with a clearer

+
+
+
        if shapeType == 0:
+            record.points = []
+
+
+
+
+
+
+ # +
+

error message if the index is out of bounds.""" +if self.numRecords: + rmax = self.numRecords - 1 + if abs(i) > rmax: + raise IndexError("Shape or Record index out of range.") + if i < 0: i = range(self.numRecords)[i] +return i

+

__shpHeader(self): +Reads the header information from a .shp or .shx file.

+
+
+
        elif shapeType in (3,5,8,13,15,18,23,25,28,31):
+            record.bbox = _Array('d', unpack("<4d", f.read(32)))
+
+
+
+
+
+
+ # +
+

File length (16-bit word * 2 = bytes)

+
+
+
        if shapeType in (3,5,13,15,23,25,31):
+            nParts = unpack("<i", f.read(4))[0]
+
+
+
+
+
+
+ # +
+

Shape type

+
+
+
        if shapeType in (3,5,8,13,15,23,25,31):
+            nPoints = unpack("<i", f.read(4))[0]
+
+
+
+
+
+
+ # +
+

The shapefile's bounding box (lower left, upper right)

+
+
+
        if nParts:
+            record.parts = _Array('i', unpack("<%si" % nParts, f.read(nParts * 4)))
+
+
+
+
+
+
+ # +
+

Elevation

+
+
+
        if shapeType == 31:
+            record.partTypes = _Array('i', unpack("<%si" % nParts, f.read(nParts * 4)))
+
+
+
+
+
+
+ # +
+

Measure

+
+
+
        if nPoints:
+            record.points = [_Array('d', unpack("<2d", f.read(16))) for p in range(nPoints)]
+
+
+
+
+
+
+ # +
+

Returns the header info and geometry for a single shape.

+
+
+
        if shapeType in (13,15,18,31):
+            (zmin, zmax) = unpack("<2d", f.read(16))
+            record.z = _Array('d', unpack("<%sd" % nPoints, f.read(nPoints * 8)))
+
+
+
+
+
+
+ # +
+ +
+
+
        if shapeType in (13,18,23,25,28,31):
+            (mmin, mmax) = unpack("<2d", f.read(16))
+
+
+
+
+
+
+ # +
+

For Null shapes create an empty points list for consistency

+
+
+
            record.m = []
+            for m in _Array('d', unpack("%sd" % nPoints, f.read(nPoints * 8))):
+                if m > -10e38:
+                    record.m.append(m)
+                else:
+                    record.m.append(None)
+
+
+
+
+
+
+ # +
+

All shape types capable of having a bounding box

+
+
+
        if shapeType in (1,11,21):
+            record.points = [_Array('d', unpack("<2d", f.read(16)))]
+
+
+
+
+
+
+ # +
+

Shape types with parts

+
+
+
        if shapeType == 11:
+            record.z = unpack("<d", f.read(8))
+
+
+
+
+
+
+ # +
+

Shape types with points

+
+
+
        if shapeType in (11,21):
+            record.m = unpack("<d", f.read(8))
+        return record
+
+
+
+
+
+
+ # +
+

Read parts

+
+
+
    def __shapeIndex(self, i=None):
+
+
+
+
+
+
+ # +
+

Read part types for Multipatch - 31

+
+
+
        record file."""
+        shp = self.__getFileObj(self.shp)
+        i = self.__restrictIndex(i)
+        offset = self.__shapeIndex(i)
+        if not offset:
+#DIVIDER
+            shapes = self.shapes()
+            return shapes[i]
+        shp.seek(offset)
+        return self.__shape()
+#DIVIDER
+    def shapes(self):
+#DIVIDER
+        shp = self.__getFileObj(self.shp)
+        shp.seek(100)
+        shapes = []
+        while shp.tell() < self.shpLength:
+            shapes.append(self.__shape())
+        return shapes
+#DIVIDER
+    def __dbfHeaderLength(self):
+#DIVIDER
+        if not self.__dbfHdrLength:
+            if not self.dbf:
+                raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no dbf file found)")
+            dbf = self.dbf
+            (self.numRecords, self.__dbfHdrLength) = \
+                    unpack("<xxxxLH22x", dbf.read(32))
+        return self.__dbfHdrLength
+#DIVIDER
+    def __dbfHeader(self):
+#DIVIDER
+        if not self.dbf:
+            raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no dbf file found)")
+        dbf = self.dbf
+        headerLength = self.__dbfHeaderLength()
+        numFields = (headerLength - 33) // 32
+        for field in range(numFields):
+            fieldDesc = list(unpack("<11sc4xBB14x", dbf.read(32)))
+            name = 0
+            idx = 0
+            if b("\x00") in fieldDesc[name]:
+                idx = fieldDesc[name].index(b("\x00"))
+            else:
+                idx = len(fieldDesc[name]) - 1
+            fieldDesc[name] = fieldDesc[name][:idx]
+            fieldDesc[name] = u(fieldDesc[name])
+            fieldDesc[name] = fieldDesc[name].lstrip()
+            fieldDesc[1] = u(fieldDesc[1])
+            self.fields.append(fieldDesc)
+        terminator = dbf.read(1)
+        assert terminator == b("\r")
+        self.fields.insert(0, ('DeletionFlag', 'C', 1, 0))
+#DIVIDER
+    def __recordFmt(self):
+#DIVIDER
+        if not self.numRecords:
+            self.__dbfHeader()
+        fmt = ''.join(['%ds' % fieldinfo[2] for fieldinfo in self.fields])
+        fmtSize = calcsize(fmt)
+        return (fmt, fmtSize)
+#DIVIDER
+    def __record(self):
+#DIVIDER
+        f = self.__getFileObj(self.dbf)
+        recFmt = self.__recordFmt()
+        recordContents = unpack(recFmt[0], f.read(recFmt[1]))
+        if recordContents[0] != b(' '):
+#DIVIDER
+            return None
+        record = []
+        for (name, typ, size, deci), value in zip(self.fields,
+                                                                                                recordContents):
+            if name == 'DeletionFlag':
+                continue
+            elif not value.strip():
+                record.append(value)
+                continue
+            elif typ == "N":
+                value = value.replace(b('\0'), b('')).strip()
+                if value == b(''):
+                    value = 0
+                elif deci:
+                    value = float(value)
+                else:
+                    value = int(value)
+            elif typ == b('D'):
+                try:
+                    y, m, d = int(value[:4]), int(value[4:6]), int(value[6:8])
+                    value = [y, m, d]
+                except:
+                    value = value.strip()
+            elif typ == b('L'):
+                value = (value in b('YyTt') and b('T')) or \
+                                        (value in b('NnFf') and b('F')) or b('?')
+            else:
+                value = u(value)
+                value = value.strip()
+            record.append(value)
+        return record
+#DIVIDER
+    def record(self, i=0):
+#DIVIDER
+        f = self.__getFileObj(self.dbf)
+        if not self.numRecords:
+            self.__dbfHeader()
+        i = self.__restrictIndex(i)
+        recSize = self.__recordFmt()[1]
+        f.seek(0)
+        f.seek(self.__dbfHeaderLength() + (i * recSize))
+        return self.__record()
+#DIVIDER
+    def records(self):
+#DIVIDER
+        if not self.numRecords:
+            self.__dbfHeader()
+        records = []
+        f = self.__getFileObj(self.dbf)
+        f.seek(self.__dbfHeaderLength())
+        for i in range(self.numRecords):
+            r = self.__record()
+            if r:
+                records.append(r)
+        return records
+#DIVIDER
+    def shapeRecord(self, i=0):
+#DIVIDER
+        all records in a shapefile."""
+        shapeRecords = []
+        return [_ShapeRecord(shape=rec[0], record=rec[1]) \
+                                for rec in zip(self.shapes(), self.records())]
+
+
+
+
+
+
+ # +
+

Read points - produces a list of [x,y] values

+
+
+
class Writer:
+
+
+
+
+
+
+ # +
+

Read z extremes and values

+
+
+
    def __init__(self, shapeType=None):
+        self._shapes = []
+        self.fields = []
+        self.records = []
+        self.shapeType = shapeType
+        self.shp = None
+        self.shx = None
+        self.dbf = None
+
+
+
+
+
+
+ # +
+

Read m extremes and values

+
+
+
        self._offsets = []
+        self._lengths = []
+
+
+
+
+
+
+ # +
+

Measure values less than -10e38 are nodata values according to the spec

+
+
+
        self.deletionFlag = 0
+
+
+
+
+
+
+ # +
+

Read a single point

+
+
+
    def __getFileObj(self, f):
+
+
+
+
+
+
+ # +
+

Read a single Z value

+
+
+
        if not f:
+            raise ShapefileException("No file-like object available.")
+        elif hasattr(f, "write"):
+            return f
+        else:
+            pth = os.path.split(f)[0]
+            if pth and not os.path.exists(pth):
+                os.makedirs(pth)
+            return open(f, "wb")
+
+
+
+
+
+
+ # +
+

Read a single M value

+
+
+
    def __shpFileLength(self):
+
+
+
+
+
+
+ # +
+

Returns the offset in a .shp file for a shape based on information

+
+
+
        size = 100
+
+
+
+
+
+
+ # +
+

in the .shx index file.""" +shx = self.shx +if not shx: + return None +if not self._offsets: + # File length (16-bit word * 2 = bytes) - header length + shx.seek(24) + shxRecordLength = (unpack(">i", shx.read(4))[0] * 2) - 100 + numRecords = shxRecordLength // 8 + # Jump to the first record. + shx.seek(100) + for r in range(numRecords): + # Offsets are 16-bit words just like the file length + self._offsets.append(unpack(">i", shx.read(4))[0] * 2) + shx.seek(shx.tell() + 4) +if not i == None: + return self._offsets[i]

+

shape(self, i=0): +Returns a shape object for a shape in the the geometry

+
+
+
        for s in self._shapes:
+
+
+
+
+
+
+ # +
+

Shx index not available so use the full list.

+
+
+
            size += 12
+
+
+
+
+
+
+ # +
+

Returns all shapes in a shapefile.

+
+
+
            if hasattr(s,'parts'):
+                nParts = len(s.parts)
+            if hasattr(s,'points'):
+                nPoints = len(s.points)
+
+
+
+
+
+
+ # +
+ +
+
+
            if self.shapeType in (3,5,8,13,15,18,23,25,28,31):
+                size += 32
+
+
+
+
+
+
+ # +
+

Retrieves the header length of a dbf file header.

+
+
+
            if self.shapeType in (3,5,13,15,23,25,31):
+
+
+
+
+
+
+ # +
+ +
+
+
                size += 4
+
+
+
+
+
+
+ # +
+

Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger

+
+
+
                size += nParts * 4
+
+
+
+
+
+
+ # +
+ +
+
+
            if self.shapeType in (3,5,8,13,15,23,25,31):
+
+
+
+
+
+
+ # +
+

Calculates the size of a .shp geometry record.

+
+
+
                size += 4
+
+
+
+
+
+
+ # +
+ +
+
+
                size += 16 * nPoints
+
+
+
+
+
+
+ # +
+

Reads and returns a dbf record row as a list of values.

+
+
+
            if self.shapeType == 31:
+                size += nParts * 4
+
+
+
+
+
+
+ # +
+ +
+
+
            if self.shapeType in (13,15,18,31):
+
+
+
+
+
+
+ # +
+

deleted record

+
+
+
                size += 16
+
+
+
+
+
+
+ # +
+

Returns a specific dbf record based on the supplied index.

+
+
+
                size += 8 * nPoints
+
+
+
+
+
+
+ # +
+ +
+
+
            if self.shapeType in (23,25,31):
+
+
+
+
+
+
+ # +
+

Returns all records in a dbf file.

+
+
+
                size += 16
+
+
+
+
+
+
+ # +
+ +
+
+
                size += 8 * nPoints
+
+
+
+
+
+
+ # +
+

Returns a combination geometry and attribute record for the

+
+
+
            if self.shapeType in (1,11,21):
+                size += 16
+
+
+
+
+
+
+ # +
+

supplied record index.""" +i = self.__restrictIndex(i) +return _ShapeRecord(shape=self.shape(i), + record=self.record(i))

+

shapeRecords(self): +Returns a list of combination geometry/attribute records for

+
+
+
            if self.shapeType == 11:
+                size += 8
+
+
+
+
+
+
+ # +
+

Provides write support for ESRI Shapefiles.

+
+
+
            if self.shapeType in (11,21):
+                size += 8
+
+
+
+
+
+
+ # +
+ +
+
+
        size //= 2
+        return size
+
+
+
+
+
+
+ # +
+

Geometry record offsets and lengths for writing shx file.

+
+
+
    def __bbox(self, shapes, shapeTypes=[]):
+        x = []
+        y = []
+        for s in shapes:
+            shapeType = self.shapeType
+            if shapeTypes:
+                shapeType = shapeTypes[shapes.index(s)]
+            px, py = list(zip(*s.points))[:2]
+            x.extend(px)
+            y.extend(py)
+        return [min(x), min(y), max(x), max(y)]
+
+
+
+
+
+
+ # +
+

Use deletion flags in dbf? Default is false (0).

+
+
+
    def __zbox(self, shapes, shapeTypes=[]):
+        z = []
+        for s in shapes:
+            try:
+                for p in s.points:
+                    z.append(p[2])
+            except IndexError:
+                pass
+        if not z: z.append(0)
+        return [min(z), max(z)]
+
+
+
+
+
+
+ # +
+

Safety handler to verify file-like objects

+
+
+
    def __mbox(self, shapes, shapeTypes=[]):
+        m = [0]
+        for s in shapes:
+            try:
+                for p in s.points:
+                    m.append(p[3])
+            except IndexError:
+                pass
+        return [min(m), max(m)]
+
+
+
+
+
+
+ # +
+ +
+
+
    def bbox(self):
+
+
+
+
+
+
+ # +
+

Calculates the file length of the shp file.

+
+
+
        return self.__zbox(self._shapes)
+
+
+
+
+
+
+ # +
+

Start with header length

+
+
+
    def mbox(self):
+
+
+
+
+
+
+ # +
+

Calculate size of all shapes

+
+
+
        return self.__mbox(self._shapes)
+
+
+
+
+
+
+ # +
+

Add in record header and shape type fields

+
+
+
    def __shapefileHeader(self, fileObj, headerType='shp'):
+
+
+
+
+
+
+ # +
+

nParts and nPoints do not apply to all shapes +if self.shapeType not in (0,1): + nParts = len(s.parts) + nPoints = len(s.points)

+
+
+
        f = self.__getFileObj(self.dbf)
+        f.seek(0)
+        version = 3
+        year, month, day = time.localtime()[:3]
+        year -= 1900
+
+
+
+
+
+
+ # +
+

All shape types capable of having a bounding box

+
+
+
        for field in self.fields:
+            if field[0].startswith("Deletion"):
+                self.fields.remove(field)
+        numRecs = len(self.records)
+        numFields = len(self.fields)
+        headerLength = numFields * 32 + 33
+        recordLength = sum([int(field[2]) for field in self.fields]) + 1
+        header = pack('<BBBBLHH20x', version, year, month, day, numRecs,
+                headerLength, recordLength)
+        f.write(header)
+
+
+
+
+
+
+ # +
+

Shape types with parts

+
+
+
        for field in self.fields:
+            name, fieldType, size, decimal = field
+            name = b(name)
+            name = name.replace(b(' '), b('_'))
+            name = name.ljust(11).replace(b(' '), b('\x00'))
+            fieldType = b(fieldType)
+            size = int(size)
+            fld = pack('<11sc4xBB14x', name, fieldType, size, decimal)
+            f.write(fld)
+
+
+
+
+
+
+ # +
+

Parts count

+
+
+
        f.write(b('\r'))
+
+
+
+
+
+
+ # +
+

Parts index array

+
+
+
    def __shpRecords(self):
+
+
+
+
+
+
+ # +
+

Shape types with points

+
+
+
        f = self.__getFileObj(self.shp)
+        f.seek(100)
+        recNum = 1
+        for s in self._shapes:
+            self._offsets.append(f.tell())
+
+
+
+
+
+
+ # +
+

Points count

+
+
+
            f.write(pack(">2i", recNum, 0))
+            recNum += 1
+            start = f.tell()
+
+
+
+
+
+
+ # +
+

Points array

+
+
+
            f.write(pack("<i", s.shapeType))
+
+
+
+
+
+
+ # +
+

Calc size of part types for Multipatch (31)

+
+
+
            if s.shapeType in (3,5,8,13,15,18,23,25,28,31):
+                try:
+                    f.write(pack("<4d", *self.__bbox([s])))
+                except error:
+                    raise ShapefileException("Falied to write bounding box for record %s. Expected floats." % recNum)
+
+
+
+
+
+
+ # +
+

Calc z extremes and values

+
+
+
            if s.shapeType in (3,5,13,15,23,25,31):
+
+
+
+
+
+
+ # +
+

z extremes

+
+
+
                f.write(pack("<i", len(s.parts)))
+
+
+
+
+
+
+ # +
+

z array

+
+
+
            if s.shapeType in (3,5,8,13,15,23,25,31):
+
+
+
+
+
+
+ # +
+

Calc m extremes and values

+
+
+
                f.write(pack("<i", len(s.points)))
+
+
+
+
+
+
+ # +
+

m extremes

+
+
+
            if s.shapeType in (3,5,13,15,23,25,31):
+                for p in s.parts:
+                    f.write(pack("<i", p))
+
+
+
+
+
+
+ # +
+

m array

+
+
+
            if s.shapeType == 31:
+                for pt in s.partTypes:
+                    f.write(pack("<i", pt))
+
+
+
+
+
+
+ # +
+

Calc a single point

+
+
+
            if s.shapeType in (3,5,8,13,15,23,25,31):
+                try:
+                    [f.write(pack("<2d", *p[:2])) for p in s.points]
+                except error:
+                    raise ShapefileException("Failed to write points for record %s. Expected floats." % recNum)
+
+
+
+
+
+
+ # +
+

Calc a single Z value

+
+
+
            if s.shapeType in (13,15,18,31):
+                try:
+                    f.write(pack("<2d", *self.__zbox([s])))
+                except error:
+                    raise ShapefileException("Failed to write elevation extremes for record %s. Expected floats." % recNum)
+                try:
+                    [f.write(pack("<d", p[2])) for p in s.points]
+                except error:
+                    raise ShapefileException("Failed to write elevation values for record %s. Expected floats." % recNum)
+
+
+
+
+
+
+ # +
+

Calc a single M value

+
+
+
            if s.shapeType in (23,25,31):
+                try:
+                    f.write(pack("<2d", *self.__mbox([s])))
+                except error:
+                    raise ShapefileException("Failed to write measure extremes for record %s. Expected floats" % recNum)
+                try:
+                    [f.write(pack("<d", p[3])) for p in s.points]
+                except error:
+                    raise ShapefileException("Failed to write measure values for record %s. Expected floats" % recNum)
+
+
+
+
+
+
+ # +
+

Calculate size as 16-bit words

+
+
+
            if s.shapeType in (1,11,21):
+                try:
+                    f.write(pack("<2d", s.points[0][0], s.points[0][1]))
+                except error:
+                    raise ShapefileException("Failed to write point for record %s. Expected floats." % recNum)
+
+
+
+
+
+
+ # +
+ +
+
+
            if s.shapeType == 11:
+                try:
+                    f.write(pack("<1d", s.points[0][2]))
+                except error:
+                    raise ShapefileException("Failed to write elevation value for record %s. Expected floats." % recNum)
+
+
+
+
+
+
+ # +
+ +
+
+
            if s.shapeType in (11,21):
+                try:
+                    f.write(pack("<1d", s.points[0][3]))
+                except error:
+                    raise ShapefileException("Failed to write measure value for record %s. Expected floats." % recNum)
+
+
+
+
+
+
+ # +
+ +
+
+
            finish = f.tell()
+            length = (finish - start) // 2
+            self._lengths.append(length)
+
+
+
+
+
+
+ # +
+

Returns the current bounding box for the shapefile which is

+
+
+
            f.seek(start-4)
+            f.write(pack(">i", length))
+            f.seek(finish)
+
+
+
+
+
+
+ # +
+

the lower-left and upper-right corners. It does not contain the +elevation or measure extremes.""" +return self.__bbox(self._shapes)

+

zbox(self): +Returns the current z extremes for the shapefile.

+
+
+
    def __shxRecords(self):
+
+
+
+
+
+
+ # +
+

Returns the current m extremes for the shapefile.

+
+
+
        f = self.__getFileObj(self.shx)
+        f.seek(100)
+        for i in range(len(self._shapes)):
+            f.write(pack(">i", self._offsets[i] // 2))
+            f.write(pack(">i", self._lengths[i]))
+
+
+
+
+
+
+ # +
+ +
+
+
    def __dbfRecords(self):
+
+
+
+
+
+
+ # +
+

Writes the specified header type to the specified file-like object.

+
+
+
        f = self.__getFileObj(self.dbf)
+        for record in self.records:
+            if not self.fields[0][0].startswith("Deletion"):
+                f.write(b(' ')) # deletion flag
+            for (fieldName, fieldType, size, dec), value in zip(self.fields, record):
+                fieldType = fieldType.upper()
+                size = int(size)
+                if fieldType.upper() == "N":
+                    value = str(value).rjust(size)
+                elif fieldType == 'L':
+                    value = str(value)[0].upper()
+                else:
+                    value = str(value)[:size].ljust(size)
+                assert len(value) == size
+                value = b(value)
+                f.write(value)
+
+
+
+
+
+
+ # +
+

Several of the shapefile formats are so similar that a single generic +method to read or write them is warranted.""" +f = self.__getFileObj(fileObj) +f.seek(0)

+

File code, Unused bytes

+

f.write(pack(">6i", 9994,0,0,0,0,0))

+

File length (Bytes / 2 = 16-bit words)

+

if headerType == 'shp': + f.write(pack(">i", self.__shpFileLength())) +elif headerType == 'shx': + f.write(pack('>i', ((100 + (len(self._shapes) * 8)) // 2)))

+

Version, Shape type

+

f.write(pack("<2i", 1000, self.shapeType))

+

The shapefile's bounding box (lower left, upper right)

+

if self.shapeType != 0: + try: + f.write(pack("<4d", *self.bbox())) + except error: + raise ShapefileException("Failed to write shapefile bounding box. Floats required.") +else: + f.write(pack("<4d", 0,0,0,0))

+

Elevation

+

z = self.zbox()

+

Measure

+

m = self.mbox() +try: + f.write(pack("<4d", z[0], z[1], m[0], m[1])) +except error: + raise ShapefileException("Failed to write shapefile elevation and measure values. Floats required.")

+

__dbfHeader(self): +Writes the dbf header and field descriptors.

+
+
+
    def null(self):
+
+
+
+
+
+
+ # +
+

Remove deletion flag placeholder from fields

+
+
+
        self._shapes.append(_Shape(NULL))
+
+
+
+
+
+
+ # +
+

Field descriptors

+
+
+
    def point(self, x, y, z=0, m=0):
+
+
+
+
+
+
+ # +
+

Terminator

+
+
+
        pointShape = _Shape(self.shapeType)
+        pointShape.points.append([x, y, z, m])
+        self._shapes.append(pointShape)
+
+
+
+
+
+
+ # +
+

Write the shp records

+
+
+
    def line(self, parts=[], shapeType=POLYLINE):
+
+
+
+
+
+
+ # +
+ +
+
+
        self.poly(parts, shapeType, [])
+
+
+
+
+
+
+ # +
+

Record number, Content length place holder

+
+
+
    def poly(self, parts=[], shapeType=POLYGON, partTypes=[]):
+
+
+
+
+
+
+ # +
+

Shape Type

+
+
+
        polyShape = _Shape(shapeType)
+        polyShape.parts = []
+        polyShape.points = []
+        for part in parts:
+            polyShape.parts.append(len(polyShape.points))
+            for point in part:
+
+
+
+
+
+
+ # +
+

All shape types capable of having a bounding box

+
+
+
                if not isinstance(point, list):
+                    point = list(point)
+
+
+
+
+
+
+ # +
+

Shape types with parts

+
+
+
                while len(point) < 4:
+                    point.append(0)
+                polyShape.points.append(point)
+        if polyShape.shapeType == 31:
+            if not partTypes:
+                for part in parts:
+                    partTypes.append(polyShape.shapeType)
+            polyShape.partTypes = partTypes
+        self._shapes.append(polyShape)
+
+
+
+
+
+
+ # +
+

Number of parts

+
+
+
    def field(self, name, fieldType="C", size="50", decimal=0):
+
+
+
+
+
+
+ # +
+

Shape types with multiple points per record

+
+
+
        self.fields.append((name, fieldType, size, decimal))
+
+
+
+
+
+
+ # +
+

Number of points

+
+
+
    def record(self, *recordList, **recordDict):
+
+
+
+
+
+
+ # +
+

Write part indexes

+
+
+
        return self._shapes
+
+
+
+
+
+
+ # +
+

Part types for Multipatch (31)

+
+
+
    def saveShp(self, target):
+
+
+
+
+
+
+ # +
+

Write points for multiple-point records

+
+
+
        if not hasattr(target, "write"):
+            target = os.path.splitext(target)[0] + '.shp'
+        if not self.shapeType:
+            self.shapeType = self._shapes[0].shapeType
+        self.shp = self.__getFileObj(target)
+        self.__shapefileHeader(self.shp, headerType='shp')
+        self.__shpRecords()
+
+
+
+
+
+
+ # +
+

Write z extremes and values

+
+
+
    def saveShx(self, target):
+
+
+
+
+
+
+ # +
+

Write m extremes and values

+
+
+
        if not hasattr(target, "write"):
+            target = os.path.splitext(target)[0] + '.shx'
+        if not self.shapeType:
+            self.shapeType = self._shapes[0].shapeType
+        self.shx = self.__getFileObj(target)
+        self.__shapefileHeader(self.shx, headerType='shx')
+        self.__shxRecords()
+
+
+
+
+
+
+ # +
+

Write a single point

+
+
+
    def saveDbf(self, target):
+
+
+
+
+
+
+ # +
+

Write a single Z value

+
+
+
        if not hasattr(target, "write"):
+            target = os.path.splitext(target)[0] + '.dbf'
+        self.dbf = self.__getFileObj(target)
+        self.__dbfHeader()
+        self.__dbfRecords()
+
+
+
+
+
+
+ # +
+

Write a single M value

+
+
+
    def save(self, target=None, shp=None, shx=None, dbf=None):
+
+
+
+
+
+
+ # +
+

Finalize record length as 16-bit words

+
+
+
        pass
+
+
+
+
+
+
+ # +
+

start - 4 bytes is the content length field

+
+
+
    def delete(self, shape=None, part=None, point=None):
+
+
+
+
+
+
+ # +
+

Writes the shx records.

+
+
+
        you to update a specific point by shape, part, point of any
+        shape type."""
+#DIVIDER
+        if shape and part and point:
+            try: self._shapes[shape]
+            except IndexError: self._shapes.append([])
+            try: self._shapes[shape][part]
+            except IndexError: self._shapes[shape].append([])
+            try: self._shapes[shape][part][point]
+            except IndexError: self._shapes[shape][part].append([])
+            p = self._shapes[shape][part][point]
+            if x: p[0] = x
+            if y: p[1] = y
+            if z: p[2] = z
+            if m: p[3] = m
+            self._shapes[shape][part][point] = p
+#DIVIDER
+        elif shape and part and not point:
+            try: self._shapes[shape]
+            except IndexError: self._shapes.append([])
+            try: self._shapes[shape][part]
+            except IndexError: self._shapes[shape].append([])
+            points = self._shapes[shape][part]
+            for i in range(len(points)):
+                p = points[i]
+                if x: p[0] = x
+                if y: p[1] = y
+                if z: p[2] = z
+                if m: p[3] = m
+                self._shapes[shape][part][i] = p
+#DIVIDER
+        elif shape and not part and not point:
+            try: self._shapes[shape]
+            except IndexError: self._shapes.append([])
+#DIVIDER
+        if addr:
+            shape, part, point = addr
+            self._shapes[shape][part][point] = [x, y, z, m]
+        else:
+            Writer.point(self, x, y, z, m)
+        if self.autoBalance:
+            self.balance()
+#DIVIDER
+    def validate(self):
+#DIVIDER
+        on which type of record was created to make sure all three files
+        are in synch."""
+        if len(self.records) > len(self._shapes):
+            self.null()
+        elif len(self.records) < len(self._shapes):
+            self.record()
+
+
+
+
+
+
+ # +
+ +
+
+
    def __fieldNorm(self, fieldName):
+
+
+
+
+
+
+ # +
+

Writes the dbf records.

+
+
+
    Doctests are contained in the module 'pyshp_usage.py'. This library was developed
+    using Python 2.3. Python 2.4 and above have some excellent improvements in the built-in
+    testing libraries but for now unit testing is done using what's available in
+    2.3.
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

Creates a null shape.

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

Creates a point shape.

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

Creates a line shape. This method is just a convienience method

+
+
+
+
+
+
+
+
+
+ # +
+

which wraps 'poly()'.

+
+
+
+
+
+
+
+
+
+ # +
+

Creates a shape that has multiple collections of points (parts)

+
+
+
+
+
+
+
+
+
+ # +
+

including lines, polygons, and even multipoint shapes. If no shape type +is specified it defaults to 'polygon'. If no part types are specified +(which they normally won't be) then all parts default to the shape type.

+
+
+
+
+
+
+
+
+
+ # +
+

Ensure point is list

+
+
+
+
+
+
+
+
+
+ # +
+

Make sure point has z and m values

+
+
+
+
+
+
+
+
+
+ # +
+

Adds a dbf field descriptor to the shapefile.

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

Creates a dbf attribute record. You can submit either a sequence of

+
+
+
+
+
+
+
+
+
+ # +
+

field values or keyword arguments of field names and values. Before +adding records you must add fields for the record values using the +fields() method. If the record values exceed the number of fields the +extra ones won't be added. In the case of using keyword arguments to specify +field/value pairs only fields matching the already registered fields +will be added.""" +record = [] +fieldCount = len(self.fields)

+

Compensate for deletion flag

+

if self.fields[0][0].startswith("Deletion"): fieldCount -= 1 +if recordList: + [record.append(recordList[i]) for i in range(fieldCount)] +elif recordDict: + for field in self.fields: + if field[0] in recordDict: + val = recordDict[field[0]] + if val: + record.append(val) + else: + record.append("") +if record: + self.records.append(record)

+

shape(self, i): +return self._shapes[i]

+

shapes(self): +Return the current list of shapes.

+
+
+
+
+
+
+
+
+
+ # +
+

Save an shp file.

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

Save an shx file.

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

Save a dbf file.

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

Save the shapefile data to three files or

+
+
+
+
+
+
+
+
+
+ # +
+

three file-like objects. SHP and DBF files can also +be written exclusively using saveShp, saveShx, and saveDbf respectively."""

+

TODO: Create a unique filename for target if None.

+

if shp: + self.saveShp(shp) +if shx: + self.saveShx(shx) +if dbf: + self.saveDbf(dbf) +elif target: + self.saveShp(target) + self.shp.close() + self.saveShx(target) + self.shx.close() + self.saveDbf(target) + self.dbf.close()

+

itor(Writer): +init(self, shapefile=None, shapeType=POINT, autoBalance=1): +self.autoBalance = autoBalance +if not shapefile: + Writer.init(self, shapeType) +elif is_string(shapefile): + base = os.path.splitext(shapefile)[0] + if os.path.isfile("%s.shp" % base): + r = Reader(base) + Writer.init(self, r.shapeType) + self._shapes = r.shapes() + self.fields = r.fields + self.records = r.records()

+

select(self, expr): +Select one or more shapes (to be implemented) +TODO: Implement expressions to select shapes.

+
+
+
+
+
+
+
+
+
+ # +
+

Deletes the specified part of any shape by specifying a shape

+
+
+
+
+
+
+
+
+
+ # +
+

number, part number, or point number."""

+

shape, part, point

+

if shape and part and point: + del self._shapes[shape][part][point]

+

shape, part

+

elif shape and part and not point: + del self._shapes[shape][part]

+

shape

+

elif shape and not part and not point: + del self._shapes[shape]

+

point

+

elif not shape and not part and point: + for s in self._shapes: + if s.shapeType == 1: + del self._shapes[point] + else: + for part in s.parts: + del s[part][point]

+

part, point

+

elif not shape and part and point: + for s in self._shapes: + del s[part][point]

+

part

+

elif not shape and part and not point: + for s in self._shapes: + del s[part]

+

point(self, x=None, y=None, z=None, m=None, shape=None, part=None, point=None, addr=None): +Creates/updates a point shape. The arguments allows

+
+
+
+
+
+
+
+
+
+ # +
+

shape, part, point

+
+
+
+
+
+
+
+
+
+ # +
+

shape, part

+
+
+
+
+
+
+
+
+
+ # +
+

shape

+
+
+
+
+
+
+
+
+
+ # +
+

point +part

+
+
+
+
+
+
+
+
+
+ # +
+

An optional method to try and validate the shapefile

+
+
+
+
+
+
+
+
+
+ # +
+

as much as possible before writing it (not implemented)."""

+

TODO: Implement validation method

+

pass

+

balance(self): +Adds a corresponding empty attribute or null geometry record depending

+
+
+
+
+
+
+
+
+
+ # +
+

Normalizes a dbf field name to fit within the spec and the

+
+
+
+
+
+
+
+
+
+ # +
+

expectations of certain ESRI software.""" +if len(fieldName) > 11: fieldName = fieldName[:11] +fieldName = fieldName.upper() +fieldName.replace(' ', '_')

+

Testing +(): +rt doctest +est.NORMALIZE_WHITESPACE = 1 +est.testfile("README.txt", verbose=1)

+

e == "main__":

+
+
+
+
+
+
+
+
+
+ # +
+

test()

+
+
+
+
+
+
+
+ diff --git a/source/layersource/shplayer.html b/source/layersource/shplayer.html new file mode 100644 index 0000000..f5a9675 --- /dev/null +++ b/source/layersource/shplayer.html @@ -0,0 +1,408 @@ + + + + + shplayer.py + + + +
+
+
+

shplayer.py

+
+
+
+
+
+ # +
+ +
+
+
from layersource import LayerSource
+from kartograph.errors import *
+from kartograph.geometry import BBox, create_feature
+
+
+
+
+
+
+ # +
+

this class handles shapefile layers

+
+
+
class ShapefileLayer(LayerSource):
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

initialize shapefile reader

+
+
+
    def __init__(self, src):
+
+
+
+
+
+
+ # +
+ +
+
+
        import shapefile
+        if isinstance(src, unicode):
+            src = src.encode('ascii', 'ignore')
+        self.shpSrc = src
+        self.sr = shapefile.Reader(src)
+        self.recs = []
+        self.shapes = {}
+        self.load_records()
+
+
+
+
+
+
+ # +
+

load shapefile records into memory. note that only the records are loaded and not the shapes.

+
+
+
    def load_records(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        self.recs = self.sr.records()
+        self.attributes = []
+        for a in self.sr.fields[1:]:
+            self.attributes.append(a[0])
+        i = 0
+        self.attrIndex = {}
+        for attr in self.attributes:
+            self.attrIndex[attr] = i
+            i += 1
+
+
+
+
+
+
+ # +
+

returns a shape of this shapefile. if requested for the first time, the shape is loaded from shapefile (slow)

+
+
+
    def get_shape(self, i):
+
+
+
+
+
+
+ # +
+ +
+
+
        if i in self.shapes:  # check cache
+            shp = self.shapes[i]
+        else:  # load shape from shapefile
+            shp = self.shapes[i] = self.sr.shapeRecord(i).shape
+        return shp
+
+
+
+
+
+
+ # +
+

returns a list of features matching to the attr -> value pair

+
+
+
    def get_features(self, attr=None, filter=None, bbox=None, verbose=False, ignore_holes=False, min_area=False, charset='utf-8'):
+
+
+
+
+
+
+ # +
+ +
+
+
        res = []
+        try_encodings = ('utf-8', 'latin-1', 'iso-8859-2')
+        tried_encodings = [charset]
+        if bbox is not None and not isinstance(bbox, BBox):
+            bbox = BBox(bbox[2] - bbox[0], bbox[3] - bbox[1], bbox[0], bbox[1])
+        ignored = 0
+        for i in range(0, len(self.recs)):
+            drec = {}
+            for j in range(len(self.attributes)):
+                drec[self.attributes[j]] = self.recs[i][j]
+            if filter is None or filter(drec):
+                props = {}
+                for j in range(len(self.attributes)):
+                    val = self.recs[i][j]
+                    if isinstance(val, str):
+                        try:
+                            val = val.decode(charset)
+                        except:
+                            print 'warning: could not decode "%s" to %s' % (val, charset)
+                            next_guess = False
+                            for enc in try_encodings:
+                                if enc not in tried_encodings:
+                                    next_guess = enc
+                                    tried_encodings.append(enc)
+                                    break
+                            if next_guess:
+                                print 'trying %s now..' % next_guess
+                                charset = next_guess
+                                j -= 1
+                                continue
+                            else:
+                                raise KartographError('having problems to decode the input data "%s"' % val)
+                    if isinstance(val, (str, unicode)):
+                        val = val.strip()
+                    props[self.attributes[j]] = val
+
+                shp = self.get_shape(i)
+
+                geom = shape2geometry(shp, ignore_holes=ignore_holes, min_area=min_area, bbox=bbox)
+                if geom is None:
+                    ignored += 1
+                    continue
+
+                feature = create_feature(geom, props)
+                res.append(feature)
+        if bbox is not None and ignored > 0 and verbose:
+            print "-ignoring %d shapes (not in bounds %s )" % (ignored, bbox)
+        return res
+
+
+
+
+
+
+ # +
+ +
+
+
def shape2geometry(shp, ignore_holes=False, min_area=False, bbox=False):
+    if bbox:
+        sbbox = BBox(left=shp.bbox[0], top=shp.bbox[1], width=shp.bbox[2] - shp.bbox[0], height=shp.bbox[3] - shp.bbox[1])
+        if not bbox.intersects(sbbox):
+
+
+
+
+
+
+ # +
+

ignore the shape if it's not within the bbox

+
+
+
            return None
+
+    if shp.shapeType in (5, 15):  # multi-polygon
+        geom = shape2polygon(shp, ignore_holes=ignore_holes, min_area=min_area)
+    elif shp.shapeType == 3:  # line
+        geom = points2line(shp)
+    else:
+        raise KartographError('unknown shape type (%d) in shapefile %s' % (shp.shapeType, self.shpSrc))
+    return geom
+
+
+
+
+
+
+ # +
+

converts a shapefile polygon to geometry.MultiPolygon

+
+
+
def shape2polygon(shp, ignore_holes=False, min_area=False):
+
+
+
+
+
+
+ # +
+

from kartograph.geometry import MultiPolygon

+
+
+
    from shapely.geometry import Polygon, MultiPolygon
+    from kartograph.geometry.utils import is_clockwise
+    parts = shp.parts[:]
+    parts.append(len(shp.points))
+    exteriors = []
+    holes = []
+    for j in range(len(parts) - 1):
+        pts = shp.points[parts[j]:parts[j + 1]]
+        if shp.shapeType == 15:
+
+
+
+
+
+
+ # +
+

remove z-coordinate from PolygonZ contours (not supported)

+
+
+
            for k in range(len(pts)):
+                pts[k] = pts[k][:2]
+        cw = is_clockwise(pts)
+        if cw:
+            exteriors.append(pts)
+        else:
+            holes.append(pts)
+    if ignore_holes:
+        holes = None
+    if len(exteriors) == 1:
+        poly = Polygon(exteriors[0], holes)
+    elif len(exteriors) > 1:
+
+
+
+
+
+
+ # +
+

use multipolygon, but we need to assign the holes to the right +exteriors

+
+
+
        from kartograph.geometry import BBox
+        used_holes = set()
+        polygons = []
+        for ext in exteriors:
+            bbox = BBox()
+            my_holes = []
+            for pt in ext:
+                bbox.update(pt)
+            for h in range(len(holes)):
+                if h not in used_holes:
+                    hole = holes[h]
+                    if bbox.check_point(hole[0]):
+
+
+
+
+
+
+ # +
+

this is a very weak test but it should be sufficient

+
+
+
                        used_holes.add(h)
+                        my_holes.append(hole)
+            polygons.append(Polygon(ext, my_holes))
+        if min_area:
+
+
+
+
+
+
+ # +
+

compute maximum area

+
+
+
            max_area = 0
+            for poly in polygons:
+                max_area = max(max_area, poly.area)
+
+
+
+
+
+
+ # +
+

filter out polygons that are below min_area * max_area

+
+
+
            polygons = [poly for poly in polygons if poly.area >= min_area * max_area]
+        poly = MultiPolygon(polygons)
+    else:
+        raise KartographError('shapefile import failed - no outer polygon found')
+    return poly
+
+
+
+
+
+
+ # +
+

converts a shapefile line to geometry.Line

+
+
+
def points2line(shp):
+
+
+
+
+
+
+ # +
+ +
+
+
    from kartograph.geometry import PolyLine
+    parts = shp.parts[:]
+    parts.append(len(shp.points))
+    lines = []
+    for j in range(len(parts) - 1):
+        pts = shp.points[parts[j]:parts[j + 1]]
+        lines.append(pts)
+    return PolyLine(lines)
+
+
+
+
+
+
+ diff --git a/source/map.html b/source/map.html new file mode 100644 index 0000000..e2bc4a5 --- /dev/null +++ b/source/map.html @@ -0,0 +1,746 @@ + + + + + map.py + + + +
+
+
+

map.py

+
+
+
+
+
+ # +
+ +
+
+
from maplayer import MapLayer
+from geometry.utils import geom_to_bbox
+from geometry import BBox, View
+from shapely.geometry.base import BaseGeometry
+from shapely.geometry import Polygon
+from proj import projections
+from filter import filter_record
+from errors import KartographError
+
+_verbose = False
+
+
+
+
+
+
+ # +
+ +
+
+
class Map(object):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(me, options, layerCache, verbose=False, format='svg', src_encoding=None):
+        me.options = options
+        me._verbose = verbose
+        me.format = format
+        me.layers = []
+        me.layersById = {}
+        me._bounds_polygons_cache = False
+        me._unprojected_bounds = None
+        if not src_encoding:
+            src_encoding = 'utf-8'
+        me._source_encoding = src_encoding
+
+        for layer_cfg in options['layers']:
+            layer_id = layer_cfg['id']
+            layer = MapLayer(layer_id, layer_cfg, me, layerCache)
+            me.layers.append(layer)
+            me.layersById[layer_id] = layer
+
+        me.proj = me._init_projection()
+        me.bounds_poly = me._init_bounds()
+        me.view = me._get_view()
+        me.view_poly = me._init_view_poly()
+
+
+
+
+
+
+ # +
+

get features

+
+
+
        for layer in me.layers:
+            layer.get_features()
+
+
+
+
+
+
+ # +
+

_debug_show_features(layerFeatures[id], 'original')

+
+
+
        me._join_layers()
+
+
+
+
+
+
+ # +
+

_debug_show_features(layerFeatures[id], 'joined')

+
+
+
        if options['export']['crop-to-view'] and format != 'kml':
+            me._crop_layers_to_view()
+
+
+
+
+
+
+ # +
+

_debug_show_features(layerFeatures[id], 'cropped to view')

+
+
+
        me._simplify_layers()
+
+
+
+
+
+
+ # +
+

_debug_show_features(layerFeatures[id], 'simplified') +self.crop_layers(layers, layerOpts, layerFeatures)

+
+
+
        me._subtract_layers()
+
+
+
+
+
+
+ # +
+

instantiates the map projection

+
+
+
    def _init_projection(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        if self.format in ('kml', 'json'):
+            return projections['ll']()  # use no projection for KML
+
+        map_center = self.__get_map_center()
+        opts = self.options
+        projC = projections[opts['proj']['id']]
+        p_opts = {}
+        for prop in opts['proj']:
+            if prop != "id":
+                p_opts[prop] = opts['proj'][prop]
+            if prop == "lon0" and p_opts[prop] == "auto":
+                p_opts[prop] = map_center[0]
+            elif prop == "lat0" and p_opts[prop] == "auto":
+                p_opts[prop] = map_center[1]
+        return projC(**p_opts)
+
+
+
+
+
+
+ # +
+

used by _init_projection() to determine the center of the +map projection, depending on the bounds config

+
+
+
    def __get_map_center(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        opts = self.options
+        mode = opts['bounds']['mode']
+        data = opts['bounds']['data']
+
+        lon0 = 0
+
+        if mode == 'bbox':
+            lon0 = data[0] + 0.5 * (data[2] - data[0])
+            lat0 = data[1] + 0.5 * (data[3] - data[1])
+
+        elif mode[:5] == 'point':
+            lon0 = 0
+            lat0 = 0
+            m = 1 / len(data)
+            for (lon, lat) in data:
+                lon0 += m * lon
+                lat0 += m * lat
+
+        elif mode[:4] == 'poly':
+            features = self._get_bounds_polygons()
+            if len(features) > 0:
+                if isinstance(features[0].geom, BaseGeometry):
+                    (lon0, lat0) = features[0].geom.representative_point().coords[0]
+            else:
+                lon0 = 0
+                lat0 = 0
+        else:
+            print "unrecognized bound mode", mode
+        return (lon0, lat0)
+
+
+
+
+
+
+ # +
+

computes the (x,y) bounding box for the map, +given a specific projection

+
+
+
    def _init_bounds(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        if self.format in ('kml', 'json'):
+            return None  # no bounds needed for KML
+
+        from geometry.utils import bbox_to_polygon
+
+        opts = self.options
+        proj = self.proj
+        bnds = opts['bounds']
+        mode = bnds['mode'][:]
+        data = bnds['data']
+
+        if _verbose:
+            print 'using bounds mode', mode
+
+        if mode == "bbox":  # catch special case bbox
+            sea = proj.bounding_geometry(data, projected=True)
+            sbbox = geom_to_bbox(sea)
+            sbbox.inflate(sbbox.width * bnds['padding'])
+            return bbox_to_polygon(sbbox)
+
+        bbox = BBox()
+
+        if mode[:5] == "point":
+            for lon, lat in data:
+                pt = proj.project(lon, lat)
+                bbox.update(pt)
+
+        if mode[:4] == "poly":
+            features = self._get_bounds_polygons()
+            ubbox = BBox()
+            if len(features) > 0:
+                for feature in features:
+                    ubbox.join(geom_to_bbox(feature.geometry))
+                    feature.project(proj)
+                    fbbox = geom_to_bbox(feature.geometry, data["min-area"])
+                    bbox.join(fbbox)
+                self._unprojected_bounds = ubbox
+            else:
+                raise KartographError('no features found for calculating the map bounds')
+        bbox.inflate(bbox.width * bnds['padding'])
+        return bbox_to_polygon(bbox)
+
+
+
+
+
+
+ # +
+

for bounds mode "polygons" this helper function +returns a list of all polygons that the map should +be cropped to

+
+
+
    def _get_bounds_polygons(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        if self._bounds_polygons_cache:
+            return self._bounds_polygons_cache
+
+        opts = self.options
+        features = []
+        data = opts['bounds']['data']
+        id = data['layer']
+
+        if id not in self.layersById:
+            raise KartographError('layer not found "%s"' % id)
+        layer = self.layersById[id]
+
+        if layer.options['filter'] is False:
+            layerFilter = lambda a: True
+        else:
+            layerFilter = lambda rec: filter_record(layer.options['filter'], rec)
+
+        if data['filter']:
+            boundsFilter = lambda rec: filter_record(data['filter'], rec)
+        else:
+            boundsFilter = lambda a: True
+
+        filter = lambda rec: layerFilter(rec) and boundsFilter(rec)
+        features = layer.source.get_features(filter=filter, min_area=data["min-area"], charset=layer.options['charset'])
+
+
+
+
+
+
+ # +
+

remove features that are too small

+
+
+
        if layer.options['filter-islands']:
+            features = [feature for feature in features if feature.geometry.area > layer.options['filter-islands']]
+
+        self._bounds_polygons_cache = features
+        return features
+
+
+
+
+
+
+ # +
+

returns the output view

+
+
+
    def _get_view(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        if self.format in ('kml', 'json'):
+            return View()  # no view transformation needed for KML
+
+        self.src_bbox = bbox = geom_to_bbox(self.bounds_poly)
+        opts = self.options
+        exp = opts["export"]
+        w = exp["width"]
+        h = exp["height"]
+        ratio = exp["ratio"]
+
+        if ratio == "auto":
+            ratio = bbox.width / float(bbox.height)
+
+        if h == "auto":
+            h = w / ratio
+        elif w == "auto":
+            w = h * ratio
+        return View(bbox, w, h - 1)
+
+
+
+
+
+
+ # +
+

creates a polygon that represents the rectangular view bounds +used for cropping the geometries to not overlap the view

+
+
+
    def _init_view_poly(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        if self.format in ('kml', 'json'):
+            return None  # no view polygon needed for KML
+        w = self.view.width
+        h = self.view.height
+        return Polygon([(0, 0), (0, h), (w, h), (w, 0)])
+
+
+
+
+
+
+ # +
+

performs polygon simplification

+
+
+
    def _simplify_layers(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        from simplify import create_point_store, simplify_lines
+
+        point_store = create_point_store()  # create a new empty point store
+
+
+
+
+
+
+ # +
+

compute topology for all layers

+
+
+
        for layer in self.layers:
+            if layer.options['simplify'] is not False:
+                for feature in layer.features:
+                    feature.compute_topology(point_store, layer.options['unify-precision'])
+
+
+
+
+
+
+ # +
+

break features into lines

+
+
+
        for layer in self.layers:
+            if layer.options['simplify'] is not False:
+                for feature in layer.features:
+                    feature.break_into_lines()
+
+
+
+
+
+
+ # +
+

simplify lines

+
+
+
        total = 0
+        kept = 0
+        for layer in self.layers:
+            if layer.options['simplify'] is not False:
+                for feature in layer.features:
+                    lines = feature.break_into_lines()
+                    lines = simplify_lines(lines, layer.options['simplify']['method'], layer.options['simplify']['tolerance'])
+                    for line in lines:
+                        total += len(line)
+                        for pt in line:
+                            if not pt.deleted:
+                                kept += 1
+                    feature.restore_geometry(lines, layer.options['filter-islands'])
+        return (total, kept)
+
+
+
+
+
+
+ # +
+

cuts the layer features to the map view

+
+
+
    def _crop_layers_to_view(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        for layer in self.layers:
+
+
+
+
+
+
+ # +
+

out = []

+
+
+
            for feat in layer.features:
+                if not feat.geometry.is_valid:
+                    pass
+
+
+
+
+
+
+ # +
+

print feat.geometry +_plot_geometry(feat.geometry)

+
+
+
                feat.crop_to(self.view_poly)
+
+
+
+
+
+
+ # +
+

if not feat.is_empty(): + out.append(feat) +layer.features = out

+
+
+
+
+
+
+
+
+
+ # +
+

handles crop-to

+
+
+
    def _crop_layers(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        for layer in self.layers:
+            if layer.options['crop-to'] is not False:
+                cropped_features = []
+                for tocrop in layer.features:
+                    cbbox = geom_to_bbox(tocrop.geom)
+                    crop_at_layer = layer.options['crop-to']
+                    if crop_at_layer not in self.layers:
+                        raise KartographError('you want to substract from layer "%s" which cannot be found' % crop_at_layer)
+                    for crop_at in self.layersById[crop_at_layer].features:
+                        if crop_at.geom.bbox().intersects(cbbox):
+                            tocrop.crop_to(crop_at.geom)
+                            cropped_features.append(tocrop)
+                layer.features = cropped_features
+
+
+
+
+
+
+ # +
+

handles subtract-from

+
+
+
    def _subtract_layers(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        for layer in self.layers:
+            if layer.options['subtract-from'] is not False:
+                for feat in layer.features:
+                    if feat.geom is None:
+                        continue
+                    cbbox = geom_to_bbox(feat.geom)
+                    for subid in layer.options['subtract-from']:
+                        if subid not in self.layers:
+                            raise KartographError('you want to subtract from layer "%s" which cannot be found' % subid)
+                        for sfeat in self.layersById[subid].features:
+                            if sfeat.geom and geom_to_bbox(sfeat.geom).intersects(cbbox):
+                                sfeat.subtract_geom(feat.geom)
+                layer.features = []
+
+
+
+
+
+
+ # +
+

joins features in layers

+
+
+
    def _join_layers(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        from geometry.utils import join_features
+
+        for layer in self.layers:
+            if layer.options['join'] is not False:
+                unjoined = 0
+                join = layer.options['join']
+                groupBy = join['group-by']
+                groups = join['groups']
+                if not groups:
+
+
+
+
+
+
+ # +
+

auto populate groups

+
+
+
                    groups = {}
+                    for feat in layer.features:
+                        fid = feat.props[groupBy]
+                        groups[fid] = [fid]
+
+                groupAs = join['group-as']
+                groupFeatures = {}
+                res = []
+                for feat in layer.features:
+                    found_in_group = False
+                    for g_id in groups:
+                        if g_id not in groupFeatures:
+                            groupFeatures[g_id] = []
+                        if feat.props[groupBy] in groups[g_id] or str(feat.props[groupBy]) in groups[g_id]:
+                            groupFeatures[g_id].append(feat)
+                            found_in_group = True
+                            break
+                    if not found_in_group:
+                        unjoined += 1
+                        res.append(feat)
+
+
+
+
+
+
+ # +
+

print unjoined,'features were not joined'

+
+
+
                for g_id in groups:
+                    props = {}
+                    for feat in groupFeatures[g_id]:
+                        fprops = feat.props
+                        for key in fprops:
+                            if key not in props:
+                                props[key] = fprops[key]
+                            else:
+                                if props[key] != fprops[key]:
+                                    props[key] = "---"
+
+                    if groupAs is not False:
+                        props[groupAs] = g_id
+                    if g_id in groupFeatures:
+                        res += join_features(groupFeatures[g_id], props)
+                layer.features = res
+
+
+
+
+
+
+ diff --git a/source/maplayer.html b/source/maplayer.html new file mode 100644 index 0000000..97448be --- /dev/null +++ b/source/maplayer.html @@ -0,0 +1,329 @@ + + + + + maplayer.py + + + +
+
+
+

maplayer.py

+
+
+
+
+
+ # +
+ +
+
+
from layersource import handle_layer_source
+from filter import filter_record
+
+
+_verbose = False
+
+
+
+
+
+
+ # +
+

MapLayer

+

Represents a layer in the map which contains a list of map features

+
+
+
class MapLayer(object):
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, id, options, _map, cache):
+
+
+
+
+
+
+ # +
+

Store layer properties as instance properties

+
+
+
        self.id = id
+        self.options = options
+        self.map = _map
+        self.cache = cache
+
+
+
+
+
+
+ # +
+

Make sure that the layer id is unique within the map.

+
+
+
        while self.id in self.map.layersById:
+            self.id += "_"
+
+
+
+
+
+
+ # +
+

Instantiate the layer source which will generate features from the source +geo data such as shapefiles or virtual sources such as graticule lines.

+
+
+
        self.source = handle_layer_source(self.options, self.cache)
+
+
+
+
+
+
+ # +
+

get_features()

+

Returns a list of projected and filtered features of a layer.

+
+
+
    def get_features(layer, filter=False, min_area=0):
+
+
+
+
+
+
+ # +
+ +
+
+
        opts = layer.map.options
+        is_projected = False
+
+
+
+
+
+
+ # +
+

Let's see if theres a better bounding box than this..

+
+
+
        bbox = [-180, -90, 180, 90]
+
+
+
+
+
+
+ # +
+

Use the clipping mode defined in the map configuration

+
+
+
        if opts['bounds']['mode'] == "bbox":
+            bbox = opts['bounds']['data']
+
+
+
+
+
+
+ # +
+

The 'crop' property overrides the clipping settings

+
+
+
        if 'crop' in opts['bounds'] and opts['bounds']['crop']:
+
+
+
+
+
+
+ # +
+

If crop is set to "auto", which is the default behaviour, Kartograph +will use the actual bounding geometry to compute the bounding box

+
+
+
            if opts['bounds']['crop'] == "auto":
+                if layer.map._unprojected_bounds:
+                    bbox = layer.map._unprojected_bounds
+                    bbox.inflate(inflate=opts['bounds']['padding'] * 2)
+                else:
+                    print 'could not compute bounding box for auto-cropping'
+            else:
+
+
+
+
+
+
+ # +
+

otherwise it will use the user defined bbox in the format +[minLon, minLat, maxLon, maxLat]

+
+
+
                bbox = opts['bounds']['crop']
+
+
+
+
+
+
+ # +
+

If the layer has the "src" property, it is a regular map layer source, which +means that there's an exernal file that we load the geometry and meta data from.

+
+
+
        if 'src' in layer.options:
+            if layer.options['filter'] is False:
+                filter = None
+            else:
+                filter = lambda rec: filter_record(layer.options['filter'], rec)
+
+
+
+
+
+
+ # +
+

Now we ask the layer source to generate the features that will be displayed +in the map.

+
+
+
            features = layer.source.get_features(
+                filter=filter,
+                bbox=bbox,
+                ignore_holes='ignore-holes' in layer.options and layer.options['ignore-holes'],
+                charset=layer.options['charset']
+            )
+            if _verbose:
+                print 'loaded %d features from shapefile %s' % (len(features), layer.options['src'])
+
+
+
+
+
+
+ # +
+

In contrast to regular layers, the geometry for special (or virtual) layers is generated +by Kartograph itself, based on some properties defined in the layer config.

+
+
+
        elif 'special' in layer.options:
+
+
+
+
+
+
+ # +
+

The graticule layer generates line features for longitudes and latitudes

+
+
+
            if layer.options['special'] == "graticule":
+                lats = layer.options['latitudes']
+                lons = layer.options['longitudes']
+                features = layer.source.get_features(lats, lons, layer.map.proj, bbox=bbox)
+
+
+
+
+
+
+ # +
+

The "sea" layer generates a MultiPolygon that represents the entire boundary +of the map. Especially useful for non-cylindrical map projections.

+
+
+
            elif layer.options['special'] == "sea":
+                features = layer.source.get_features(layer.map.proj)
+                is_projected = True
+
+        for feature in features:
+
+
+
+
+
+
+ # +
+

If the features are not projected yet, we project them now.

+
+
+
            if not is_projected:
+                feature.project(layer.map.proj)
+
+
+
+
+
+
+ # +
+

Transform features to view coordinates.

+
+
+
            feature.project_view(layer.map.view)
+
+
+
+
+
+
+ # +
+

Remove features that don't intersect our clipping polygon

+
+
+
        if layer.map.view_poly:
+            features = [feature for feature in features
+            if feature.geometry and feature.geometry.intersects(layer.map.view_poly)]
+        layer.features = features
+
+
+
+
+
+
+ diff --git a/source/options.html b/source/options.html new file mode 100644 index 0000000..3ebc799 --- /dev/null +++ b/source/options.html @@ -0,0 +1,499 @@ + + + + + options.py + + + +
+
+
+

options.py

+
+
+
+
+
+ # +
+

API 2.0 +helper methods for validating options dictionary

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
import os.path, proj, errors
+
+
+Error = errors.KartographOptionParseError
+
+
+
+
+
+
+ # +
+ +
+
+
def is_str(s):
+    return isinstance(s, (str, unicode))
+
+
+
+
+
+
+ # +
+

check out that the option dict is filled correctly

+
+
+
def parse_options(opts):
+
+
+
+
+
+
+ # +
+

projection

+
+
+
    parse_proj(opts)
+    parse_layers(opts)
+    parse_bounds(opts)
+    parse_export(opts)
+
+
+
+
+
+
+ # +
+

checks projections

+
+
+
def parse_proj(opts):
+
+
+
+
+
+
+ # +
+ +
+
+
    if 'proj' not in opts:
+        opts['proj'] = {}
+    prj = opts['proj']
+    if 'id' not in prj:
+        if 'bounds' not in opts:
+            prj['id'] = 'robinson'
+        else:
+            prj['id'] = 'laea'
+    if prj['id'] not in proj.projections:
+        raise Error('unknown projection')
+    prjClass = proj.projections[prj['id']]
+    for attr in prjClass.attributes():
+        if attr not in prj:
+            prj[attr] = "auto"
+        else:
+            if prj[attr] != "auto":
+                prj[attr] = float(prj[attr])
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_layers(opts):
+    if 'layers' not in opts:
+        opts['layers'] = []
+    l_id = 0
+    g_id = 0
+    s_id = 0
+    for layer in opts['layers']:
+        if 'styles' not in layer:
+            layer['styles'] = {}
+        if 'src' not in layer and 'special' not in layer:
+            raise Error('you need to define the source for your layers')
+        if 'src' in layer:
+            if not os.path.exists(layer['src']):
+                raise Error('layer source not found: ' + layer['src'])
+            if 'id' not in layer:
+                layer['id'] = 'layer_' + str(l_id)
+                l_id += 1
+            if 'charset' not in layer:
+                layer['charset'] = 'utf-8'
+        elif 'special' in layer:
+            if layer['special'] == 'graticule':
+                if 'id' not in layer:
+                    layer['id'] = 'graticule'
+                    if g_id > 0:
+                        layer['id'] += '_' + str(g_id)
+                    g_id += 1
+                if 'fill' not in layer['styles']:
+                    layer['styles']['fill'] = 'None'
+                parse_layer_graticule(layer)
+            elif layer['special'] == 'sea':
+                if 'id' not in layer:
+                    layer['id'] = 'sea'
+                    if s_id > 0:
+                        layer['id'] += '_' + str(s_id)
+                    s_id += 1
+
+        parse_layer_attributes(layer)
+        parse_layer_filter(layer)
+        parse_layer_join(layer)
+        parse_layer_simplify(layer)
+        parse_layer_subtract(layer)
+        parse_layer_cropping(layer)
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_layer_attributes(layer):
+    if 'attributes' not in layer:
+        layer['attributes'] = []
+        return
+    attrs = []
+    for attr in layer['attributes']:
+        if is_str(attr):
+            if isinstance(layer['attributes'], list):  # ["ISO_A3", "FIPS"]
+                attrs.append({'src': attr, 'tgt': attr})
+            elif isinstance(layer['attributes'], dict):  # { "ISO_A3": "iso" }
+                attrs.append({'src': attr, 'tgt': layer['attributes'][attr]})
+        elif isinstance(attr, dict) and 'src' in attr and 'tgt' in attr:
+            attrs.append(attr)
+    layer['attributes'] = attrs
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_layer_filter(layer):
+    if 'filter' not in layer:
+        layer['filter'] = False
+        return
+    return  # todo: check valid filter syntax (recursivly, place code in filter.py)
+    filter = layer['filter']
+    if 'type' not in filter:
+        filter['type'] = 'include'
+    if 'attribute' not in filter:
+        raise Error('layer filter must define an attribute to filter on')
+    if 'equals' in filter:
+        if isinstance(filter['equals'], (str, unicode, int, float)):
+            filter['equals'] = [filter['equals']]
+    elif 'greater-than' in filter:
+        try:
+            filter['greater-than'] = float(filter['greater-than'])
+        except ValueError:
+            raise Error('could not convert filter value "greater-than" to float')
+    elif 'less-than' in filter:
+        try:
+            filter['less-than'] = float(filter['less-than'])
+        except ValueError:
+            raise Error('could not convert filter value "less-than" to float')
+    else:
+        raise Error('you must define either "equals", "greater-than" or "less-than" in the filter')
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_layer_join(layer):
+    if 'join' not in layer:
+        layer['join'] = False
+        return
+    if layer['join'] is False:
+        return
+
+    join = layer['join']
+    if 'group-by' not in join:
+        raise Error('missing attribute "group-by": you need to specify an attribute by which the features should be joined.')
+    if 'groups' not in join:
+        join['groups'] = None
+    if 'group-as' not in join:
+        join['group-as'] = False
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_layer_simplify(layer):
+    if 'unify-precision' not in layer:
+        layer['unify-precision'] = None
+    if 'simplify' not in layer:
+        layer['simplify'] = False
+        return
+    if layer['simplify'] is False:
+        return
+    if isinstance(layer['simplify'], (int, float, str, unicode)):
+
+
+
+
+
+
+ # +
+

default to visvalingam-whyatt

+
+
+
        layer['simplify'] = {"method": "visvalingam-whyatt", "tolerance": float(layer['simplify'])}
+    try:
+        layer['simplify']['tolerance'] = float(layer['simplify']['tolerance'])
+    except ValueError:
+        raise Error('could not convert simplification amount to float')
+    if 'filter-islands' not in layer:
+        layer['filter-islands'] = False
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_layer_subtract(layer):
+    if 'subtract-from' not in layer:
+        layer['subtract-from'] = False
+        return
+    if isinstance(layer['subtract-from'], (str, unicode)):
+        layer['subtract-from'] = [layer['subtract-from']]
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_layer_cropping(layer):
+    if 'crop-to' not in layer:
+        layer['crop-to'] = False
+        return
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_layer_graticule(layer):
+    if 'latitudes' not in layer:
+        layer['latitudes'] = []
+    elif isinstance(layer['latitudes'], (int, float)):
+        step = layer['latitudes']
+        layer['latitudes'] = [0]
+        for lat in _xfrange(step, 90, step):
+            layer['latitudes'] += [lat, -lat]
+
+    if 'longitudes' not in layer:
+        layer['longitudes'] = []
+    elif isinstance(layer['longitudes'], (int, float)):
+        step = layer['longitudes']
+        layer['longitudes'] = [0]
+        for lon in _xfrange(step, 181, step):
+            if lon == 180:
+                p = [lon]
+            else:
+                p = [lon, -lon]
+            layer['longitudes'] += p
+
+
+
+
+
+
+ # +
+ +
+
+
def _xfrange(start, stop, step):
+    while (step > 0 and start < stop) or (step < 0 and start > step):
+        yield start
+        start += step
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_bounds(opts):
+    if 'bounds' not in opts:
+        opts['bounds'] = {}
+
+
+
+
+
+
+ # +
+

return

+
+
+
    bounds = opts['bounds']
+    if 'mode' not in bounds:
+        bounds['mode'] = 'bbox'
+
+    if 'data' not in bounds:
+        bounds['data'] = [-180, -90, 180, 90]
+        bounds['mode'] = 'bbox'
+
+    mode = bounds['mode']
+    data = bounds['data']
+
+    if 'crop' not in bounds:
+        bounds['crop'] = 'auto'
+
+    if "padding" not in bounds:
+        bounds["padding"] = 0
+
+    if mode == "bbox":
+        try:
+            if len(data) == 4:
+                for i in range(0, 4):
+                    data[i] = float(data[i])
+            else:
+                raise Error('bounds mode bbox requires array with exactly 4 values [lon0,lat0,lon1,lat]')
+        except Error:
+            raise
+        except:
+            raise Error('bounds mode bbox requires array with exactly 4 values [lon0,lat0,lon1,lat]')
+    elif mode == "points":
+        try:
+            for i in range(0, len(data)):
+                pt = data[i]
+                if len(pt) == 2:
+                    pt = map(float, pt)
+                else:
+                    raise Error('bounds mode points requires array with (lon,lat) tuples')
+        except Error:
+            raise
+        except:
+            raise Error('bounds mode points requires array with (lon,lat) tuples')
+    elif mode in ("polygons", "polygon"):
+        bounds['mode'] = mode = "polygons"
+        if "layer" not in data or not is_str(data["layer"]):
+            raise Error('you must specify a layer for bounds mode ' + mode)
+        if "filter" not in data:
+            data["filter"] = False
+        if "attribute" not in data or not is_str(data["attribute"]):
+            data["attribute"] = None
+        if "values" not in data:
+            if data["attribute"] is None:
+                data["values"] = None
+            else:
+                raise Error('you must specify a list of values to match in bounds mode ' + mode)
+        if is_str(data["values"]):
+            data["values"] = [data["values"]]
+        if "min-area" in data:
+            try:
+                data["min-area"] = float(data["min-area"])
+            except:
+                raise Error('min_area must be an integer or float')
+        else:
+            data['min-area'] = 0
+
+
+
+
+
+
+ # +
+ +
+
+
def parse_export(opts):
+    if "export" not in opts:
+        opts["export"] = {}
+    exp = opts["export"]
+    if "width" not in exp and "height" not in exp:
+        exp["width"] = 1000
+        exp["height"] = "auto"
+    elif "height" not in exp:
+        exp["height"] = "auto"
+    elif "width" not in exp:
+        exp["width"] = "auto"
+
+    if "ratio" not in exp:
+        exp["ratio"] = "auto"
+    if "round" not in exp:
+        exp["round"] = False
+    else:
+        exp["round"] = int(exp["round"])
+    if "crop-to-view" not in exp:
+        exp['crop-to-view'] = True
+
+
+
+
+
+
+ diff --git a/source/proj/__init__.html b/source/proj/__init__.html new file mode 100644 index 0000000..46c09b2 --- /dev/null +++ b/source/proj/__init__.html @@ -0,0 +1,219 @@ + + + + + __init__.py + + + +
+
+
+

__init__.py

+
+
+
+
+
+ # +
+
kartograph - a svg mapping library
+Copyright (C) 2011  Gregor Aisch
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+
+
projections = dict()
+
+from base import Proj
+from cylindrical import *
+
+projections['lonlat'] = Equirectangular
+projections['cea'] = CEA
+projections['gallpeters'] = GallPeters
+projections['hobodyer'] = HoboDyer
+projections['behrmann'] = Behrmann
+projections['balthasart'] = Balthasart
+projections['mercator'] = Mercator
+projections['ll'] = LonLat
+
+from pseudocylindrical import *
+
+projections['naturalearth'] = NaturalEarth
+projections['robinson'] = Robinson
+projections['eckert4'] = EckertIV
+projections['sinusoidal'] = Sinusoidal
+projections['mollweide'] = Mollweide
+projections['wagner4'] = WagnerIV
+projections['wagner5'] = WagnerV
+projections['loximuthal'] = Loximuthal
+projections['canters1'] = CantersModifiedSinusoidalI
+projections['goodehomolosine'] = GoodeHomolosine
+projections['hatano'] = Hatano
+projections['aitoff'] = Aitoff
+projections['winkel3'] = Winkel3
+projections['nicolosi'] = Nicolosi
+
+from azimuthal import *
+
+projections['ortho'] = Orthographic
+projections['laea'] = LAEA
+projections['stereo'] = Stereographic
+projections['satellite'] = Satellite
+projections['eda'] = EquidistantAzimuthal
+projections['aitoff'] = Aitoff
+
+from conic import *
+
+projections['lcc'] = LCC
+
+
+
+
+
+
+ # +
+ +
+
+
class Proj4(Proj):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, projstr):
+        import pyproj
+        self.proj = pyproj.Proj(projstr)
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        x, y = self.proj(lon, lat)
+        return (x, y * -1)
+
+
+
+
+
+
+ # +
+ +
+
+
class LCC__(Proj4):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lat0=0, lon0=0, lat1=28, lat2=30):
+        Proj4.__init__(self, '+proj=lcc +lat_0=%f +lon_0=%f +lat_1=%f +lat_2=%f' % (lat0, lon0, lat1, lat2))
+
+
+
+
+
+
+ # +
+ +
+
+
    def _visible(self, lon, lat):
+        return True
+
+
+
+
+
+
+ # +
+ +
+
+
    def _truncate(self, x, y):
+        return (x, y)
+
+
+for pjname in projections:
+    projections[pjname].name = pjname
+
+
+if __name__ == '__main__':
+    import sys
+
+
+
+
+
+
+ # +
+

some class testing +p = LAEA(52.0,10.0) +x,y = p.project(50,5) +assert (round(x,2),round(y,2)) == (3962799.45, -2999718.85), 'LAEA proj error'

+
+
+
    from kartograph.geometry import BBox
+
+    print Proj.fromXML(Robinson(lat0=3, lon0=4).toXML(), projections)
+
+    Robinson(lat0=3, lon0=4)
+
+    for pj in projections:
+        Proj = projections[pj]
+        bbox = BBox()
+        try:
+            proj = Proj(lon0=60)
+            print proj.project(0, 0)
+            print proj.world_bounds(bbox)
+            print proj.toXML()
+        except:
+            print 'Error', pj
+            print sys.exc_info()[0]
+            raise
+
+
+
+
+
+
+ diff --git a/source/proj/base.html b/source/proj/base.html new file mode 100644 index 0000000..85b7b16 --- /dev/null +++ b/source/proj/base.html @@ -0,0 +1,432 @@ + + + + + base.py + + + +
+
+
+

base.py

+
+
+
+
+
+ # +
+
kartograph - a svg mapping library
+Copyright (C) 2011  Gregor Aisch
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+
+
import math
+from kartograph.errors import KartographError
+from shapely.geometry import Polygon, LineString, Point, MultiPolygon, MultiLineString, MultiPoint
+
+
+
+
+
+
+ # +
+

base class for projections

+
+
+
class Proj(object):
+
+
+
+
+
+
+ # +
+ +
+
+
    HALFPI = math.pi * .5
+    QUARTERPI = math.pi * .25
+
+    minLat = -90
+    maxLat = 90
+    minLon = -180
+    maxLon = 180
+
+
+
+
+
+
+ # +
+ +
+
+
    def _shift_polygon(self, polygon):
+        return [polygon]  # no shifting
+
+
+
+
+
+
+ # +
+ +
+
+
    def plot(self, geometry):
+        geometries = hasattr(geometry, 'geoms') and geometry.geoms or [geometry]
+        res = []
+
+
+
+
+
+
+ # +
+

at first shift polygons +shifted = [] +for geom in geometries: + if isinstance(geom, Polygon): + shifted += self._shift_polygon(geom) + else: + shifted += [geom]

+
+
+
        for geom in geometries:
+            if isinstance(geom, Polygon):
+                res += self.plot_polygon(geom)
+            elif isinstance(geom, LineString):
+                rings = self.plot_linear_ring(geom.coords)
+                res += map(LineString, rings)
+            elif isinstance(geom, Point):
+                if self._visible(geom.x, geom.y):
+                    x, y = self.project(geom.x, geom.y)
+                    res.append(Point(x, y))
+            else:
+                pass
+
+
+
+
+
+
+ # +
+

raise KartographError('proj.plot(): unknown geometry type %s' % geom)

+
+
+
        if len(res) > 0:
+            if isinstance(res[0], Polygon):
+                if len(res) > 1:
+                    return MultiPolygon(res)
+                else:
+                    return res[0]
+            elif isinstance(res[0], LineString):
+                if len(res) > 1:
+                    return MultiLineString(res)
+                else:
+                    return LineString(res[0])
+            else:
+                if len(res) > 1:
+                    return MultiPoint(res)
+                else:
+                    return Point(res[0][0], res[0][1])
+
+
+
+
+
+
+ # +
+ +
+
+
    def plot_polygon(self, polygon):
+        ext = self.plot_linear_ring(polygon.exterior, truncate=True)
+        if len(ext) == 1:
+            pts_int = []
+            for interior in polygon.interiors:
+                pts_int += self.plot_linear_ring(interior, truncate=True)
+            return [Polygon(ext[0], pts_int)]
+        elif len(ext) == 0:
+            return []
+        else:
+            raise KartographError('unhandled case: exterior is split into multiple rings')
+
+
+
+
+
+
+ # +
+ +
+
+
    def plot_linear_ring(self, ring, truncate=False):
+        ignore = True
+        points = []
+        for (lon, lat) in ring.coords:
+            vis = self._visible(lon, lat)
+            if vis:
+                ignore = False
+            x, y = self.project(lon, lat)
+            if not vis and truncate:
+                points.append(self._truncate(x, y))
+            else:
+                points.append((x, y))
+        if ignore:
+            return []
+        return [points]
+
+
+
+
+
+
+ # +
+ +
+
+
    def ll(self, lon, lat):
+        return (lon, lat)
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        assert False, 'Proj is an abstract class'
+
+
+
+
+
+
+ # +
+ +
+
+
    def project_inverse(self, x, y):
+        assert False, 'inverse projection is not supporte by %s' % self.name
+
+
+
+
+
+
+ # +
+ +
+
+
    def _visible(self, lon, lat):
+        assert False, 'Proj is an abstract class'
+
+
+
+
+
+
+ # +
+ +
+
+
    def _truncate(self, x, y):
+        assert False, 'truncation is not implemented'
+
+
+
+
+
+
+ # +
+ +
+
+
    def world_bounds(self, bbox, llbbox=(-180, -90, 180, 90)):
+        sea = self.sea_shape(llbbox)
+        for x, y in sea[0]:
+            bbox.update((x, y))
+        return bbox
+
+
+
+
+
+
+ # +
+

returns a WGS84 polygon that represents the limits of this projection +points that lie outside this polygon will not be plotted +this polygon will also be used to render the sea layer in world maps

+

defaults to full WGS84 range

+
+
+
    def bounding_geometry(self, llbbox=(-180, -90, 180, 90), projected=False):
+
+
+
+
+
+
+ # +
+ +
+
+
        from shapely.geometry import Polygon
+        sea = []
+
+        minLon = llbbox[0]
+        maxLon = llbbox[2]
+        minLat = max(self.minLat, llbbox[1])
+        maxLat = min(self.maxLat, llbbox[3])
+
+
+
+
+
+
+ # +
+ +
+
+
        def xfrange(start, stop, step):
+            if stop > start:
+                while start < stop:
+                    yield start
+                    start += step
+            else:
+                while stop < start:
+                    yield start
+                    start -= step
+
+        lat_step = abs((maxLat - minLat) / 180.0)
+        lon_step = abs((maxLon - minLon) / 360.0)
+
+        for lat in xfrange(minLat, maxLat, lat_step):
+            sea.append((minLon, lat))
+        for lon in xfrange(minLon, maxLon, lon_step):
+            sea.append((lon, maxLat))
+        for lat in xfrange(maxLat, minLat, lat_step):
+            sea.append((maxLon, lat))
+        for lon in xfrange(maxLon, minLon, lon_step):
+            sea.append((lon, minLat))
+
+        if projected:
+            sea = [self.project(lon, lat) for (lon, lat) in sea]
+
+        return Polygon(sea)
+
+
+
+
+
+
+ # +
+ +
+
+
    def __str__(self):
+        return 'Proj(' + self.name + ')'
+
+
+
+
+
+
+ # +
+ +
+
+
    def attrs(self):
+        return dict(id=self.name)
+
+
+
+
+
+
+ # +
+

returns array of attribute names of this projection

+
+
+
    @staticmethod
+    def attributes():
+
+
+
+
+
+
+ # +
+ +
+
+
        return []
+
+
+
+
+
+
+ # +
+ +
+
+
    @staticmethod
+    def fromXML(xml, projections):
+        id = xml['id']
+        if id in projections:
+            ProjClass = projections[id]
+            args = {}
+            for (prop, val) in xml:
+                if prop[0] != "id":
+                    args[prop[0]] = float(val)
+            return ProjClass(**args)
+        raise Exception("could not restore projection from xml")
+
+
+
+
+
+
+ diff --git a/source/proj/conic.html b/source/proj/conic.html new file mode 100644 index 0000000..e2de70e --- /dev/null +++ b/source/proj/conic.html @@ -0,0 +1,320 @@ + + + + + conic.py + + + +
+
+
+

conic.py

+
+
+
+
+
+ # +
+
kartograph - a svg mapping library
+Copyright (C) 2011  Gregor Aisch
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+
+
from base import Proj
+import math
+from math import radians as rad
+
+
+
+
+
+
+ # +
+ +
+
+
class Conic(Proj):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lat0=0, lon0=0, lat1=0, lat2=0):
+        self.lat0 = lat0
+        self.phi0 = rad(lat0)
+        self.lon0 = lon0
+        self.lam0 = rad(lon0)
+        self.lat1 = lat1
+        self.phi1 = rad(lat1)
+        self.lat2 = lat2
+        self.phi2 = rad(lat2)
+
+        if lon0 != 0.0:
+            self.bounds = self.bounding_geometry()
+
+
+
+
+
+
+ # +
+ +
+
+
    def _visible(self, lon, lat):
+        return True
+
+
+
+
+
+
+ # +
+ +
+
+
    def _truncate(self, x, y):
+        return (x, y)
+
+
+
+
+
+
+ # +
+ +
+
+
    def attrs(self):
+        p = super(Conic, self).attrs()
+        p['lon0'] = self.lon0
+        p['lat0'] = self.lat0
+        p['lat1'] = self.lat1
+        p['lat2'] = self.lat2
+        return p
+
+
+
+
+
+
+ # +
+

shifts a polygon according to the origin longitude

+
+
+
    def _shift_polygon(self, polygon):
+
+
+
+
+
+
+ # +
+ +
+
+
        if self.lon0 == 0.0:
+            return [polygon]  # no need to shift anything
+
+        from shapely.geometry import Polygon
+
+
+
+
+
+
+ # +
+

we need to split and join some polygons

+
+
+
        poly_coords = []
+        holes = []
+        for (lon, lat) in polygon.exterior.coords:
+            poly_coords.append((lon - self.lon0, lat))
+        for hole in polygon.interiors:
+            hole_coords = []
+            for (lon, lat) in hole.coords:
+                hole_coords.append((lon - self.lon0, lat))
+            holes.append(hole_coords)
+        poly = Polygon(poly_coords, holes)
+
+        polygons = []
+
+
+
+
+
+
+ # +
+

print "shifted polygons", (time.time() - start) +start = time.time()

+
+
+
        try:
+            p_in = poly.intersection(self.bounds)
+            polygons += hasattr(p_in, 'geoms') and p_in.geoms or [p_in]
+        except:
+            pass
+
+
+
+
+
+
+ # +
+

print "computed polygons inside bounds", (time.time() - start) +start = time.time()

+
+
+
        try:
+            p_out = poly.symmetric_difference(self.bounds)
+            out_geoms = hasattr(p_out, 'geoms') and p_out.geoms or [p_out]
+        except:
+            out_geoms = []
+            pass
+
+
+
+
+
+
+ # +
+

print "computed polygons outside bounds", (time.time() - start) +start = time.time()

+
+
+
        for polygon in out_geoms:
+            ext_pts = []
+            int_pts = []
+            s = 0  # at first we compute the avg longitude
+            c = 0
+            for (lon, lat) in polygon.exterior.coords:
+                s += lon
+                c += 1
+            left = s / float(c) < -180  # and use it to decide where to shift the polygon
+            for (lon, lat) in polygon.exterior.coords:
+                ext_pts.append((lon + (-360, 360)[left], lat))
+            for interior in polygon.interiors:
+                pts = []
+                for (lon, lat) in interior.coords:
+                    pts.append((lon + (-360, 360)[left], lat))
+                int_pts.append(pts)
+            polygons.append(Polygon(ext_pts, int_pts))
+
+
+
+
+
+
+ # +
+

print "shifted outside polygons to inside", (time.time() - start)

+
+
+
        return polygons
+
+
+
+
+
+
+ # +
+

Lambert Conformal Conic Projection (spherical)

+
+
+
    @staticmethod
+    def attributes():
+        return ['lon0', 'lat0', 'lat1', 'lat2']
+
+
+class LCC(Conic):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lat0=0, lon0=0, lat1=30, lat2=50):
+        from math import sin, cos, tan, pow, log
+        self.minLat = -60
+        self.maxLat = 85
+        Conic.__init__(self, lat0=lat0, lon0=lon0, lat1=lat1, lat2=lat2)
+        self.n = n = sin(self.phi1)
+        cosphi = cos(self.phi1)
+        secant = abs(self.phi1 - self.phi2) >= 1e-10
+        if secant:
+            n = log(cosphi / cos(self.phi2)) / log(tan(self.QUARTERPI + .5 * self.phi2) / tan(self.QUARTERPI + .5 * self.phi1))
+        self.c = c = cosphi * pow(tan(self.QUARTERPI + .5 * self.phi1), n) / n
+        if abs(abs(self.phi0) - self.HALFPI) < 1e-10:
+            self.rho0 = 0.
+        else:
+            self.rho0 = c * pow(tan(self.QUARTERPI + .5 * self.phi0), -n)
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        phi = rad(lat)
+        lam = rad(lon)
+        n = self.n
+        if abs(abs(phi) - self.HALFPI) < 1e-10:
+            rho = 0.0
+        else:
+            rho = self.c * math.pow(math.tan(self.QUARTERPI + 0.5 * phi), -n)
+        lam_ = (lam - self.lam0) * n
+        x = 1000 * rho * math.sin(lam_)
+        y = 1000 * (self.rho0 - rho * math.cos(lam_))
+
+        return (x, y * -1)
+
+
+
+
+
+
+ diff --git a/source/proj/cylindrical.html b/source/proj/cylindrical.html new file mode 100644 index 0000000..7e77b6c --- /dev/null +++ b/source/proj/cylindrical.html @@ -0,0 +1,426 @@ + + + + + cylindrical.py + + + +
+
+
+

cylindrical.py

+
+
+
+
+
+ # +
+
kartograph - a svg mapping library
+Copyright (C) 2011  Gregor Aisch
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+
+
from base import Proj
+import math
+from math import radians as rad
+
+
+
+
+
+
+ # +
+ +
+
+
class Cylindrical(Proj):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0.0, flip=0):
+        self.flip = flip
+        self.lon0 = lon0
+        self.bounds = self.bounding_geometry()
+
+
+
+
+
+
+ # +
+

shifts a polygon according to the origin longitude

+
+
+
    def _shift_polygon(self, polygon):
+
+
+
+
+
+
+ # +
+ +
+
+
        if self.lon0 == 0.0:
+            return [polygon]  # no need to shift anything
+
+        from shapely.geometry import Polygon
+
+
+
+
+
+
+ # +
+

we need to split and join some polygons

+
+
+
        poly_coords = []
+        holes = []
+        for (lon, lat) in polygon.exterior.coords:
+            poly_coords.append((lon - self.lon0, lat))
+        for hole in polygon.interiors:
+            hole_coords = []
+            for (lon, lat) in hole.coords:
+                hole_coords.append((lon - self.lon0, lat))
+            holes.append(hole_coords)
+        poly = Polygon(poly_coords, holes)
+
+        polygons = []
+
+
+
+
+
+
+ # +
+

print "shifted polygons", (time.time() - start) +start = time.time()

+
+
+
        try:
+            p_in = poly.intersection(self.bounds)
+            polygons += hasattr(p_in, 'geoms') and p_in.geoms or [p_in]
+        except:
+            pass
+
+
+
+
+
+
+ # +
+

print "computed polygons inside bounds", (time.time() - start) +start = time.time()

+
+
+
        try:
+            p_out = poly.symmetric_difference(self.bounds)
+            out_geoms = hasattr(p_out, 'geoms') and p_out.geoms or [p_out]
+        except:
+            out_geoms = []
+            pass
+
+
+
+
+
+
+ # +
+

print "computed polygons outside bounds", (time.time() - start) +start = time.time()

+
+
+
        for polygon in out_geoms:
+            ext_pts = []
+            int_pts = []
+            s = 0  # at first we compute the avg longitude
+            c = 0
+            for (lon, lat) in polygon.exterior.coords:
+                s += lon
+                c += 1
+            left = s / float(c) < -180  # and use it to decide where to shift the polygon
+            for (lon, lat) in polygon.exterior.coords:
+                ext_pts.append((lon + (-360, 360)[left], lat))
+            for interior in polygon.interiors:
+                pts = []
+                for (lon, lat) in interior.coords:
+                    pts.append((lon + (-360, 360)[left], lat))
+                int_pts.append(pts)
+            polygons.append(Polygon(ext_pts, int_pts))
+
+
+
+
+
+
+ # +
+

print "shifted outside polygons to inside", (time.time() - start)

+
+
+
        return polygons
+
+
+
+
+
+
+ # +
+ +
+
+
    def _visible(self, lon, lat):
+        return True
+
+
+
+
+
+
+ # +
+ +
+
+
    def _truncate(self, x, y):
+        return (x, y)
+
+
+
+
+
+
+ # +
+ +
+
+
    def attrs(self):
+        a = super(Cylindrical, self).attrs()
+        a['lon0'] = self.lon0
+        a['flip'] = self.flip
+        return a
+
+
+
+
+
+
+ # +
+ +
+
+
    def __str__(self):
+        return 'Proj(' + self.name + ', lon0=%s)' % self.lon0
+
+
+
+
+
+
+ # +
+

Equirectangular Projection, aka lonlat, aka plate carree

+
+
+
    @staticmethod
+    def attributes():
+        return ['lon0', 'flip']
+
+    def ll(self, lon, lat):
+        if self.flip == 1:
+            return (-lon, -lat)
+        return (lon, lat)
+
+
+class Equirectangular(Cylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0.0, lat0=0.0, flip=0):
+        self.lat0 = lat0
+        self.phi0 = rad(lat0 * -1)
+        Cylindrical.__init__(self, lon0=lon0, flip=flip)
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        return (lon * math.cos(self.phi0) * 1000, lat * -1 * 1000)
+
+
+
+
+
+
+ # +
+

Cylindrical Equal Area Projection

+
+
+
class CEA(Cylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lat0=0.0, lon0=0.0, lat1=0.0, flip=0):
+        self.lat0 = lat0
+        self.lat1 = lat1
+        self.phi0 = rad(lat0 * -1)
+        self.phi1 = rad(lat1 * -1)
+        self.lam0 = rad(lon0)
+        Cylindrical.__init__(self, lon0=lon0, flip=flip)
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        lam = rad(lon)
+        phi = rad(lat * -1)
+        x = (lam) * math.cos(self.phi1) * 1000
+        y = math.sin(phi) / math.cos(self.phi1) * 1000
+        return (x, y)
+
+
+
+
+
+
+ # +
+ +
+
+
    def attrs(self):
+        p = super(CEA, self).attrs()
+        p['lat1'] = self.lat1
+        return p
+
+
+
+
+
+
+ # +
+ +
+
+
    @staticmethod
+    def attributes():
+        return ['lon0', 'lat1', 'flip']
+
+    def __str__(self):
+        return 'Proj(' + self.name + ', lon0=%s, lat1=%s)' % (self.lon0, self.lat1)
+
+
+class GallPeters(CEA):
+    def __init__(self, lat0=0.0, lon0=0.0, flip=0):
+        CEA.__init__(self, lon0=lon0, lat0=0, lat1=45, flip=flip)
+
+
+class HoboDyer(CEA):
+    def __init__(self, lat0=0.0, lon0=0.0, flip=0):
+        CEA.__init__(self, lon0=lon0, lat0=lat0, lat1=37.5, flip=flip)
+
+
+class Behrmann(CEA):
+    def __init__(self, lat0=0.0, lon0=0.0, flip=0):
+        CEA.__init__(self, lat1=30, lat0=lat0, lon0=lon0, flip=flip)
+
+
+class Balthasart(CEA):
+    def __init__(self, lat0=0.0, lon0=0.0, flip=0):
+        CEA.__init__(self, lat1=50, lat0=lat0, lon0=lon0, flip=flip)
+
+
+class Mercator(Cylindrical):
+    def __init__(self, lon0=0.0, lat0=0.0, flip=0):
+        Cylindrical.__init__(self, lon0=lon0, flip=flip)
+        self.minLat = -85
+        self.maxLat = 85
+
+    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        lam = rad(lon)
+        phi = rad(lat * -1)
+        x = lam * 1000
+        y = math.log((1 + math.sin(phi)) / math.cos(phi)) * 1000
+        return (x, y)
+
+
+class LonLat(Cylindrical):
+    def project(self, lon, lat):
+        return (lon, lat)
+
+
+
+
+
+
+ diff --git a/source/proj/pseudocylindrical.html b/source/proj/pseudocylindrical.html new file mode 100644 index 0000000..aa6cbda --- /dev/null +++ b/source/proj/pseudocylindrical.html @@ -0,0 +1,1014 @@ + + + + + pseudocylindrical.py + + + +
+
+
+

pseudocylindrical.py

+
+
+
+
+
+ # +
+
kartograph - a svg mapping library
+Copyright (C) 2011  Gregor Aisch
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+
+
from cylindrical import Cylindrical
+import math
+from math import radians as rad
+
+
+
+
+
+
+ # +
+ +
+
+
class PseudoCylindrical(Cylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0.0, flip=0):
+        Cylindrical.__init__(self, lon0=lon0, flip=flip)
+
+
+
+
+
+
+ # +
+

src: http://www.shadedrelief.com/NE_proj/

+
+
+
class NaturalEarth(PseudoCylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lat0=0.0, lon0=0.0, flip=0):
+        PseudoCylindrical.__init__(self, lon0=lon0, flip=flip)
+        from math import pi
+        s = self
+        s.A0 = 0.8707
+        s.A1 = -0.131979
+        s.A2 = -0.013791
+        s.A3 = 0.003971
+        s.A4 = -0.001529
+        s.B0 = 1.007226
+        s.B1 = 0.015085
+        s.B2 = -0.044475
+        s.B3 = 0.028874
+        s.B4 = -0.005916
+        s.C0 = s.B0
+        s.C1 = 3 * s.B1
+        s.C2 = 7 * s.B2
+        s.C3 = 9 * s.B3
+        s.C4 = 11 * s.B4
+        s.EPS = 1e-11
+        s.MAX_Y = 0.8707 * 0.52 * pi
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        lplam = rad(lon)
+        lpphi = rad(lat * -1)
+        phi2 = lpphi * lpphi
+        phi4 = phi2 * phi2
+        x = lplam * (self.A0 + phi2 * (self.A1 + phi2 * (self.A2 + phi4 * phi2 * (self.A3 + phi2 * self.A4)))) * 180 + 500
+        y = lpphi * (self.B0 + phi2 * (self.B1 + phi4 * (self.B2 + self.B3 * phi2 + self.B4 * phi4))) * 180 + 270
+        return (x, y)
+
+
+
+
+
+
+ # +
+ +
+
+
class Robinson(PseudoCylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lat0=0.0, lon0=0.0, flip=0):
+        PseudoCylindrical.__init__(self, lon0=lon0, flip=flip)
+        self.X = [1, -5.67239e-12, -7.15511e-05, 3.11028e-06,  0.9986, -0.000482241, -2.4897e-05, -1.33094e-06, 0.9954, -0.000831031, -4.4861e-05, -9.86588e-07, 0.99, -0.00135363, -5.96598e-05, 3.67749e-06, 0.9822, -0.00167442, -4.4975e-06, -5.72394e-06, 0.973, -0.00214869, -9.03565e-05, 1.88767e-08, 0.96, -0.00305084, -9.00732e-05, 1.64869e-06, 0.9427, -0.00382792, -6.53428e-05, -2.61493e-06, 0.9216, -0.00467747, -0.000104566, 4.8122e-06, 0.8962, -0.00536222, -3.23834e-05, -5.43445e-06, 0.8679, -0.00609364, -0.0001139, 3.32521e-06, 0.835, -0.00698325, -6.40219e-05, 9.34582e-07, 0.7986, -0.00755337, -5.00038e-05, 9.35532e-07, 0.7597, -0.00798325, -3.59716e-05, -2.27604e-06, 0.7186, -0.00851366, -7.0112e-05, -8.63072e-06, 0.6732, -0.00986209, -0.000199572, 1.91978e-05, 0.6213, -0.010418, 8.83948e-05, 6.24031e-06, 0.5722, -0.00906601, 0.000181999, 6.24033e-06, 0.5322,  0.,  0.,  0.]
+        self.Y = [0, 0.0124, 3.72529e-10, 1.15484e-09, 0.062, 0.0124001, 1.76951e-08, -5.92321e-09, 0.124, 0.0123998, -7.09668e-08, 2.25753e-08, 0.186, 0.0124008, 2.66917e-07, -8.44523e-08, 0.248, 0.0123971, -9.99682e-07, 3.15569e-07, 0.31, 0.0124108, 3.73349e-06, -1.1779e-06, 0.372, 0.0123598, -1.3935e-05, 4.39588e-06, 0.434, 0.0125501, 5.20034e-05, -1.00051e-05, 0.4968, 0.0123198, -9.80735e-05, 9.22397e-06, 0.5571, 0.0120308, 4.02857e-05, -5.2901e-06, 0.6176, 0.0120369, -3.90662e-05, 7.36117e-07, 0.6769, 0.0117015, -2.80246e-05, -8.54283e-07, 0.7346, 0.0113572, -4.08389e-05, -5.18524e-07, 0.7903, 0.0109099, -4.86169e-05, -1.0718e-06, 0.8435, 0.0103433, -6.46934e-05, 5.36384e-09, 0.8936, 0.00969679, -6.46129e-05, -8.54894e-06, 0.9394, 0.00840949, -0.000192847, -4.21023e-06, 0.9761, 0.00616525, -0.000256001, -4.21021e-06, 1.,  0.,  0.,  0]
+        self.NODES = 18
+        self.FXC = 0.8487
+        self.FYC = 1.3523
+        self.C1 = 11.45915590261646417544
+        self.RC1 = 0.08726646259971647884
+        self.ONEEPS = 1.000001
+        self.EPS = 1e-8
+
+
+
+
+
+
+ # +
+ +
+
+
    def _poly(self, arr, off, z):
+        return arr[off] + z * (arr[off + 1] + z * (arr[off + 2] + z * (arr[off + 3])))
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        lplam = rad(lon)
+        lpphi = rad(lat * -1)
+
+        phi = abs(lpphi)
+        i = int(phi * self.C1)
+        if i >= self.NODES:
+            i = self.NODES - 1
+        phi = math.degrees(phi - self.RC1 * i)
+        i *= 4
+        x = 1000 * self._poly(self.X, i, phi) * self.FXC * lplam
+        y = 1000 * self._poly(self.Y, i, phi) * self.FYC
+        if lpphi < 0.0:
+            y = -y
+
+        return (x, y)
+
+
+
+
+
+
+ # +
+ +
+
+
class EckertIV(PseudoCylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0.0, lat0=0, flip=0):
+        PseudoCylindrical.__init__(self, lon0=lon0, flip=flip)
+
+        self.C_x = .42223820031577120149
+        self.C_y = 1.32650042817700232218
+        self.RC_y = .75386330736002178205
+        self.C_p = 3.57079632679489661922
+        self.RC_p = .28004957675577868795
+        self.EPS = 1e-7
+        self.NITER = 6
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        lplam = rad(lon)
+        lpphi = rad(lat * -1)
+
+        p = self.C_p * math.sin(lpphi)
+        V = lpphi * lpphi
+        lpphi *= 0.895168 + V * (0.0218849 + V * 0.00826809)
+
+        i = self.NITER
+        while i > 0:
+            c = math.cos(lpphi)
+            s = math.sin(lpphi)
+            V = (lpphi + s * (c + 2.) - p) / (1. + c * (c + 2.) - s * s)
+            lpphi -= V
+            if abs(V) < self.EPS:
+                break
+            i -= 1
+
+        if i == 0:
+            x = self.C_x * lplam
+            y = (self.C_y, - self.C_y)[lpphi < 0]
+        else:
+            x = self.C_x * lplam * (1. + math.cos(lpphi))
+            y = self.C_y * math.sin(lpphi)
+        return (x, y)
+
+
+
+
+
+
+ # +
+ +
+
+
class Sinusoidal(PseudoCylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0.0, lat0=0.0, flip=0):
+        PseudoCylindrical.__init__(self, lon0=lon0, flip=flip)
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        lam = rad(lon)
+        phi = rad(lat * -1)
+        x = 1032 * lam * math.cos(phi)
+        y = 1032 * phi
+        return (x, y)
+
+
+
+
+
+
+ # +
+ +
+
+
class Mollweide(PseudoCylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, p=1.5707963267948966, lon0=0.0, lat0=0.0, cx=None, cy=None, cp=None, flip=0):
+        PseudoCylindrical.__init__(self, lon0=lon0, flip=flip)
+        self.MAX_ITER = 10
+        self.TOLERANCE = 1e-7
+
+        if p != None:
+            p2 = p + p
+            sp = math.sin(p)
+            r = math.sqrt(math.pi * 2.0 * sp / (p2 + math.sin(p2)))
+            self.cx = 2. * r / math.pi
+            self.cy = r / sp
+            self.cp = p2 + math.sin(p2)
+        elif cx != None and cy != None and cp != None:
+            self.cx = cx
+            self.cy = cy
+            self.cp = cp
+        else:
+            assert False, 'either p or cx,cy,cp must be defined'
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        lam = rad(lon)
+        phi = rad(lat)
+
+        k = self.cp * math.sin(phi)
+        i = self.MAX_ITER
+
+        while i != 0:
+            v = (phi + math.sin(phi) - k) / (1. + math.cos(phi))
+            phi -= v
+            if abs(v) < self.TOLERANCE:
+                break
+            i -= 1
+
+        if i == 0:
+            phi = (self.HALFPI, -self.HALFPI)[phi < 0]
+        else:
+            phi *= 0.5
+
+        x = 1000 * self.cx * lam * math.cos(phi)
+        y = 1000 * self.cy * math.sin(phi)
+        return (x, y * -1)
+
+
+
+
+
+
+ # +
+ +
+
+
class GoodeHomolosine(PseudoCylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0, flip=0):
+        self.lat1 = 41.737
+        PseudoCylindrical.__init__(self, lon0=lon0, flip=flip)
+        self.p1 = Mollweide()
+        self.p0 = Sinusoidal()
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+
+
+
+
+
+
+ # +
+

lon = me.clon(lon)

+
+
+
        if abs(lat) > self.lat1:
+            return self.p1.project(lon, lat)
+        else:
+            return self.p0.project(lon, lat)
+
+
+
+
+
+
+ # +
+ +
+
+
class WagnerIV(Mollweide):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0, lat0=0, flip=0):
+
+
+
+
+
+
+ # +
+

p=math.pi/3

+
+
+
        Mollweide.__init__(self, p=1.0471975511965976, flip=flip)
+
+
+
+
+
+
+ # +
+ +
+
+
class WagnerV(Mollweide):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lat0=0, lon0=0, flip=0):
+        Mollweide.__init__(self, cx=0.90977, cy=1.65014, cp=3.00896, flip=flip)
+
+
+
+
+
+
+ # +
+ +
+
+
class Loximuthal(PseudoCylindrical):
+
+    minLat = -89
+    maxLat = 89
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0.0, lat0=0.0, flip=0):
+        PseudoCylindrical.__init__(self, lon0=lon0, flip=flip)
+        if flip == 1:
+            lat0 = -lat0
+        self.lat0 = lat0
+        self.phi0 = rad(lat0)
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        lon, lat = self.ll(lon, lat)
+        lam = rad(lon)
+        phi = rad(lat)
+        if phi == self.phi0:
+            x = lam * math.cos(self.phi0)
+        else:
+            try:
+                x = lam * (phi - self.phi0) / (math.log(math.tan(self.QUARTERPI + phi * 0.5)) - math.log(math.tan(self.QUARTERPI + self.phi0 * 0.5)))
+            except:
+                return None
+        x *= 1000
+        y = 1000 * (phi - self.phi0)
+        return (x, y * -1)
+
+
+
+
+
+
+ # +
+ +
+
+
    def attrs(self):
+        p = super(Loximuthal, self).attrs()
+        p['lat0'] = self.lat0
+        return p
+
+
+
+
+
+
+ # +
+

Canters, F. (2002) Small-scale Map projection Design. p. 218-219. +Modified Sinusoidal, equal-area.

+

implementation borrowed from +http://cartography.oregonstate.edu/temp/AdaptiveProjection/src/projections/Canters1.js

+
+
+
    @staticmethod
+    def attributes():
+        return ['lon0', 'lat0', 'flip']
+
+
+class CantersModifiedSinusoidalI(PseudoCylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0.0, flip=0):
+        PseudoCylindrical.__init__(self, lon0=lon0, flip=flip)
+        self.C1 = 1.1966
+        self.C3 = -0.1290
+        self.C3x3 = 3 * self.C3
+        self.C5 = -0.0076
+        self.C5x5 = 5 * self.C5
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(self, lon, lat):
+        me = self
+        lon, lat = me.ll(lon, lat)
+
+        lon = rad(lon)
+        lat = rad(lat)
+
+        y2 = lat * lat
+        y4 = y2 * y2
+        x = 1000 * lon * math.cos(lat) / (me.C1 + me.C3x3 * y2 + me.C5x5 * y4)
+        y = 1000 * lat * (me.C1 + me.C3 * y2 + me.C5 * y4)
+        return (x, y * -1)
+
+
+
+
+
+
+ # +
+ +
+
+
class Hatano(PseudoCylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(me, lon0=0, flip=0):
+        PseudoCylindrical.__init__(me, lon0=lon0, flip=flip)
+        me.NITER = 20
+        me.EPS = 1e-7
+        me.ONETOL = 1.000001
+        me.CN = 2.67595
+        me.CS = 2.43763
+        me.RCN = 0.37369906014686373063
+        me.RCS = 0.41023453108141924738
+        me.FYCN = 1.75859
+        me.FYCS = 1.93052
+        me.RYCN = 0.56863737426006061674
+        me.RYCS = 0.51799515156538134803
+        me.FXC = 0.85
+        me.RXC = 1.17647058823529411764
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(me, lon, lat):
+        [lon, lat] = me.ll(lon, lat)
+        lam = rad(lon)
+        phi = rad(lat)
+        c = math.sin(phi) * (me.CN, me.CS)[phi < 0.0]
+        for i in range(me.NITER, 0, -1):
+            th1 = (phi + math.sin(phi) - c) / (1.0 + math.cos(phi))
+            phi -= th1
+            if abs(th1) < me.EPS:
+                break
+        phi *= 0.5
+        x = 1000 * me.FXC * lam * math.cos(phi)
+        y = 1000 * math.sin(phi) * (me.FYCN, me.FYCS)[phi < 0.0]
+        return (x, y * -1)
+
+
+
+
+
+
+ # +
+

Aitoff projection

+

implementation taken from +Snyder, Map projections - A working manual

+
+
+
class Aitoff(PseudoCylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0, flip=0):
+        PseudoCylindrical.__init__(self, lon0=lon0, flip=flip)
+        self.winkel = False
+        self.COSPHI1 = 0.636619772367581343
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(me, lon, lat):
+        [lon, lat] = me.ll(lon, lat)
+        lam = rad(lon)
+        phi = rad(lat)
+        c = 0.5 * lam
+        d = math.acos(math.cos(phi) * math.cos(c))
+        if d != 0:
+            y = 1.0 / math.sin(d)
+            x = 2.0 * d * math.cos(phi) * math.sin(c) * y
+            y *= d * math.sin(phi)
+        else:
+            x = y = 0
+        if me.winkel:
+            x = (x + lam * me.COSPHI1) * 0.5
+            y = (y + phi) * 0.5
+        return (x * 1000, y * -1000)
+
+
+
+
+
+
+ # +
+ +
+
+
class Winkel3(Aitoff):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, lon0=0, flip=0):
+        Aitoff.__init__(self, lon0=lon0, flip=flip)
+        self.winkel = True
+
+
+
+
+
+
+ # +
+ +
+
+
class Nicolosi(PseudoCylindrical):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(me, lon0=0, flip=0):
+        me.EPS = 1e-10
+        PseudoCylindrical.__init__(me, lon0=lon0, flip=flip)
+        me.r = me.HALFPI * 100
+        sea = []
+        r = me.r
+        for phi in range(0, 361):
+            sea.append((math.cos(rad(phi)) * r, math.sin(rad(phi)) * r))
+        me.sea = sea
+
+
+
+
+
+
+ # +
+ +
+
+
    def _clon(me, lon):
+        lon -= me.lon0
+        if lon < -180:
+            lon += 360
+        elif lon > 180:
+            lon -= 360
+        return lon
+
+
+
+
+
+
+ # +
+ +
+
+
    def _visible(me, lon, lat):
+
+
+
+
+
+
+ # +
+

lon = me._clon(lon)

+
+
+
        return lon > -90 and lon < 90
+
+
+
+
+
+
+ # +
+ +
+
+
    def _truncate(me, x, y):
+        theta = math.atan2(y, x)
+        x1 = me.r * math.cos(theta)
+        y1 = me.r * math.sin(theta)
+        return (x1, y1)
+
+
+
+
+
+
+ # +
+ +
+
+
    def world_bounds(self, bbox, llbbox=(-180, -90, 180, 90)):
+        if llbbox == (-180, -90, 180, 90):
+            d = self.r * 2
+            bbox.update((-d, -d))
+            bbox.update((d, d))
+        else:
+            bbox = super(PseudoCylindrical, self).world_bounds(bbox, llbbox)
+        return bbox
+
+
+
+
+
+
+ # +
+ +
+
+
    def sea_shape(self, llbbox=(-180, -90, 180, 90)):
+        out = []
+        if llbbox == (-180, -90, 180, 90) or llbbox == [-180, -90, 180, 90]:
+            for phi in range(0, 360):
+                x = math.cos(math.radians(phi)) * self.r
+                y = math.sin(math.radians(phi)) * self.r
+                out.append((x, y))
+            out = [out]
+        else:
+            out = super(PseudoCylindrical, self).sea_shape(llbbox)
+        return out
+
+
+
+
+
+
+ # +
+ +
+
+
    def project(me, lon, lat):
+        [lon, lat] = me.ll(lon, lat)
+        lam = rad(lon)
+        phi = rad(lat)
+
+        if abs(lam) < me.EPS:
+            x = 0
+            y = phi
+        elif abs(phi) < me.EPS:
+            x = lam
+            y = 0
+        elif abs(abs(lam) - me.HALFPI) < me.EPS:
+            x = lam * math.cos(phi)
+            y = me.HALFPI * math.sin(phi)
+        elif abs(abs(phi) - me.HALFPI) < me.EPS:
+            x = 0
+            y = phi
+        else:
+            tb = me.HALFPI / lam - lam / me.HALFPI
+            c = phi / me.HALFPI
+            sp = math.sin(phi)
+            d = (1 - c * c) / (sp - c)
+            r2 = tb / d
+            r2 *= r2
+            m = (tb * sp / d - 0.5 * tb) / (1.0 + r2)
+            n = (sp / r2 + 0.5 * d) / (1.0 + 1.0 / r2)
+            x = math.cos(phi)
+            x = math.sqrt(m * m + x * x / (1.0 + r2))
+            x = me.HALFPI * (m + (x, -x)[lam < 0])
+            f = n * n - (sp * sp / r2 + d * sp - 1.0) / (1.0 + 1.0 / r2)
+            if f < 0:
+                y = phi
+            else:
+                y = math.sqrt(f)
+                y = me.HALFPI * (n + (-y, y)[phi < 0])
+        return (x * 100, y * -100)
+
+
+
+
+
+
+ # +
+ +
+
+
    def plot(self, polygon, truncate=True):
+        polygons = self._shift_polygon(polygon)
+        plotted = []
+        for polygon in polygons:
+            points = []
+            ignore = True
+            for (lon, lat) in polygon:
+                vis = self._visible(lon, lat)
+                if vis:
+                    ignore = False
+                x, y = self.project(lon, lat)
+                if not vis and truncate:
+                    points.append(self._truncate(x, y))
+                else:
+                    points.append((x, y))
+            if ignore:
+                continue
+            plotted.append(points)
+        return plotted
+
+
+
+
+
+
+ diff --git a/source/pycco.css b/source/pycco.css new file mode 100644 index 0000000..57e6c47 --- /dev/null +++ b/source/pycco.css @@ -0,0 +1,186 @@ +/*--------------------- Layout and Typography ----------------------------*/ +body { + font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; + font-size: 16px; + line-height: 24px; + color: #252519; + margin: 0; padding: 0; +} +a { + color: #261a3b; +} + a:visited { + color: #261a3b; + } +p { + margin: 0 0 15px 0; +} +h1, h2, h3, h4, h5, h6 { + margin: 40px 0 15px 0; +} +h2, h3, h4, h5, h6 { + margin-top: 0; + } +#container, div.section { + position: relative; +} +#background { + position: fixed; + top: 0; left: 580px; right: 0; bottom: 0; + background: #f5f5ff; + border-left: 1px solid #e5e5ee; + z-index: -1; +} +#jump_to, #jump_page { + background: white; + -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; + -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; + font: 10px Arial; + text-transform: uppercase; + cursor: pointer; + text-align: right; +} +#jump_to, #jump_wrapper { + position: fixed; + right: 0; top: 0; + padding: 5px 10px; +} + #jump_wrapper { + padding: 0; + display: none; + } + #jump_to:hover #jump_wrapper { + display: block; + } + #jump_page { + padding: 5px 0 3px; + margin: 0 0 25px 25px; + } + #jump_page .source { + display: block; + padding: 5px 10px; + text-decoration: none; + border-top: 1px solid #eee; + } + #jump_page .source:hover { + background: #f5f5ff; + } + #jump_page .source:first-child { + } +div.docs { + float: left; + max-width: 500px; + min-width: 500px; + min-height: 5px; + padding: 10px 25px 1px 50px; + vertical-align: top; + text-align: left; +} + .docs pre { + margin: 15px 0 15px; + padding-left: 15px; + } + .docs p tt, .docs p code { + background: #f8f8ff; + border: 1px solid #dedede; + font-size: 12px; + padding: 0 0.2em; + } + .octowrap { + position: relative; + } + .octothorpe { + font: 12px Arial; + text-decoration: none; + color: #454545; + position: absolute; + top: 3px; left: -20px; + padding: 1px 2px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + } + div.docs:hover .octothorpe { + opacity: 1; + } +div.code { + margin-left: 580px; + padding: 14px 15px 16px 50px; + vertical-align: top; +} + .code pre, .docs p code { + font-size: 12px; + } + pre, tt, code { + line-height: 18px; + font-family: Monaco, Consolas, "Lucida Console", monospace; + margin: 0; padding: 0; + } +div.clearall { + clear: both; +} + + +/*---------------------- Syntax Highlighting -----------------------------*/ +td.linenos { background-color: #f0f0f0; padding-right: 10px; } +span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } +body .hll { background-color: #ffffcc } +body .c { color: #408080; font-style: italic } /* Comment */ +body .err { border: 1px solid #FF0000 } /* Error */ +body .k { color: #954121 } /* Keyword */ +body .o { color: #666666 } /* Operator */ +body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +body .cp { color: #BC7A00 } /* Comment.Preproc */ +body .c1 { color: #408080; font-style: italic } /* Comment.Single */ +body .cs { color: #408080; font-style: italic } /* Comment.Special */ +body .gd { color: #A00000 } /* Generic.Deleted */ +body .ge { font-style: italic } /* Generic.Emph */ +body .gr { color: #FF0000 } /* Generic.Error */ +body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +body .gi { color: #00A000 } /* Generic.Inserted */ +body .go { color: #808080 } /* Generic.Output */ +body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +body .gs { font-weight: bold } /* Generic.Strong */ +body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +body .gt { color: #0040D0 } /* Generic.Traceback */ +body .kc { color: #954121 } /* Keyword.Constant */ +body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ +body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ +body .kp { color: #954121 } /* Keyword.Pseudo */ +body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ +body .kt { color: #B00040 } /* Keyword.Type */ +body .m { color: #666666 } /* Literal.Number */ +body .s { color: #219161 } /* Literal.String */ +body .na { color: #7D9029 } /* Name.Attribute */ +body .nb { color: #954121 } /* Name.Builtin */ +body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +body .no { color: #880000 } /* Name.Constant */ +body .nd { color: #AA22FF } /* Name.Decorator */ +body .ni { color: #999999; font-weight: bold } /* Name.Entity */ +body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +body .nf { color: #0000FF } /* Name.Function */ +body .nl { color: #A0A000 } /* Name.Label */ +body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +body .nt { color: #954121; font-weight: bold } /* Name.Tag */ +body .nv { color: #19469D } /* Name.Variable */ +body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +body .w { color: #bbbbbb } /* Text.Whitespace */ +body .mf { color: #666666 } /* Literal.Number.Float */ +body .mh { color: #666666 } /* Literal.Number.Hex */ +body .mi { color: #666666 } /* Literal.Number.Integer */ +body .mo { color: #666666 } /* Literal.Number.Oct */ +body .sb { color: #219161 } /* Literal.String.Backtick */ +body .sc { color: #219161 } /* Literal.String.Char */ +body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ +body .s2 { color: #219161 } /* Literal.String.Double */ +body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +body .sh { color: #219161 } /* Literal.String.Heredoc */ +body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +body .sx { color: #954121 } /* Literal.String.Other */ +body .sr { color: #BB6688 } /* Literal.String.Regex */ +body .s1 { color: #219161 } /* Literal.String.Single */ +body .ss { color: #19469D } /* Literal.String.Symbol */ +body .bp { color: #954121 } /* Name.Builtin.Pseudo */ +body .vc { color: #19469D } /* Name.Variable.Class */ +body .vg { color: #19469D } /* Name.Variable.Global */ +body .vi { color: #19469D } /* Name.Variable.Instance */ +body .il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/source/renderer/__init__.html b/source/renderer/__init__.html new file mode 100644 index 0000000..f46964c --- /dev/null +++ b/source/renderer/__init__.html @@ -0,0 +1,100 @@ + + + + + __init__.py + + + +
+
+
+

__init__.py

+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
class MapRenderer(object):
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, map):
+        self.map = map
+
+
+
+
+
+
+ # +
+ +
+
+
    def render(self):
+        pass
+
+
+
+
+
+
+ # +
+ +
+
+
    def write(self, filename):
+        raise 'Not implemented yet'
+
+
+
+
+
+
+ # +
+ +
+
+
    def preview(self):
+        raise 'Not implemented yet'
+
+
+from svg import SvgRenderer
+from kml import KmlRenderer
+
+__all__ = ['MapRenderer', 'SvgRenderer', 'KmlRenderer']
+
+
+
+
+
+
+ diff --git a/source/renderer/kml.html b/source/renderer/kml.html new file mode 100644 index 0000000..676fc42 --- /dev/null +++ b/source/renderer/kml.html @@ -0,0 +1,272 @@ + + + + + kml.py + + + +
+
+
+

kml.py

+
+
+
+
+
+ # +
+ +
+
+
from kartograph.renderer import MapRenderer
+from kartograph.errors import KartographError
+from pykml.factory import KML_ElementMaker as KML
+
+
+
+
+
+
+ # +
+ +
+
+
class KmlRenderer(MapRenderer):
+
+
+
+
+
+
+ # +
+ +
+
+
    def render(self):
+        self._init_kml_canvas()
+        self._store_layers_kml()
+
+
+
+
+
+
+ # +
+ +
+
+
    def write(self, filename):
+        outfile = open(filename, 'w')
+        from lxml import etree
+        outfile.write(etree.tostring(self.kml, pretty_print=True))
+
+
+
+
+
+
+ # +
+ +
+
+
    def preview(self):
+        self.write('tmp.kml')
+        from subprocess import call
+        call(["open", "tmp.kml"])
+
+
+
+
+
+
+ # +
+ +
+
+
    def _init_kml_canvas(self):
+        self.kml = KML.kml(
+            KML.Document(
+                KML.name('kartograph map')
+            )
+        )
+
+
+
+
+
+
+ # +
+

store features in kml (projected to WGS84 latlon)

+
+
+
    def _store_layers_kml(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        from pykml.factory import KML_ElementMaker as KML
+
+        for layer in self.map.layers:
+            if len(layer.features) == 0:
+                continue  # ignore empty layers
+            g = KML.Folder(
+                KML.name(id)
+            )
+            for feat in layer.features:
+                g.append(self._render_feature(feat, layer.options['attributes']))
+            self.kml.Document.append(g)
+
+
+
+
+
+
+ # +
+ +
+
+
    def _render_feature(self, feature, attributes=[]):
+        path = self._render_geometry(feature.geometry)
+
+        pm = KML.Placemark(
+            KML.name(unicode(feature.props[attributes[0]['src']])),
+            path
+        )
+
+        xt = KML.ExtendedData()
+
+        for cfg in attributes:
+            if 'src' in cfg:
+                if cfg['src'] not in feature.props:
+                    continue
+
+
+
+
+
+
+ # +
+

raise KartographError(('attribute not found "%s"'%cfg['src']))

+
+
+
                val = feature.props[cfg['src']]
+                import unicodedata
+                if isinstance(val, str):
+                    val = unicode(val, errors='ignore')
+                    val = unicodedata.normalize('NFKD', val).encode('ascii', 'ignore')
+                xt.append(KML.Data(
+                    KML.value(unicode(val)),
+                    name=cfg['tgt']
+                ))
+            elif 'where' in cfg:
+                src = cfg['where']
+                tgt = cfg['set']
+                if len(cfg['equals']) != len(cfg['to']):
+                    raise KartographError('attributes: "equals" and "to" arrays must be of same length')
+                for i in range(len(cfg['equals'])):
+                    if feature.props[src] == cfg['equals'][i]:
+                        xt.append(KML.Data(
+                            KML.value(cfg['to'][i]),
+                            name=tgt
+                        ))
+        pm.append(xt)
+
+        return pm
+
+
+
+
+
+
+ # +
+ +
+
+
    def _render_geometry(self, geometry):
+        from shapely.geometry import Polygon, MultiPolygon
+        if isinstance(geometry, (Polygon, MultiPolygon)):
+            return self._render_polygon(geometry)
+        else:
+            raise KartographError('kml-renderer is not fully implemented yet')
+
+
+
+
+
+
+ # +
+

renders a Polygon or MultiPolygon as KML node

+
+
+
    def _render_polygon(self, geometry):
+
+
+
+
+
+
+ # +
+ +
+
+
        geoms = hasattr(geometry, 'geoms') and geometry.geoms or [geometry]
+
+        kml_polys = []
+
+        for geom in geoms:
+            poly = KML.Polygon(
+                KML.tesselate("1")
+            )
+            outer = KML.outerBoundaryIs()
+            coords = ''
+            for pt in geom.exterior.coords:
+                coords += ','.join(map(str, pt)) + ' '
+            outer.append(KML.LinearRing(
+                KML.coordinates(coords)
+            ))
+            poly.append(outer)
+            inner = KML.innerBoundaryIs()
+            for hole in geom.interiors:
+                coords = ''
+                for pt in hole.coords:
+                    coords += ','.join(map(str, pt)) + ' '
+                inner.append(KML.LinearRing(
+                    KML.coordinates(coords)
+                ))
+            if len(inner) > 0:
+                poly.append(inner)
+            kml_polys.append(poly)
+
+        if len(kml_polys) == 1:
+            return kml_polys[0]
+        multigeometry = KML.MultiGeometry()
+        for p in kml_polys:
+            multigeometry.append(p)
+        return multigeometry
+
+
+
+
+
+
+ diff --git a/source/renderer/svg.html b/source/renderer/svg.html new file mode 100644 index 0000000..f2dfe78 --- /dev/null +++ b/source/renderer/svg.html @@ -0,0 +1,525 @@ + + + + + svg.py + + + +
+
+
+

svg.py

+
+
+
+
+
+ # +
+ +
+
+
from kartograph.renderer import MapRenderer
+from kartograph.errors import KartographError
+
+
+
+
+
+
+ # +
+

This script contains everything that is needed by Kartograph to finally +render the processed maps into SVG files.

+
+
+
+
+
+
+
+
+
+ # +
+

The SVG renderer is based on xml.dom.minidom.

+
+
+
from xml.dom import minidom
+from xml.dom.minidom import parse
+import re
+
+
+
+
+
+
+ # +
+ +
+
+
class SvgRenderer(MapRenderer):
+
+
+
+
+
+
+ # +
+

The render() method prepares a new empty SVG document and +stores all the layer features into SVG groups.

+
+
+
    def render(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        self._init_svg_doc()
+        self._store_layers_to_svg()
+
+
+
+
+
+
+ # +
+ +
+
+
    def _init_svg_doc(self):
+
+
+
+
+
+
+ # +
+

Load width and height of the map view +We add two pixels to the height to ensure that +the map fits.

+
+
+
        w = self.map.view.width
+        h = self.map.view.height + 2
+
+
+
+
+
+
+ # +
+

SvgDocument is a handy wrapper around xml.dom.minidom. It is defined below.

+
+
+
        svg = SvgDocument(
+            width='%dpx' % w,
+            height='%dpx' % h,
+            viewBox='0 0 %d %d' % (w, h),
+            enable_background='new 0 0 %d %d' % (w, h),
+            style='stroke-linejoin: round; stroke:#000; fill:#f6f3f0;')
+
+        defs = svg.node('defs', svg.root)
+        style = svg.node('style', defs, type='text/css')
+        css = 'path { fill-rule: evenodd; }\n#context path { fill: #eee; stroke: #bbb; } '
+        svg.cdata(css, style)
+        metadata = svg.node('metadata', svg.root)
+        views = svg.node('views', metadata)
+        view = svg.node('view', views,
+            padding=str(self.map.options['bounds']['padding']), w=w, h=h)
+
+        svg.node('proj', view, **self.map.proj.attrs())
+        svg.node('bbox', view,
+            x=round(self.map.src_bbox.left, 2),
+            y=round(self.map.src_bbox.top, 2),
+            w=round(self.map.src_bbox.width, 2),
+            h=round(self.map.src_bbox.height, 2))
+
+        ll = [-180, -90, 180, 90]
+        if self.map.options['bounds']['mode'] == "bbox":
+            ll = self.map.options['bounds']['data']
+        svg.node('llbbox', view,
+            lon0=ll[0], lon1=ll[2],
+            lat0=ll[1], lat1=ll[3])
+        self.svg = svg
+
+
+
+
+
+
+ # +
+

store features in svg

+
+
+
    def _store_layers_to_svg(self):
+
+
+
+
+
+
+ # +
+ +
+
+
        svg = self.svg
+        for layer in self.map.layers:
+            if len(layer.features) == 0:
+                print "ignoring empty layer", layer.id
+                continue  # ignore empty layers
+            g = svg.node('g', svg.root, id=layer.id)
+            for feat in layer.features:
+                node = self._render_feature(feat, layer.options['attributes'])
+                if node is not None:
+                    g.appendChild(node)
+                else:
+                    print "feature.to_svg is None", feat
+            if 'styles' in layer.options:
+                for prop in layer.options['styles']:
+                    g.setAttribute(prop, str(layer.options['styles'][prop]))
+
+
+
+
+
+
+ # +
+ +
+
+
    def _render_feature(self, feature, attributes=[]):
+        node = self._render_geometry(feature.geometry)
+        if node is None:
+            return None
+
+        for cfg in attributes:
+            if 'src' in cfg:
+                tgt = re.sub('(\W|_)+', '-', cfg['tgt'].lower())
+                if cfg['src'] not in feature.props:
+                    continue
+
+
+
+
+
+
+ # +
+

raise KartographError(('attribute not found "%s"'%cfg['src']))

+
+
+
                val = feature.props[cfg['src']]
+                if isinstance(val, (int, float)):
+                    val = str(val)
+                node.setAttribute('data-' + tgt, val)
+                if tgt == "id":
+                    node.setAttribute('id', val)
+
+            elif 'where' in cfg:
+
+
+
+
+
+
+ # +
+

can be used to replace attributes...

+
+
+
                src = cfg['where']
+                tgt = cfg['set']
+                if len(cfg['equals']) != len(cfg['to']):
+                    raise KartographError('attributes: "equals" and "to" arrays must be of same length')
+                for i in range(len(cfg['equals'])):
+                    if feature.props[src] == cfg['equals'][i]:
+                        node.setAttribute('data-' + tgt, cfg['to'][i])
+
+        if '__color__' in feature.props:
+            node.setAttribute('fill', self.props['__color__'])
+        return node
+
+
+
+
+
+
+ # +
+ +
+
+
    def _render_geometry(self, geometry):
+        from shapely.geometry import Polygon, MultiPolygon
+        if isinstance(geometry, (Polygon, MultiPolygon)):
+            return self._render_polygon(geometry)
+
+
+
+
+
+
+ # +
+

constructs a svg representation of a polygon

+
+
+
    def _render_polygon(self, geometry):
+
+
+
+
+
+
+ # +
+ +
+
+
        _round = self.map.options['export']['round']
+        path_str = ""
+        if _round is False:
+            fmt = '%f,%f'
+        else:
+            fmt = '%.' + str(_round) + 'f'
+            fmt = fmt + ',' + fmt
+
+        geoms = hasattr(geometry, 'geoms') and geometry.geoms or [geometry]
+        for polygon in geoms:
+            if polygon is None:
+                continue
+            for ring in [polygon.exterior] + list(polygon.interiors):
+                cont_str = ""
+                kept = []
+                for pt in ring.coords:
+                    kept.append(pt)
+                if len(kept) <= 3:
+                    continue
+                for pt in kept:
+                    if cont_str == "":
+                        cont_str = "M"
+                    else:
+                        cont_str += "L"
+                    cont_str += fmt % pt
+                cont_str += "Z "
+                path_str += cont_str
+        if path_str == "":
+            return None
+        path = self.svg.node('path', d=path_str)
+        return path
+
+
+
+
+
+
+ # +
+ +
+
+
    def write(self, filename):
+        self.svg.write(filename)
+
+
+
+
+
+
+ # +
+ +
+
+
    def preview(self):
+        self.svg.preview()
+
+
+
+
+
+
+ # +
+

SvgDocument

+

SVGDocument is a handy wrapper around xml.dom.minidom which allows us +to quickly build XML structures. It is largely inspired by the SVG class +of the svgfig project, which was +used by one of the earlier versions of Kartograph.

+
+
+
+
+
+
+
+
+
+ # +
+ +
+
+
class SvgDocument(object):
+
+
+
+
+
+
+ # +
+

Of course, we need to create and XML document with all this +boring SVG header stuff added to it.

+
+
+
    def __init__(self, **kwargs):
+        imp = minidom.getDOMImplementation('')
+        dt = imp.createDocumentType('svg',
+            '-//W3C//DTD SVG 1.1//EN',
+            'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd')
+        self.doc = imp.createDocument('http://www.w3.org/2000/svg', 'svg', dt)
+        self.root = svg = self.doc.getElementsByTagName('svg')[0]
+        svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
+        svg.setAttribute('version', '1.1')
+        svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
+        _add_attrs(self.root, kwargs)
+
+
+
+
+
+
+ # +
+

This is the magic of SvgDocument. Instead of having to do appendChild() +and addAttribute() for every node we create, we just call svgdoc.node() +which is smart enough to append itself to the parent if we specify one, +and also sets all attributes we pass as keyword arguments.

+
+
+
    def node(self, name, parent=None, **kwargs):
+        el = self.doc.createElement(name)
+        _add_attrs(el, kwargs)
+        if parent is not None:
+            parent.appendChild(el)
+        return el
+
+
+
+
+
+
+ # +
+

Sometimes we also need a <[CDATA]> block, for instance if we embed +CSS code in the SVG document.

+
+
+
    def cdata(self, data, parent=None):
+        cd = minidom.CDATASection()
+        cd.data = data
+        if parent is not None:
+            parent.appendChild(cd)
+        return cd
+
+
+
+
+
+
+ # +
+

Here we finally write the SVG file, and we're brave enough +to try to write it in Unicode.

+
+
+
    def write(self, outfile):
+        if isinstance(outfile, str):
+            outfile = open(outfile, 'w')
+        raw = self.doc.toxml()
+        try:
+            raw = raw.encode('utf-8')
+        except:
+            print 'warning: could not encode to unicode'
+
+        outfile.write(raw)
+        outfile.close()
+
+
+
+
+
+
+ # +
+

Don't blame me if you don't have a command-line shortcut to +simply the best free browser of the world.

+
+
+
    def preview(self):
+        self.write('tmp.svg')
+        from subprocess import call
+        call(["firefox", "tmp.svg"])
+
+
+
+
+
+
+ # +
+ +
+
+
    def tostring(self):
+        return self.doc.toxml()
+
+
+
+
+
+
+ # +
+

This is an artifact of an older version of Kartograph, but +maybe we'll need it later. It will load an SVG document from +a file.

+
+
+
    @staticmethod
+    def load(filename):
+        svg = SvgDocument()
+        dom = parse(filename)
+        svg.doc = dom
+        svg.root = dom.getElementsByTagName('svg')[0]
+        return svg
+
+
+def _add_attrs(node, attrs):
+    for key in attrs:
+        node.setAttribute(key, str(attrs[key]))
+
+
+
+
+
+
+ diff --git a/source/simplify/__init__.html b/source/simplify/__init__.html new file mode 100644 index 0000000..15a2001 --- /dev/null +++ b/source/simplify/__init__.html @@ -0,0 +1,85 @@ + + + + + __init__.py + + + +
+
+
+

__init__.py

+
+
+
+
+
+ # +
+ +
+
+
__all__ = ['create_point_store', 'unify_polygon', 'unify_polygons', 'simplify_lines']
+
+from unify import *
+from distance import simplify_distance
+from douglas_peucker import simplify_douglas_peucker
+from visvalingam import simplify_visvalingam_whyatt
+
+
+simplification_methods = dict()
+simplification_methods['distance'] = simplify_distance
+simplification_methods['douglas-peucker'] = simplify_douglas_peucker
+simplification_methods['visvalingam-whyatt'] = simplify_visvalingam_whyatt
+
+
+
+
+
+
+ # +
+

simplifies a set of lines

+
+
+
def simplify_lines(lines, method, params):
+
+
+
+
+
+
+ # +
+ +
+
+
    simplify = simplification_methods[method]
+    out = []
+    for line in lines:
+
+
+
+
+
+
+ # +
+

remove duplicate points from line

+
+
+
        unique = [line[0]]
+        for i in range(1, len(line)):
+            if line[i] != line[i - 1]:
+                unique.append(line[i])
+        simplify(unique, params)
+        out.append(unique)
+    return out
+
+
+
+
+
+
+ diff --git a/source/simplify/distance.html b/source/simplify/distance.html new file mode 100644 index 0000000..f1de2e3 --- /dev/null +++ b/source/simplify/distance.html @@ -0,0 +1,108 @@ + + + + + distance.py + + + +
+
+
+

distance.py

+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

simplifies a line segment using a very simple algorithm that checks the distance +to the last non-deleted point. the algorithm operates on line segments.

+

in order to preserve topology of the original polygons the algorithm +- never removes the first or the last point of a line segment +- flags all points as simplified after processing (so it won't be processed twice)

+
+
+
def simplify_distance(points, dist):
+
+
+
+
+
+
+ # +
+ +
+
+
    dist_sq = dist * dist
+    n = len(points)
+
+    kept = []
+    deleted = 0
+    if n < 4:
+        return points
+
+    for i in range(0, n):
+        pt = points[i]
+        if i == 0 or i == n - 1:
+
+
+
+
+
+
+ # +
+

never remove first or last point of line

+
+
+
            pt.simplified = True
+            lpt = pt
+            kept.append(pt)
+        else:
+            d = (pt.x - lpt.x) * (pt.x - lpt.x) + (pt.y - lpt.y) * (pt.y - lpt.y)  # compute distance to last point
+            if pt.simplified or d > dist_sq:  # if point already handled or distance exceeds threshold..
+                kept.append(pt)  # ..keep the point
+                lpt = pt
+            else:  # otherwise remove it
+                deleted += 1
+                pt.deleted = True
+            pt.simplified = True
+
+    if len(kept) < 4:
+        for pt in points:
+            pt.deleted = False
+        return points
+
+    return kept
+
+
+
+
+
+
+ # +
+

print 'kept %d deleted %d' % (kept, deleted)

+
+
+
+
+
+
+
+ diff --git a/source/simplify/douglas_peucker.html b/source/simplify/douglas_peucker.html new file mode 100644 index 0000000..464ba85 --- /dev/null +++ b/source/simplify/douglas_peucker.html @@ -0,0 +1,221 @@ + + + + + douglas_peucker.py + + + +
+
+
+

douglas_peucker.py

+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

simplifies a line segment using the Douglas-Peucker algorithm.

+

taken from +http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm#Pseudocode

+

in order to preserve topology of the original polygons the algorithm +- never removes the first or the last point of a line segment +- flags all points as simplified after processing (so it won't be processed twice)

+
+
+
def simplify_douglas_peucker(points, epsilon):
+
+
+
+
+
+
+ # +
+ +
+
+
    n = len(points)
+    kept = []
+    if n < 4:
+        return points  # skip short lines
+
+    if not points[0].simplified:
+        _douglas_peucker(points, 0, n - 1, epsilon)
+
+    return kept
+
+
+
+
+
+
+ # +
+

print 'kept %d deleted %d' % (kept, deleted)

+
+
+
+
+
+
+
+
+
+ # +
+

inner part of Douglas-Peucker algorithm, called recursively

+
+
+
def _douglas_peucker(points, start, end, epsilon):
+
+
+
+
+
+
+ # +
+ +
+
+
    dmax = 0
+    index = 0
+
+
+
+
+
+
+ # +
+

Find the point with the maximum distance

+
+
+
    for i in range(start + 1, end):
+        x1, y1 = points[start]
+        x2, y2 = points[end]
+        if x1 == x2 and y1 == y2:
+            return
+        x3, y3 = points[i]
+        d = _min_distance(x1, y1, x2, y2, x3, y3)
+        if d > dmax:
+            index = i
+            dmax = d
+
+
+
+
+
+
+ # +
+

If max distance is greater than epsilon, recursively simplify

+
+
+
    if dmax >= epsilon and start < index < end:
+
+
+
+
+
+
+ # +
+

recursivly call

+
+
+
        _douglas_peucker(points, start, index, epsilon)
+        _douglas_peucker(points, index, end, epsilon)
+    else:
+
+
+
+
+
+
+ # +
+

remove any point but the first and last

+
+
+
        for i in range(start, end + 1):
+            points[i].deleted = i == start or i == end
+            points[i].simplified = True
+
+
+
+
+
+
+ # +
+

the perpendicular distance from a point (x3,y3) to the line from (x1,y1) to (x2,y2) +taken from http://local.wasp.uwa.edu.au/~pbourke/geometry/pointline/

+
+
+
def _min_distance(x1, y1, x2, y2, x3, y3):
+
+
+
+
+
+
+ # +
+ +
+
+
    d = _dist(x1, y1, x2, y2)
+    u = (x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1) / (d * d)
+    x = x1 + u * (x2 - x1)
+    y = y1 + u * (y2 - y1)
+    return _dist(x, y, x3, y3)
+
+
+
+
+
+
+ # +
+

eucledian distance between two points

+
+
+
def _dist(x1, y1, x2, y2):
+
+
+
+
+
+
+ # +
+ +
+
+
    import math
+    dx = x2 - x1
+    dy = y2 - y1
+    return math.sqrt(dx * dx + dy * dy)
+
+
+
+
+
+
+ diff --git a/source/simplify/mpoint.html b/source/simplify/mpoint.html new file mode 100644 index 0000000..f6d9f30 --- /dev/null +++ b/source/simplify/mpoint.html @@ -0,0 +1,133 @@ + + + + + mpoint.py + + + +
+
+
+

mpoint.py

+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

Point class used for polygon simplification

+
+
+
class MPoint:
+
+
+
+
+
+
+ # +
+ +
+
+
    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+        self.simplified = False
+        self.deleted = False
+        self.keep = False
+        self.features = set()
+
+
+
+
+
+
+ # +
+ +
+
+
    def isDeletable(self):
+        if self.keep or self.simplified or self.three:
+            return False
+        return True
+
+
+
+
+
+
+ # +
+ +
+
+
    def __repr__(self):
+        return 'Pt(%.2f,%.2f)' % (self.x, self.y)
+
+
+
+
+
+
+ # +
+ +
+
+
    def __len__(self):
+        return 2
+
+
+
+
+
+
+ # +
+ +
+
+
    def __getitem__(self, key):
+        if key == 0:
+            return self.x
+        if key == 1:
+            return self.y
+        raise IndexError()
+
+
+
+
+
+
+ # +
+ +
+
+
    def __contains__(self, key):
+        if key == "deleted":
+            return True
+        return False
+
+
+
+
+
+
+ diff --git a/source/simplify/unify.html b/source/simplify/unify.html new file mode 100644 index 0000000..758861b --- /dev/null +++ b/source/simplify/unify.html @@ -0,0 +1,152 @@ + + + + + unify.py + + + +
+
+
+

unify.py

+
+
+
+
+
+ # +
+

the whole point of the unification step is to convert all points into unique MPoint instances

+
+
+
from mpoint import MPoint
+
+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

creates a new point_store

+
+
+
def create_point_store():
+
+
+
+
+
+
+ # +
+ +
+
+
    point_store = {'kept': 0, 'removed': 0}
+    return point_store
+
+
+
+
+
+
+ # +
+ +
+
+
def unify_rings(rings, point_store, precision=None, feature=None):
+    out = []
+    for ring in rings:
+        out.append(unify_ring(ring, point_store, precision=precision, feature=feature))
+    return out
+
+
+
+
+
+
+ # +
+

Replaces duplicate points with MPoint instances

+
+
+
def unify_ring(ring, point_store, precision=None, feature=None):
+
+
+
+
+
+
+ # +
+ +
+
+
    out_ring = []
+    lptid = ''
+    for pt in ring:
+        if 'deleted' not in pt:
+            pt = MPoint(pt[0], pt[1])  # eventually convert to MPoint
+
+
+
+
+
+
+ # +
+

generate hash for point

+
+
+
        if precision is not None:
+            fmt = '%' + precision + 'f-%' + precision + 'f'
+        else:
+            fmt = '%f-%f'
+        pid = fmt % (pt.x, pt.y)
+        if pid == lptid:
+            continue  # skip double points
+        lptid = pid
+        if pid in point_store:
+
+
+
+
+
+
+ # +
+

load existing point from point store

+
+
+
            point = point_store[pid]
+            point_store['removed'] += 1
+        else:
+            point = pt
+            point_store['kept'] += 1
+            point_store[pid] = pt
+
+        point.features.add(feature)
+        out_ring.append(point)
+    return out_ring
+
+
+
+
+
+
+ diff --git a/source/simplify/visvalingam.html b/source/simplify/visvalingam.html new file mode 100644 index 0000000..a4b10b6 --- /dev/null +++ b/source/simplify/visvalingam.html @@ -0,0 +1,218 @@ + + + + + visvalingam.py + + + +
+
+
+

visvalingam.py

+
+
+
+
+
+ # +
+ +
+
+
+
+
+
+
+
+
+ # +
+

Visvalingam-Whyatt simplification

+
+
+
def simplify_visvalingam_whyatt(points, tolerance):
+
+
+
+
+
+
+ # +
+

mplementation borrowed from @migurski: +ttps://github.com/migurski/Bloch/blob/master/Bloch/init.py#L133

+
+
+
    if len(points) < 3:
+        return
+    if points[1].simplified:
+        return
+
+    min_area = tolerance ** 2
+
+    pts = range(len(points))  # pts stores an index of all non-deleted points
+
+    while len(pts) > 4:
+        preserved, popped = set(), []
+        areas = []
+
+        for i in range(1, len(pts) - 1):
+            x1, y1 = points[pts[i - 1]]
+            x2, y2 = points[pts[i]]
+            x3, y3 = points[pts[i + 1]]
+
+
+
+
+
+
+ # +
+

compute and store triangle area

+
+
+
            areas.append((_tri_area(x1, y1, x2, y2, x3, y3), i))
+
+        areas = sorted(areas)
+
+        if not areas or areas[0][0] > min_area:
+
+
+
+
+
+
+ # +
+

there's nothing to be done

+
+
+
            for pt in points:
+                pt.simplified = True
+            break
+
+
+
+
+
+
+ # +
+

Reduce any segments that makes a triangle whose area is below +the minimum threshold, starting with the smallest and working up. +Mark segments to be preserved until the next iteration.

+
+
+
        for (area, i) in areas:
+
+            if area > min_area:
+
+
+
+
+
+
+ # +
+

there won't be any more points to remove.

+
+
+
                break
+
+            if i - 1 in preserved or i + 1 in preserved:
+
+
+
+
+
+
+ # +
+

the current segment is too close to a previously-preserved one. +print "-pre", preserved

+
+
+
                continue
+
+            points[pts[i]].deleted = True
+            popped.append(i)
+
+
+
+
+
+
+ # +
+

make sure that the adjacent points

+
+
+
            preserved.add(i - 1)
+            preserved.add(i + 1)
+
+        if len(popped) == 0:
+
+
+
+
+
+
+ # +
+

no points removed, so break out of loop

+
+
+
            break
+
+        popped = sorted(popped, reverse=True)
+        for i in popped:
+
+
+
+
+
+
+ # +
+

remove point from index list

+
+
+
            pts = pts[:i] + pts[i + 1:]
+
+    for pt in points:
+        pt.simplified = True
+
+
+
+
+
+
+ # +
+

computes the area of a triangle given by three points +implementation taken from: +http://www.btinternet.com/~se16/hgb/triangle.htm

+
+
+
def _tri_area(x1, y1, x2, y2, x3, y3):
+
+
+
+
+
+
+ # +
+ +
+
+
    return abs((x2*y1-x1*y2)+(x3*y2-x2*y3)+(x1*y3-x3*y1))/2.0
+
+
+
+
+
+
+