aboutsummaryrefslogtreecommitdiff
path: root/docs/plot/plot.py
diff options
context:
space:
mode:
Diffstat (limited to 'docs/plot/plot.py')
-rw-r--r--docs/plot/plot.py312
1 files changed, 241 insertions, 71 deletions
diff --git a/docs/plot/plot.py b/docs/plot/plot.py
index 2ccb2814..8eebad24 100644
--- a/docs/plot/plot.py
+++ b/docs/plot/plot.py
@@ -1,39 +1,132 @@
'''
-Plot map data in different projections supported by proj.4
+Plot map data in different projections supported by PROJ.4
-Can be called either as
+Call with:
-> python plot.py definitions.txt [out]
+> python plot.py plotdefs outdir [plotname, [plotname, [plotname, ...]]]
-or
+The output dir is optional. Uses current dir if not set.
-> python plotproj.py "+proj=lcc +lat_1=20" [out]
+Inputs:
+------
+
+ plotdefs: A JSON file with setup for each auto-generated plot.
+ outdir: Directory to put the plots in.
+ plotname: A list of plot names within the plotdefs file. More
+ than one plotname can be given at once.
+
+Plot definitions:
+----------------
+
+Plots are defined by a JSON dict. A plot is defined by a name, filename,
+bounding box (lat/long space), a proj-string, plottype and resolution.
+An example of the setup for the plot for the Airy projection is shown below:
+
+ {
+ "filename": "airy.png",
+ "latmax": 90,
+ "latmin": -90,
+ "lonmax": 90,
+ "lonmin": -90,
+ "name": "airy",
+ "projstring": "+proj=airy",
+ "res": "low",
+ "type": "poly"
+ }
+
+"res" can be either "low" or "med" and "type" can be either "poly"(gon)
+or "line". The rest of the inputs are fairly free form.
-The output dir is optional. Uses current dir if not set.
-Change PROJ and COASTLINE_DATA to path on your local system
-before running the script.
+Change PROJ to path on your local system before running the script.
'''
+from __future__ import print_function
+from __future__ import division
+
import os
+import os.path
import sys
import json
import subprocess
+import functools
import numpy as np
import matplotlib.pyplot as plt
+from matplotlib.patches import Polygon
+import fiona
+from shapely.geometry import Polygon
+from shapely.geometry import MultiPolygon
+from shapely.geometry import shape
+from shapely.ops import transform
+from descartes import PolygonPatch
+
+PROJ = '../../src/proj'
+#PROJ = 'proj'
+
+LINE_LOW = 'data/coastline.geojson'
+LINE_MED = 'data/coastline50.geojson'
+POLY_LOW = 'data/land.geojson'
+POLY_MED = 'data/land50.geojson'
-PROJ = 'proj'
+GRATICULE_WIDTH = 15
+N_POINTS = 1000
-COASTLINE_DATA = 'data/coastline.geojson'
+# colors
+COLOR_LAND = '#000000'
+COLOR_COAST = '#000000'
+COLOR_GRAT = '#888888'
-GRATICULE_WIDTH = 15
-N_POINTS = 100
+def interp_coords(coords, tol):
+ '''
+ Interpolates extra coordinates when the euclidean distance between adjacent
+ points are larger than given tolerance.
+
+ The function tries to densify a list of coordinates based on a simple measure.
+ If two adjacent points in the input coordinate list are further away from each
+ other than the specified tolerance, the list will be densied between those two
+ points. Roughly speaking, a point will be inserted every `tol` between the
+ points.
+
+ Inputs:
+ -------
+ coords: 2xN numpy array with geodetic coordinates
+ tol: Euclidean distance tolerance.
-LAT_MIN = -90
-LAT_MAX = 90
-LON_MIN = -180
-LON_MAX = 180
+ Returns:
+ --------
+ A densified numpy array of coordinates. If tol < 0 the input array is returned
+ unchanged.
+
+ '''
+ if tol < 0:
+ return coords
+
+ x, y = coords
+ x = np.array(x)
+ y = np.array(y)
+
+ l = len(x)
+ dsts = np.sqrt((x[0:-1]-x[1:l])**2 + (y[0:-1]-y[1:l])**2)
+ I = dsts > tol
+
+ offset = 0
+ xy = np.ndarray(shape=(2, 0))
+ for i, b in enumerate(I):
+ xy = np.append(xy, (x[i], y[i]))
+ if not b:
+ continue
+
+ n = round(dsts[i] / tol/2)
+ x1 = np.linspace(x[i], x[i+1], n+2)
+ y1 = np.linspace(y[i], y[i+1], n+2)
+ xy = np.append(xy, zip(x1[1:], y1[1:]))
+
+ # the last coordinate is not added in the loop above
+ #xy = np.append(xy, (x[i+1], y[i+1])).reshape((-1,2)).tolist()
+ xy = xy.reshape((-1, 2)).tolist()
+
+ return xy
def project(coordinates, proj_string, in_radians=False):
'''
@@ -63,7 +156,14 @@ def project(coordinates, proj_string, in_radians=False):
stdout, _ = proc.communicate(coordinates.tobytes())
out = np.frombuffer(stdout, dtype=np.double)
- return np.reshape(out, coordinates.shape)
+ return np.reshape(out, (-1, 2))
+
+def project_xy(x, y, proj_string):
+ '''
+ Wrapper for project() that works with shapely.ops.transform().
+ '''
+ a = project(zip(x, y), proj_string)
+ return zip(*a)
def meridian(lon, lat_min, lat_max):
'''
@@ -83,55 +183,113 @@ def parallel(lat, lon_min, lon_max):
coords[:, 1] = lat
return coords
-def plotproj(proj_str, coastline, frame, graticule, outdir):
+def build_graticule(lonmin=-180, lonmax=180, latmin=-85, latmax=85):
+ '''
+ Build graticule
+ '''
+ # we might get unwanted floats
+ lonmin = int(lonmin)
+ lonmax = int(lonmax)
+ latmin = int(latmin)
+ latmax = int(latmax)
+ graticule = []
+
+ for lon in range(lonmin, lonmax+1, GRATICULE_WIDTH):
+ graticule.append(meridian(lon, latmin, latmax))
+
+ for lat in range(latmin, latmax+1, GRATICULE_WIDTH):
+ graticule.append(parallel(lat, lonmin, lonmax))
+
+ return graticule
+
+
+def resample_polygon(polygon):
+ '''
+ Use interp_coords() to resample (multi)polygons.
+ '''
+ xy = polygon.exterior.coords.xy
+ ext = interp_coords(xy, 2)
+ # interiors
+ rings = []
+ for int_ring in polygon.interiors:
+ rings.append(interp_coords(int_ring.coords.xy, 2))
+ return Polygon(ext, rings)
+
+def plotproj(plotdef, data, outdir):
'''
Plot map.
'''
fig = plt.figure()
axes = fig.add_subplot(111)
+ bounds = (plotdef['lonmin'], plotdef['latmin'], plotdef['lonmax'], plotdef['latmax'])
+ for geom in data.filter(bbox=bounds):
+ temp_pol = shape(geom['geometry'])
+
+ box = Polygon([
+ (plotdef['lonmin'], plotdef['latmin']),
+ (plotdef['lonmin'], plotdef['latmax']),
+ (plotdef['lonmax'], plotdef['latmax']),
+ (plotdef['lonmax'], plotdef['latmin']),
+ ])
+ temp_pol = temp_pol.intersection(box)
+
+ if plotdef['type'] == 'poly':
+ if isinstance(temp_pol, MultiPolygon):
+ polys = []
+ for polygon in temp_pol:
+ polys.append(resample_polygon(polygon))
+ pol = MultiPolygon(polys)
+ else:
+ pol = resample_polygon(temp_pol)
+ else:
+ pol = temp_pol
+
+ trans = functools.partial(project_xy, proj_string=plotdef['projstring'])
+ proj_geom = transform(trans, pol)
+
+ if plotdef['type'] == 'poly':
+ patch = PolygonPatch(proj_geom, fc=COLOR_LAND, zorder=0)
+ axes.add_patch(patch)
+ else:
+ x, y = proj_geom.xy
+ plt.plot(x, y, color=COLOR_COAST, linewidth=0.5)
+
# Plot frame
+ frame = []
+ frame.append(parallel(plotdef['latmin'], plotdef['lonmin'], plotdef['lonmax']))
+ frame.append(parallel(plotdef['latmax'], plotdef['lonmin'], plotdef['lonmax']))
for line in frame:
- line = project(line, proj_str)
+ line = project(line, plotdef['projstring'])
x, y = zip(*line)
plt.plot(x, y, '-k')
- # Plot coastline
- for feature in coastline['features']:
- C = np.array(feature['geometry']['coordinates'])
-
- if np.any(C[:, 0] > 180.0):
- C[C[:, 0] > 180.0, 0] = 180.0
-
- if np.any(C[:, 0] < -180.0):
- C[C[:, 0] < -180.0, 0] = -180.0
-
- if np.any(C[:, 1] > 90.0):
- C[C[:, 1] > 90.0, 1] = 90.0
-
- if np.any(C[:, 1] < -90.0):
- C[C[:, 1] < -90.0, 1] = -90.0
-
- coords = project(C, proj_str)
- x, y = zip(*coords)
- plt.plot(x, y, '-k', linewidth=0.5)
+ graticule = build_graticule(
+ plotdef['lonmin'],
+ plotdef['lonmax'],
+ plotdef['latmin'],
+ plotdef['latmax'],
+ )
# Plot graticule
- graticule = project(graticule, proj_str)
for feature in graticule:
+ feature = project(feature, plotdef['projstring'])
x, y = zip(*feature)
- plt.plot(x, y, '-k', linewidth=0.2)
+ plt.plot(x, y, color=COLOR_GRAT, linewidth=0.4)
axes.axis('off')
- font = {'family': 'serif',
- 'color': 'black',
- 'style': 'italic',
- 'size': 12}
- plt.suptitle(proj_str, fontdict=font)
+ font = {
+ 'family': 'serif',
+ 'color': 'black',
+ 'style': 'italic',
+ 'size': 12
+ }
+ plt.suptitle(plotdef['projstring'], fontdict=font)
plt.autoscale(tight=True)
- name = proj_str.split()[0].split('=')[1]
- plt.savefig(outdir + '/' + name + '.png', dpi=300)
+ if not os.path.exists(outdir):
+ os.makedirs(outdir)
+ plt.savefig(outdir + '/' + plotdef['filename'], dpi=300)
# Clean up
fig = None
@@ -140,38 +298,50 @@ def plotproj(proj_str, coastline, frame, graticule, outdir):
del axes
plt.close()
-if __name__ == "__main__":
+
+def main():
'''
+ Main function of plotting script.
+
+ Parses json-file with plot setups and runs the plotting
+ for each plot setup.
'''
- with open(COASTLINE_DATA) as data:
- coastline = json.load(data)
+ data = {
+ ('line', 'low'): fiona.open(LINE_LOW),
+ ('line', 'med'): fiona.open(LINE_MED),
+ ('poly', 'low'): fiona.open(POLY_LOW),
+ ('poly', 'med'): fiona.open(POLY_MED),
+ }
if os.path.exists(sys.argv[1]):
- # it must be a file with proj-strings
- with open(sys.argv[1]) as projs:
- projs = projs.readlines()
+ # first argument is the JSON plot definition setup file
+ with open(sys.argv[1]) as plotsetup:
+ plotdefs = json.load(plotsetup)
else:
- #assumes it is a single roj string
- projs = [sys.argv[1]]
+ raise ValueError('No plot definition file entered')
- outdir = '.'
- if len(sys.argv) > 2:
- outdir = sys.argv[2]
+ plots = []
+ # second argument is the output dir
+ outdir = sys.argv[2]
- frame = []
- frame.append(parallel(LAT_MIN, LON_MIN, LON_MAX))
- frame.append(parallel(LAT_MAX, LON_MIN, LON_MAX))
+ # subsecond arguments are (optional) names of plot in plotdef.json
+ if len(sys.argv) > 3:
+ plots = sys.argv[3:len(sys.argv)]
- # build graticule
- graticule = []
- for lon in range(LON_MIN, LON_MAX+1, GRATICULE_WIDTH):
- graticule.append(meridian(lon, LAT_MIN, LAT_MAX))
+ for i, plotdef in enumerate(plotdefs):
+ if plots != [] and plotdef['name'] not in plots:
+ continue
- for lat in range(LAT_MIN, LAT_MAX+1, GRATICULE_WIDTH):
- graticule.append(parallel(lat, LON_MIN, LON_MAX))
+ print(i, plotdef['projstring'])
+ if 'skip' in plotdef.keys():
+ print('skipping')
+ continue
- for i, proj in enumerate(projs):
- proj_str = proj.strip()
- print(i, proj_str)
- plotproj(proj_str, coastline, frame, graticule, outdir)
+ plotproj(plotdef, data[(plotdef['type'], plotdef['res'])], outdir)
+
+ for key in data:
+ data[key].close()
+
+if __name__ == "__main__":
+ sys.exit(main())