Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: tsibley/kartograph.py
base: 8151cba5a9
...
head fork: tsibley/kartograph.py
compare: a693794a38
  • 3 commits
  • 14 files changed
  • 0 commit comments
  • 1 contributor
Commits on Apr 02, 2012
@gka gka fixed graticule layer
added polyline geometry
added crop option
5acc46e
Commits on Apr 03, 2012
@gka gka - cleaned command-line interface, using argparse now
- removed -c option
55a23fe
@gka gka increased minor version since the cli changed a693794
View
11 CHANGELOG
@@ -1,3 +1,14 @@
+0.2.0
+-----
+* cleaned command-line interface, using argparse now
+* removed command-line option -c (pass config filename as positional argument instead)
+* removed command-line command generate (use svg instead)
+
+0.1.8
+-----
+* adding crop latlon bbox for limiting the number of processed geometries
+* bugfixes
+
0.1.7
-----
* removed default styles from sea layer
View
137 kartograph/cli.py 100644 → 100755
@@ -1,73 +1,102 @@
+#!/usr/bin/env python
"""
command line interface for kartograph
"""
+import argparse
+import os.path
+import json
+from errors import KartographError
+
+
+parser = argparse.ArgumentParser(prog='kartograph', description='Generating SVG maps from shapefiles')
+#parser.add_argument('command', type=str, choices=['svg', 'kml', 'generate'], help='specifies what kartograph is supposed to do')
+
+subparsers = parser.add_subparsers(help='sub-command help')
+
+parser_svg = subparsers.add_parser('svg', help='generates svg map')
+parser_svg.add_argument('config', type=argparse.FileType('r'), help='the configuration for the map. accepts json and yaml.')
+parser_svg.add_argument('--output', '-o', metavar='FILE', type=argparse.FileType('w'), help='the file in which the map will be stored')
+
+parser_kml = subparsers.add_parser('kml', help='generates kml map')
+parser_kml.add_argument('config', type=argparse.FileType('r'), help='the configuration for the map. accepts json and yaml.')
+parser_kml.add_argument('--output', '-o', metavar='FILE', type=argparse.FileType('w'), help='the file in which the map will be stored')
+
+parser_cartogram = subparsers.add_parser('cartogram', help='cartogram tool')
+parser_cartogram.add_argument('map', type=argparse.FileType('r'), help='the map svg the cartogram should be added to')
+parser_cartogram.add_argument('layer', type=str, help='id of the layer the cartogram should be generated from')
+parser_cartogram.add_argument('attr', type=str, help='the attribute that identifies the map features')
+parser_cartogram.add_argument('data', type=argparse.FileType('r'), help='csv file')
+parser_cartogram.add_argument('key', type=str, help='the column that contains the keys to identify features')
+parser_cartogram.add_argument('val', type=str, help='the column that contains the values for each feature')
+
+
from kartograph import Kartograph
from cartogram import Cartogram
-import sys, os, os.path, getopt, json, time
-
+import time
+import sys
-def main():
- start = time.time()
- from errors import KartographError
-
- if len(sys.argv) < 3:
- print "try: kartograph svg map-config.yaml"
- sys.exit(1)
-
- command = sys.argv[1]
-
- if command in ("generate", "kml", "svg"):
-
- cfg = {}
- output = None
- opt_src = None
- opts, args = getopt.getopt(sys.argv[2:], 'c:o:', ['config=', 'output='])
- for o, a in opts:
- if o in ('-c', '--config'):
- opt_src = a
- elif o in ('-o', '--output'):
- output = a
-
- # check and load map configuration
- if os.path.exists(opt_src):
- t = open(opt_src, 'r').read()
- if opt_src[-5:].lower() == '.json':
- cfg = json.loads(t)
- elif opt_src[-5:].lower() == '.yaml':
- import yaml
- cfg = yaml.load(t)
- else:
- raise KartographError('supported config formats are .json and .yaml')
+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:
- raise KartographError('configuration not found')
+ return cfg
+ else:
+ raise KartographError('supported config formats are .json and .yaml')
- K = Kartograph()
- try:
- if command == "kml":
- K.generate_kml(cfg, output)
- else:
- K.generate(cfg, output)
- except KartographError as e:
- print e
+def svg(args):
+ cfg = parse_config(args.config)
+ K = Kartograph()
+ K.generate(cfg, args.output)
+
- elapsed = (time.time() - start)
+def kml(args):
+ cfg = parse_config(args.config)
+ K = Kartograph()
+ K.generate_kml(cfg, args.config)
- print 'execution time: %.4f secs' % elapsed
- sys.exit(0)
- elif command == "cartogram":
+def cartogram(args):
+ C = Cartogram()
+ C.generate(args.map, args.attr, args.data, args.key, args.val)
- map = sys.argv[2]
- attr = sys.argv[3]
- data = sys.argv[4]
- key = sys.argv[5]
- val = sys.argv[6]
- C = Cartogram()
- C.generate(map, attr, data, key, val)
+parser_svg.set_defaults(func=svg)
+parser_kml.set_defaults(func=kml)
+parser_cartogram.set_defaults(func=cartogram)
+
+
+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__":
View
7 kartograph/geometry/__init__.py
@@ -3,13 +3,12 @@
geometry package
"""
-__all__ = ['Feature','Geometry','SolidGeometry','MultiPolygon','BBox','Point','View','Line']
+__all__ = ['Feature', 'Geometry', 'SolidGeometry', 'MultiPolygon', 'BBox', 'Point', 'View', 'Line', 'PolyLine']
from feature import Feature
from geometry import Geometry, SolidGeometry
-from polygon import Polygon, MultiPolygon
+from polygon import MultiPolygon
from point import Point
from bbox import BBox
from view import View
-from line import Line
-
+from line import Line, PolyLine
View
1  kartograph/geometry/bbox.py
@@ -1,6 +1,7 @@
from point import Point
+
class BBox(object):
"""
2D bounding box
View
2  kartograph/geometry/feature.py
@@ -80,7 +80,7 @@ def to_kml(self, round, attributes=[]):
import unicodedata
if isinstance(val, str):
val = unicode(val, errors='ignore')
- val = unicodedata.normalize('NFKD', val).encode('ascii','ignore')
+ val = unicodedata.normalize('NFKD', val).encode('ascii', 'ignore')
xt.append(KML.Data(
KML.value(val),
name=cfg['tgt']
View
136 kartograph/geometry/line.py
@@ -3,13 +3,105 @@
from geometry import Geometry
+class PolyLine(Geometry):
+ """ polyline """
+ def __init__(self, contours):
+ self.contours = contours
+
+ def bbox(self):
+ """
+ returns the bounding box of the line
+ """
+ bbox = BBox()
+ for pts in self.contours:
+ for pt in pts:
+ bbox.update(pt)
+ return bbox
+
+ def project(self, proj):
+ """
+ projects the line to a map projection
+ """
+ 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)
+
+ def project_view(self, view):
+ """
+ transforms the line to a new 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)
+
+ def to_svg(self, svg, round):
+ """
+ constructs a svg representation of this line
+ """
+ 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
+
+ def update(self):
+ """
+ is called after the points of this geometry have been
+ changed from outside this class
+ """
+ pass
+
+
class Line(Geometry):
"""
simple line (= list of points)
"""
def __init__(self, points):
self.pts = points
-
+
def bbox(self):
"""
returns the bounding box of the line
@@ -18,7 +110,7 @@ def bbox(self):
for pt in self.pts:
bbox.update(pt)
return bbox
-
+
def project(self, proj):
"""
projects the line to a map projection
@@ -29,8 +121,7 @@ def project(self, proj):
if p is not None:
pts.append(p)
return Line(pts)
-
-
+
def project_view(self, view):
"""
transforms the line to a new view
@@ -40,50 +131,45 @@ def project_view(self, view):
p = view.project(pt)
pts.append(p)
return Line(pts)
-
-
+
def to_svg(self, svg, round):
"""
constructs a svg representation of this line
"""
path_str = ""
- if round is False: fmt = '%f,%f'
- else:
- fmt = '%.'+str(round)+'f'
- fmt = fmt+','+fmt
-
+ 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"
+ if path_str == "":
+ path_str = "M"
+ else:
+ path_str += "L"
path_str += fmt % (pt.x, pt.y)
-
+
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]
-
-
+
def update(self):
"""
is called after the points of this geometry have been
changed from outside this class
"""
pass
-
-
View
63 kartograph/kartograph.py
@@ -20,10 +20,10 @@ def generate(self, opts, outfile=None, preview=True):
generates svg map
"""
parse_options(opts)
+ self._verbose = 'verbose' in opts and opts['verbose']
self.prepare_layers(opts)
proj = self.get_projection(opts)
- print proj
bounds_poly = self.get_bounds(opts, proj)
bbox = bounds_poly.bbox()
@@ -105,7 +105,7 @@ def get_map_center(self, opts):
lon0 = data[0] + 0.5 * (data[2] - data[0])
lat0 = data[1] + 0.5 * (data[3] - data[1])
- elif mode == 'points':
+ elif mode[:5] == 'point':
lon0 = 0
lat0 = 0
m = 1 / len(data)
@@ -113,10 +113,14 @@ def get_map_center(self, opts):
lon0 += m * lon
lat0 += m * lat
- elif mode == 'polygons':
+ elif mode[:4] == 'poly':
features = self.get_bounds_polygons(opts)
- if isinstance(features[0].geom, SolidGeometry):
- (lon0, lat0) = features[0].geom.centroid()
+ if len(features) > 0:
+ if isinstance(features[0].geom, SolidGeometry):
+ (lon0, lat0) = features[0].geom.centroid()
+ else:
+ lon0 = 0
+ lat0 = 0
return (lon0, lat0)
def get_bounds(self, opts, proj):
@@ -130,6 +134,9 @@ def get_bounds(self, opts, proj):
mode = bnds['mode'][:]
data = bnds['data']
+ if self._verbose:
+ print 'bounds mode', mode
+
if mode == "bbox": # catch special case bbox
sea = proj.sea_shape(data)
spoly = MultiPolygon(sea)
@@ -139,17 +146,19 @@ def get_bounds(self, opts, proj):
bbox = BBox()
- if mode == "points":
+ if mode[:5] == "point":
for lon, lat in data:
pt = proj.project(lon, lat)
bbox.update(pt)
- if mode == "polygons":
+ if mode[:4] == "poly":
features = self.get_bounds_polygons(opts)
- for feature in features:
- fbbox = feature.geom.project(proj).bbox(data["min-area"])
- bbox.join(fbbox)
-
+ if len(features) > 0:
+ for feature in features:
+ fbbox = feature.geom.project(proj).bbox(data["min-area"])
+ bbox.join(fbbox)
+ else:
+ raise KartographError('no features found for calculating the map bounds')
bbox.inflate(bbox.width * bnds['padding'])
return bbox_to_polygon(bbox)
@@ -170,7 +179,7 @@ def get_bounds_polygons(self, opts):
filter = None
else:
filter = lambda rec: rec[attr] in data['values']
- features = layer.get_features(filter)
+ features = layer.get_features(filter=filter)
return features
def get_view(self, opts, bbox):
@@ -199,21 +208,24 @@ def get_features(self, layer, proj, view, opts, view_poly):
src = self.layers[id]
is_projected = False
+ bbox = [-180, -90, 180, 90]
+ if opts['bounds']['mode'] == "bbox":
+ bbox = opts['bounds']['data']
+ if 'crop' in opts['bounds']:
+ bbox = opts['bounds']['crop']
+
if 'src' in layer: # regular geodata layer
if layer['filter'] is False:
filter = None
else:
filter = lambda rec: filter_record(layer['filter'], rec)
- features = src.get_features(filter)
+ features = src.get_features(filter=filter, bbox=bbox)
elif 'special' in layer: # special layers need special treatment
if layer['special'] == "graticule":
- bbox = [-180, -90, 180, 90]
- if opts['bounds']['mode'] == "bbox":
- bbox = opts['bounds']['data']
lats = layer['latitudes']
lons = layer['longitudes']
- features = src.get_features(lats, lons, proj, bbox)
+ features = src.get_features(lats, lons, proj, bbox=bbox)
elif layer['special'] == "sea":
features = src.get_features(proj.sea_shape())
@@ -235,7 +247,7 @@ def init_svg_canvas(self, opts, proj, view, bbox):
w = view.width
h = view.height + 2
- svg = svgdoc.Document(width='%dpx' % w, height='%dpx' % h, viewBox='0 0 %d %d' % (w, h), enable_background='new 0 0 %d %d' % (w, h), style='stroke-width:0.7px; stroke-linejoin: round; stroke:#000; fill:#f6f3f0;')
+ svg = svgdoc.Document(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; } '
@@ -267,7 +279,7 @@ def simplify_layers(self, layers, layerFeatures, layerOpts):
for feature in layerFeatures[id]:
feature.geom.unify(point_store, layerOpts[id]['unify-precision'])
- #print 'unified points:', point_store['removed'], (100*point_store['removed']/(point_store['removed']+point_store['kept'])),'%'
+ # print 'unified points:', point_store['removed'], (100*point_store['removed']/(point_store['removed']+point_store['kept'])),'%'
to_simplify = []
for id in layers:
@@ -277,7 +289,8 @@ def simplify_layers(self, layers, layerFeatures, layerOpts):
to_simplify.sort(key=lambda id: layerOpts[id]['simplify'])
for id in to_simplify:
- print 'simplifying', id
+ if self._verbose:
+ print 'simplifying', id
for feature in layerFeatures[id]:
for pts in feature.geom.points():
simplify_distance(pts, layerOpts[id]['simplify'])
@@ -386,7 +399,8 @@ def store_layers_svg(self, layers, layerOpts, layerFeatures, svg, opts):
store features in svg
"""
for id in layers:
- print id
+ if self._verbose:
+ print id
if len(layerFeatures[id]) == 0:
continue # ignore empty layers
g = svg.node('g', svg.root, id=id)
@@ -441,10 +455,10 @@ def generate_kml(self, opts, outfile=None):
self.store_layers_kml(layers, layerOpts, layerFeatures, kml, opts)
if outfile is None:
- outfile = 'tmp.kml'
+ outfile = open('tmp.kml', 'w')
from lxml import etree
- open(outfile, 'w').write(etree.tostring(kml, pretty_print=True))
+ outfile.write(etree.tostring(kml, pretty_print=True))
def init_kml_canvas(self):
from pykml.factory import KML_ElementMaker as KML
@@ -462,7 +476,8 @@ def store_layers_kml(self, layers, layerOpts, layerFeatures, kml, opts):
from pykml.factory import KML_ElementMaker as KML
for id in layers:
- print id
+ if self._verbose:
+ print id
if len(layerFeatures[id]) == 0:
continue # ignore empty layers
g = KML.Folder(
View
4 kartograph/layersource/layersource.py
@@ -4,7 +4,5 @@ class LayerSource:
"""
base class for layer source data providers (e.g. shapefiles)
"""
- def get_features(self, attr=None, filter=None):
+ def get_features(self, attr=None, filter=None, bbox=None):
raise NotImplementedError()
-
-
View
41 kartograph/layersource/shplayer.py
@@ -1,6 +1,7 @@
from layersource import LayerSource
-from kartograph import errors
+from os.path import basename
+
class ShapefileLayer(LayerSource):
"""
@@ -38,18 +39,21 @@ def get_shape(self, i):
"""
returns a shape of this shapefile. if requested for the first time, the shape is loaded from shapefile (slow)
"""
- if i in self.shapes: # check cache
+ if i in self.shapes: # check cache
shp = self.shapes[i]
- else: # load shape from shapefile
+ else: # load shape from shapefile
shp = self.shapes[i] = self.sr.shapeRecord(i).shape
return shp
- def get_features(self, filter=None):
+ def get_features(self, attr=None, filter=None, bbox=None):
"""
returns a list of features matching to the attr -> value pair
"""
- from kartograph.geometry import Feature
+ from kartograph.geometry import Feature, BBox
res = []
+ if bbox is not None:
+ 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)):
@@ -66,10 +70,19 @@ def get_features(self, filter=None):
if shp.shapeType == 5: # multi-polygon
geom = points2polygon(shp)
+ elif shp.shapeType == 3: # line
+ geom = points2line(shp)
+ else:
+ print 'unknown shape type', shp.shapeType
+
+ if bbox is not None and not bbox.intersects(geom.bbox()):
+ ignored += 1
+ continue # ignore if not within bounds
feature = Feature(geom, props)
res.append(feature)
-
+ if bbox is not None and ignored > 0:
+ print "[%s] ignored %d shapes (not in bounds)" % (basename(self.shpSrc), ignored)
return res
@@ -81,8 +94,8 @@ def points2polygon(shp):
parts = shp.parts[:]
parts.append(len(shp.points))
contours = []
- for j in range(len(parts)-1):
- pts = shp.points[parts[j]:parts[j+1]]
+ for j in range(len(parts) - 1):
+ pts = shp.points[parts[j]:parts[j + 1]]
pts_ = []
lpt = None
for pt in pts:
@@ -94,3 +107,15 @@ def points2polygon(shp):
contours.append(pts_)
poly = MultiPolygon(contours)
return poly
+
+
+def points2line(shp):
+ """ converts a shapefile line to geometry.Line """
+ 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)
View
15 kartograph/layersource/special/graticule.py
@@ -33,17 +33,18 @@ def xfrange(start, stop, step):
for lon in xfrange(-180, 181, 0.5):
if lon < minLon or lon > maxLon:
continue
- if isinstance(proj, Azimuthal):
- lon += proj.lon0
- if lon < -180:
- lon += 360
- if lon > 180:
- lon -= 360
+ #if isinstance(proj, Azimuthal):
+ # lon += proj.lon0
+ # if lon < -180:
+ # lon += 360
+ # if lon > 180:
+ # lon -= 360
if proj._visible(lon, lat):
pts.append((lon, lat))
if len(pts) > 1:
line = Feature(Line(pts), props)
line_features.append(line)
+ print line_features
# longitudes
for lon in longitudes:
@@ -54,7 +55,7 @@ def xfrange(start, stop, step):
#lat_range = xfrange(step[0], 181-step[0],1)
#if lon % 90 == 0:
# lat_range = xfrange(0, 181,1)
- for lat in xfrange(0, 181, 1):
+ for lat in xfrange(0, 181, 0.5):
lat_ = lat - 90
if lat_ < minLat or lat_ > maxLat:
continue
View
1  kartograph/options.py
@@ -182,6 +182,7 @@ def parse_layer_graticule(layer):
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)):
View
8 kartograph/proj/azimuthal/satellite.py
@@ -100,7 +100,7 @@ def attrs(self):
return p
def _truncate(self, x, y):
- theta = math.atan2(y - self.r, x - self.r)
- x1 = self.r + self.r * math.cos(theta)
- y1 = self.r + self.r * math.sin(theta)
- return (x1, y1)
+ #theta = math.atan2(y - self.r, x - self.r)
+ #x1 = self.r + self.r * math.cos(theta)
+ #y1 = self.r + self.r * math.sin(theta)
+ return (x, y)
View
9 kartograph/svg.py
@@ -36,10 +36,11 @@ def preview(self):
from subprocess import call
call(["firefox", "tmp.svg"])
- def save(self, filename):
- f = open(filename, 'w')
- f.write(self.doc.toxml())
- f.close()
+ def save(self, outfile):
+ if isinstance(outfile, str):
+ outfile = open(outfile, 'w')
+ outfile.write(self.doc.toxml())
+ outfile.close()
def tostring(self):
return self.doc.toxml()
View
2  setup.py
@@ -7,7 +7,7 @@
setup(
name='kartograph.py',
- version='0.1.7',
+ version='0.2.0',
description="Open Source Python library for generating semantic SVG maps",
long_description=long_desc,
classifiers=[

No commit comments for this range

Something went wrong with that request. Please try again.